diff --git a/1-js/1-getting-started/1-intro/article.md b/1-js/1-getting-started/1-intro/article.md new file mode 100644 index 00000000..0d4171a8 --- /dev/null +++ b/1-js/1-getting-started/1-intro/article.md @@ -0,0 +1,143 @@ +# Введение в JavaScript + +Давайте посмотрим, что такого особенного в JavaScript, почему именно он, и какие еще технологии существуют, кроме JavaScript. +[cut] +## Что такое JavaScript? + +*JavaScript* изначально создавался для того, чтобы сделать web-странички "живыми". +Программы на этом языке называются *скриптами*. Они подключаются напрямую к HTML и, как только загружается страничка -- тут же выполняются. + +**Программы на JavaScript -- обычный текст**. Они не требуют какой-то специальной подготовки. + +В этом плане JavaScript сильно отличается от другого языка, который называется Java. + +[smart header="Почему JavaScript?"] +Когда создавался язык JavaScript, у него изначально было другое название: "LiveScript". Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным. + +Планировалось, что JavaScript будет эдаким "младшим братом" Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения. + +У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся. +[/smart] + +Чтобы читать и выполнять текст на JavaScript, нужна специальная программа -- [интерпретатор](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%BF%D1%80%D0%B5%D1%82%D0%B0%D1%82%D0%BE%D1%80). Процесс выполнения скрипта называют *"интерпретацией"*. + +[smart header="Компиляция и интерпретация, для программистов"] +Строго говоря, для выполнения программ существуют "компиляторы" и "интерпретаторы". + +Компиляторы преобразуют программу в машинный код. Этот машинный код затем распространяется и запускается. + +А интерпретаторы, в частности, встроенный JS-интерпретатор браузера -- получают программу в виде исходного кода. При этом распространяется именно сам исходный код (скрипт). + +Современные интерпретаторы перед выполнением преобразуют JavaScript в машинный код или близко к нему, а уже затем выполняют. +[/smart] + +Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице. + +Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор. + +## Что умеет JavaScript? + +Современный JavaScript -- это "безопасный" язык программирования общего назначения. Он не предоставляет низкоуровневых средств работы с памятью, процессором, так как изначально был ориентирован на браузеры, в которых это не требуется. + +Что же касается остальных возможностей -- они зависят от окружения, в котором запущен JavaScript. + +В браузере JavaScript умеет делать все, что относится к манипуляции со страницей, взаимодействию с посетителем и, в какой-то мере, с сервером: + + + +## Что НЕ умеет JavaScript? + +JavaScript -- быстрый и мощный язык, но браузер накладывает на его исполнение некоторые ограничения.. + +Это сделано для безопасности пользователей, чтобы злоумышленник не мог с помощью JavaScript получить личные данные или как-то навредить компьютеру пользователя. + +Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере. Кроме того, различные браузеры предоставляют свои механизмы по установке плагинов и расширений, которые обладают расширенными возможностями, но требуют специальных действий по установке от пользователя + +**Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.** + + + + + +## В чем уникальность JavaScript? + +Есть как минимум *три* замечательных особенности JavaScript: + +[compare] ++Полная интеграция с HTML/CSS. ++Простые вещи делаются просто. ++Поддерживается всеми распространенными браузерами и включен по умолчанию. +[/compare] + +**Этих трёх вещей одновременно нет больше ни в одной браузерной технологии.** Поэтому JavaScript и является самым распространенным средством создания браузерных интерфейсов. + +## Тенденции развития. + +Перед тем, как вы планируете изучить новую технологию, полезно ознакомиться с ее развитием и перспективами. Здесь в JavaScript всё более чем хорошо. + +### HTML 5 + +*HTML 5* -- эволюция стандарта HTML, добавляющая новые теги и, что более важно, ряд новых возможностей браузерам. + +Вот несколько примеров: + + +Многие возможности HTML5 всё ещё в разработке, но браузеры постепенно начинают их поддерживать. + +[summary]Тенденция: JavaScript становится всё более и более мощным и возможности браузера растут в сторону десктопных приложений.[/summary] + +### EcmaScript 6 + +Сам язык JavaScript улучшается. Современный стандарт EcmaScript 5 включает в себя новые возможности для разработки, EcmaScript 6 будет шагом вперёд в улучшении синтаксиса языка. + +Современные браузеры улучшают свои движки, чтобы увеличить скорость исполнения JavaScript, исправляют баги и стараются следовать стандартам. + +[summary]Тенденция: JavaScript становится всё быстрее и стабильнее.[/summary] + +Очень важно то, что новые стандарты HTML5 и ECMAScript сохраняют максимальную совместимость с предыдущими версиями. Это позволяет избежать неприятностей с уже существующими приложениями. + +Впрочем, небольшая проблема с HTML5 всё же есть. Иногда браузеры стараются включить новые возможности, которые еще не полностью описаны в стандарте, но настолько интересны, что разработчики просто не могут ждать. + +...Однако, со временем стандарт меняется и браузерам приходится подстраиваться к нему, что может привести к ошибкам в уже написанном (старом) коде. Поэтому следует дважды подумать перед тем, как применять на практике такие "супер-новые" решения. + +При этом все браузеры сходятся к стандарту, и различий между ними уже гораздо меньше, чем всего лишь несколько лет назад. + +[summary]Тенденция: всё идет к полной совместимости со стандартом.[/summary] + +## Недостатки JavaScript + +Зачастую, недостатки подходов и технологий -- это обратная сторона их полезности. Стоит ли упрекать молоток в том, что он -- тяжелый? Да, неудобно, зато гвозди забиваются лучше. + +В JavaScript, однако, есть вполне объективные недоработки, связанные с тем, что язык, по выражению его автора (Brendan Eich) делался "за 10 бессонных дней и ночей". Поэтому некоторые моменты продуманы плохо, есть и откровенные ошибки (которые признает тот же Brendan). + +Конкретные примеры мы увидим в дальнейшем, т.к. их удобнее обсуждать в процессе освоения языка. + +Пока что нам важно знать, что некоторые "странности" языка не являются чем-то очень умным, а просто не были достаточно хорошо продуманы в своё время. В этом учебнике мы будем обращать особое внимание на основные недоработки и "грабли". Ничего критичного в них нет, если знаешь -- не наступишь. + +**В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают.** + +Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше. + diff --git a/1-js/1-getting-started/1-intro/jslimit.jpg b/1-js/1-getting-started/1-intro/jslimit.jpg new file mode 100755 index 00000000..12753ea9 Binary files /dev/null and b/1-js/1-getting-started/1-intro/jslimit.jpg differ diff --git a/1-js/1-getting-started/2-alternatives/allow.png b/1-js/1-getting-started/2-alternatives/allow.png new file mode 100755 index 00000000..42f874de Binary files /dev/null and b/1-js/1-getting-started/2-alternatives/allow.png differ diff --git a/1-js/1-getting-started/2-alternatives/article.md b/1-js/1-getting-started/2-alternatives/article.md new file mode 100644 index 00000000..8d8368d0 --- /dev/null +++ b/1-js/1-getting-started/2-alternatives/article.md @@ -0,0 +1,101 @@ +# Альтернативные браузерные технологии + +Современный JavaScript используется во многих областях. Если говорить о браузерах, то вместе с JavaScript на страницах используются и другие технологии. + +Самые извеcтные -- это Flash, Java, ActiveX/NPAPI. Связка с ними может помочь достигнуть более интересных результатов в тех местах, где браузерный JavaScript пока не столь хорош, как хотелось бы. + +[cut] + +## Java + +Java -- язык общего назначения, на нем можно писать самые разные программы. Для интернет-страниц есть особая возможность - написание *апплетов*. + +*Апплет* -- это программа на языке Java, которую можно подключить к HTML при помощи тега `applet`: + +```html + + + + + +``` + +Такой тег загружает Java-программу из файла `BTApplet.class` и выполняет ее с параметрами `param`. Апплет выполняется в отдельной части страницы, в прямоугольном "контейнере". Все действия пользователя внутри него обрабатывает апплет. Контейнер, впрочем, может быть и спрятан, если апплету нечего показывать. + +Конечно, для этого на компьютере должна быть установлена и включена среда выполнения Java, включая браузерный плагин. Статистика показывает, что это около 80% компьютеров. Кроме того, апплет должен быть подписан сертификатом издателя (в примере выше апплет без подписи), иначе Java заблокирует его. + +**Чем нам, JavaScript-разработчикам, может быть интересен Java?** + +В первую очередь тем, что подписанный Java-апплет может всё то же, что и обычная программа, установлена на компьютере посетителя. + +При попытке сделать потенциально опасное действие -- пользователь получает вопрос, который выглядит примерно так: + + + +Обойти это подтверждение или поменять его внешний вид нельзя. То есть, согласие посетителя действительно необходимо. + +И если оно есть -- то Java-апплет может выполнять любые действия, в отличие от JavaScript. + +[compare] ++Java может делать *всё* от имени посетителя, совсем как установленная программа. В целях безопасности, потенциально опасные действия требуют подписанного апплета и доверия пользователя. +-Java требует больше времени для загрузки. +-Среда выполнения Java, включая браузерный плагин, должна быть установлена на компьютере посетителя и включена. Таких посетителей в интернет -- около 80%. +-Java-апплет не интегрирован с HTML-страницей, а выполняется отдельно. Но он может вызывать функции JavaScript. +[/compare] + +Подписанный Java-апплет -- это возможность делать все, что угодно, на компьютере посетителя, если он вам доверяет. + +Java и JavaScript могут взаимодействовать, так что можно вынести в Java те вызовы, которым нужно обойти контекст безопасности, а для самой страницы использовать JavaScript. + + +## ActiveX/NPAPI, плагины и расширения для браузера + +ActiveX для IE и NPAPI для остальных браузеров позволяют писать плагины для браузера, в том числе на языке C. Как и в ситуации с Java-апплетом, посетитель поставит их в том случае, если вам доверяет. + +Эти плагины могут как отображать содержимое специального формата (плагин для проигрывания музыки, для показа PDF), так и взаимодействовать со страницей. + +ActiveX при этом еще и очень удобен в установке. Лично я - не фанат Microsoft, но видел отличные приложения, написанные на ActiveX и я могу понять, почему люди используют его и привязываются к IE. + +## Adobe Flash + +Adobe Flash -- кросс-браузерная платформа для мультимедиа-приложений, анимаций, аудио и видео. + +*Flash-ролик* -- это скомпилированная программа, написанная на языке ActionScript. Ее можно подключить к HTML-странице и запустить в прямоугольном контейнере. + +В первую очередь Flash полезен тем, что позволяет **кросс-браузерно** работать с микрофоном, камерой, с буфером обмена, а также поддерживает продвинутые возможности по работе с сетевыми соединениями. + +[compare] ++Сокеты, UDP для P2P и другие продвинутые возможности по работе с сетевыми соединениями ++Поддержка мультмедиа: изображения, аудио, видео. Работа с веб-камерой и микрофоном. +-Flash должен быть установлен и включен. А на некоторых устройствах он вообще не поддерживается. +-Flash не интегрирован с HTML-страницей, а выполняется отдельно. +-Существуют ограничения безопасности, однако они немного другие, чем в JavaScript. +[/compare] + +**JavaScript и ActionScript могут вызывать функции друг друга**, поэтому обычно сайты используют JavaScript, а там, где он не справляется -- можно подумать о Flash. + +## Dart + +Язык Dart предложен компанией Google как замена JavaScript, у которого, по выражению создателей Dart, есть [фатальные недостатки](http://lurkmore.to/%D0%A4%D0%B0%D1%82%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%BD%D0%B5%D0%B4%D0%BE%D1%81%D1%82%D0%B0%D1%82%D0%BE%D0%BA). + +Сейчас этот язык, хотя и доступен, находится в стадии разработки. Многие из возможностей еще ожидают своей реализации, есть ряд проблем. Другие ведущие интернет-компании объявляли о своей незаинтересованности в Dart. Но в будущем он может составить конкуренцию JS, если его доведут до ума. + +Программы на языке Dart специальным инструментом могут быть преобразованы в JavaScript. Конечно, при этом часть важных возможностей Dart теряется. + +## CoffeeScript + +Язык [CoffeeScript](http://coffeescript.org) -- это "синтаксический сахар" для JavaScript, который короче и, местами, проще. + +Этот язык напрямую в браузере не работает, но предлагается специальная программа для преобразования CoffeeScript в JavaScript, и при этом, так как CoffeeScript изначально задумывался как преобразуемый в JavaScript, результирующий код выглядит вполне хорошо и работает тоже. + +Есть и другие языки, которые написаны "поверх" JavaScript, например [TypeScript](http://www.typescriptlang.org/). + +Строго говоря, это не альтернативные технологии, для их использования необходимо, как базу, хорошо знать и понимать JavaScript. + +Возможно, вы захотите изучить этот язык после того, как освоите JavaScript, многим он нравится. + +## Итого + +Язык JavaScript уникален благодаря своей полной интеграции с HTML/CSS. Он работает почти у всех посетителей. + +**...Но хороший JavaScript-программист не должен забывать и о других технологиях.** Ведь наша цель -- создание хороших приложений, и здесь Flash, Java, ActiveX/NPAPI имеют свои уникальные возможности, которые можно использовать вместе с JavaScript. diff --git a/1-js/1-getting-started/3-pre-coding/article.md b/1-js/1-getting-started/3-pre-coding/article.md new file mode 100644 index 00000000..ab3cf454 --- /dev/null +++ b/1-js/1-getting-started/3-pre-coding/article.md @@ -0,0 +1,111 @@ +# Справочники и спецификации + +В этом разделе мы познакомимся со справочниками и спецификациями. + +Если вы только начинаете изучение, то вряд ли они будут нужны прямо сейчас. Тем не менее, эта глава находится в начале, так как предсказать точный момент, когда вы захотите заглянуть в справочник -- невозможно, но точно известно, что этот момент настанет. + +Поэтому рекомендуется кратко взглянуть на эту страницу и взять её на заметку, чтобы при необходимости вернуться к ней в будущем. + +[cut] + +## Справочники, и как в них искать + +Самая полная и подробная информация по JavaScript и браузерам есть в справочниках. + +Её объём таков, что перевести все с английского невозможно. Даже сделать "единый полный справочник" не получается, так как изменений много и они постоянно происходят. + +Тем не менее, жить вполне можно если знать, куда смотреть. + +**Есть три основных справочника по JavaScript на английском языке**: + +
    +
  1. [Mozilla Developer Network](https://developer.mozilla.org/) -- содержит информацию, верную для основных браузеров. Также там присутствуют расширения только для Firefox, они помечены. + +Когда мне нужно быстро найти "стандартную" информацию по `RegExp` - ввожу в Google **"RegExp MDN"**, и ключевое слово "MDN" (Mozilla Developer Network) приводит к информации из этого справочника. +
  2. +
  3. [MSDN](http://msdn.microsoft.com) -- справочник от Microsoft. Там много информации, в том числе и по JavaScript (они называют его "JScript"). Если нужно что-то, специфичное для IE -- лучше лезть сразу туда. + +Например, для информации об особенностях `RegExp` в IE -- полезное сочетание: **"RegExp msdn"**. Иногда к поисковой фразе лучше добавить термин "JScript": **"RegExp msdn jscript"**.
  4. +
  5. [Safari Developer Library](https://developer.apple.com/library/safari/navigation/index.html) -- менее известен и используется реже, но в нём тоже можно найти ценную информацию.
  6. +
+ +Есть ещё справочники, не от разработчиков браузеров, но тоже хорошие: + +
    +
  1. [http://help.dottoro.com]() -- содержит подробную информацию по HTML/CSS/JavaScript.
  2. +
  3. [http://javascript.ru/manual]() -- справочник по JavaScript на русском языке, он содержит основную информацию по языку, без функций для работы с документом. К нему можно обращаться и по адресу, если знаете, что искать. Например, так: [http://javascript.ru/RegExp](). +
  4. +
  5. [http://www.quirksmode.org]() -- информация о поддержке тех или иных возможностей и несовместимостях. +Для поиска можно пользоваться комбинацией **"quirksmode onkeypress"** в Google. +
  6. +
+ +## Спецификации + +Спецификация -- это самый главный, определяющий документ, в котором написано, как себя ведёт JavaScript, браузер, CSS и т.п. + +Если что-то непонятно, и справочник не даёт ответ, то спецификация, как правило, раскрывает тему гораздо глубже и позволяет расставить точки над i. + +### Спецификация ECMAScript + +Спецификация (формальное описание синтаксиса, базовых объектов и алгоритмов) языка Javascript называется ECMAScript. + +Ее перевод есть на сайте в разделе [стандарт языка](http://javascript.ru/ecma). + +[smart header="Почему не просто "JavaScript" ?"] +Вы можете спросить: "Почему спецификация для JavaScript не называется просто *"JavaScript"*, зачем существует какое-то отдельное название?" + +Всё потому, что JavaScript™ -- зарегистрированная торговая марка, принадлежащая корпорации Oracle. + +Название "ECMAScript" было выбрано, чтобы сохранить спецификацию независимой от владельцев торговой марки. +[/smart] + +Спецификация может рассказать многое о том, как работает язык, и является самым фундаментальным, доверенным источником информации. + +Мы живем во время, когда все быстро изменяется. Современный стандарт -- это ECMA-262 5.1 (или просто ES5), поддерживается всеми современными браузерами. + +Не за горами -- новая спецификация ES6, в которой предусмотрены еще много полезных возможностей, делающих разработку быстрее и веселее :) + +### Спецификации HTML/CSS + +JavaScript -- язык общего назначения, поэтому в спецификации ECMAScript нет ни слова о браузерах. + +Соответствующую информацию вы можете найти на сайте [w3.org](http://w3.org). Там расположены стандарты HTML, CSS и многие другие. + +К сожалению, найти в этой куче то, что нужно, может быть нелегко, особенно когда неизвестно в каком именно стандарте искать. Самый лучший способ -- попросить Google с указанием сайта. + +Например, для поиска `document.cookie` набрать [document.cookie site:w3.org](https://www.google.com/search?q=document.cookie+site%3Aw3.org). + +Последние версии стандартов расположены на домене [dev.w3.org](http://dev.w3.org). + +## Итого + +Итак, посмотрим какие у нас есть источники информации. + +Справочники: + + +Спецификации содержат важнейшую информацию о том, как оно "должно работать": + + + +То, как оно на самом деле работает и несовместимости: + + \ No newline at end of file diff --git a/1-js/1-getting-started/4-editor/article.md b/1-js/1-getting-started/4-editor/article.md new file mode 100644 index 00000000..2fafff7d --- /dev/null +++ b/1-js/1-getting-started/4-editor/article.md @@ -0,0 +1,70 @@ +# Редакторы для кода + +Для разработки обязательно нужен хороший редактор. + +Тот, который вы выберете должен иметь в своем арсенале: + +
    +
  1. Подсветку синтаксиса.
  2. +
  3. Автодополнение.
  4. +
  5. "Фолдинг" (от англ. folding) -- возможность скрыть-раскрыть блок кода.
  6. +
+ +[cut] +## IDE + +Термин IDE (Integrated Development Environment) -- "интегрированная среда разработки", означает редактор, который расширен большим количеством "наворотов", умеет работать со вспомогательными системами, такими как багтрекер, контроль версий, и много чего ещё. + +Как правило, IDE загружает весь проект целиком, поэтому может предоставлять автодополнение по функциям всего проекта, удобную навигацию по его файлам и т.п. + +Если вы еще не задумывались над выбором IDE, присмотритесь к следующим вариантам. + + + +Почти все они, за исключением Visual Studio, кросс-платформенные. + +Сортировка в этом списке ничего не означает. Выбор осуществляется по вкусу и по другим технологиям, которые нужно использовать вместе с JavaScript. + +Большинство IDE -- платные, с возможностью скачать и бесплатно использовать некоторое время. Но их стоимость, по сравнению с зарплатой веб-разработчика, невелика, поэтому ориентироваться можно на удобство. + +## Лёгкие редакторы + +Лёгкие редакторы -- не такие мощные, как IDE, но они быстрые и простые, мгновенно стартуют. + +Основная сфера применения лёгкого редактора -- мгновенно открыть нужный файл, чтобы что-то в нём поправить. + +На практике "лёгкие" редакторы могут обладать большим количеством плагинов, так что граница между IDE и "лёгким" редактором размыта, спорить что именно редактор, а что IDE -- не имеет смысла. + +Достойны внимания: + + + +## Мои редакторы + +Лично мои любимые редакторы: + + + +Если не знаете, что выбрать -- можно посмотреть на них ;) + +## Не будем ссориться + +В списках выше перечислены редакторы, которые использую я или мои знакомые -- хорошие разработчики. Конечно, существуют и другие отличные редакторы, если вам что-то нравится -- пользуйтесь. + +Выбор редактора, как и любого инструмента, во многом индивидуален и зависит от ваших проектов, привычек, личных предпочтений. \ No newline at end of file diff --git a/1-js/1-getting-started/5-devtools/article.md b/1-js/1-getting-started/5-devtools/article.md new file mode 100644 index 00000000..a4a187bb --- /dev/null +++ b/1-js/1-getting-started/5-devtools/article.md @@ -0,0 +1,112 @@ +# Консоль разработчика + +При разработке скриптов всегда возможны ошибки... Впрочем, что я говорю? У вас абсолютно точно будут ошибки, если конечно вы -- человек, а не робот инопланетный. + +Чтобы читать их в удобном виде, а также получать массу полезной информации о выполнении скриптов, в браузерах есть *инструменты разработки*. + +**Для разработки рекомендуется использовать Chrome или Firefox.** + +Другие браузеры, как правило, находятся в положении "догоняющих" по возможностям встроенных инструментов разработки. Если ошибка, к примеру, именно в Internet Explorer, тогда уже смотрим конкретно в нём, но обычно -- Chrome/Firefox. + +В инструментах разработчика предусмотрена масса возможностей, но на текущем этапе мы просто посмотрим, как их открывать, смотреть в консоли ошибки и запускать команды JavaScript. + +[cut] + +## Google Chrome + +Откройте страницу [bug.html](/devtools/bug.html). + +В её JavaScript-коде есть ошибка. Конечно, обычному посетителю она не видна, нужно открыть инструменты разработчика. + +Для этого используйте сочетание клавиш [key Ctrl+Shift+J], а если у вас Mac, то [key Cmd+Shift+J]. + +При этом откроются инструменты разработчика и вкладка Console, в которой будет ошибка. + +Выглядеть будет примерно так: + + + + + + +Далее в учебнике мы подробнее рассмотрим отладку в Chrome в главе [](/debugging-chrome). + +## Firefox + +Для разработки в Firefox используется расширение Firebug. + +
    +
  1. Первым делом его надо установить. + +Это можно сделать со страницы https://addons.mozilla.org/ru/firefox/addon/firebug/. + +Перезапустите браузер. Firebug появится в правом-нижнем углу окна: + + + +Если иконки не видно -- возможно, у вас выключена панель расширений. Нажмите [key Ctrl+\] для ее показа. + +Ну а если ее нет и там, то нажмите [key F12] -- это горячая клавиша для запуска Firebug, расширение появится, если установлено. +
  2. +
  3. Далее, для того чтобы консоль заработала, её надо включить. + +Если консоль уже была включена ранее, то этот шаг не нужен, но если она серая -- выберите в меню `Консоль` и включите её: + + +
  4. +
  5. Для того, чтобы Firebug работал без глюков, желательно сначала открыть Firebug, а уже потом -- зайти на страницу. + +С открытым Firebug зайдите на страницу с ошибкой: [bug.html](/devtools/bug.html). + +Консоль покажет ошибку: + + + +Кликните на строчке с ошибкой и браузер покажет исходный код. При необходимости включайте дополнительные панели. +
  6. +
+ +Как и в Chrome, можно набирать и запускать команды, область для команд на рисунке находится справа, запуск команд осуществляется нажатием [key Ctrl+Enter] (для Mac -- [key Cmd]). + +Можно перенести её вниз. нажав на кнопочку -- на рисунке она не видна, но есть справа-снизу панели разработки. + +Об основных возможностях можно прочитать на сайте firebug.ru. + +## Internet Explorer + +Панель разработчика запускается нажатием [key F12]. + +Откройте её и зайдите на страницу с ошибкой: [bug.html](/devtools/bug.html). Если вы разобрались с Chrome/Firefox, то дальнейшее будет вам более-менее понятно, так как инструменты IE построены позже и по аналогии с Chrome/Firefox. + +## Safari + +Горячие клавиши: [key Ctrl+Shift+I], [key Ctrl+Alt+C] для Mac -- [key Cmd] вместо [key Ctrl]. + +Для доступа к функционалу разработки через меню: + +
    +
  1. +В Safari первым делом нужно активировать меню разработки. + +Откройте меню, нажав на колесико справа-сверху и выберите `Настройки`. + +Затем вкладка `Дополнительно`: + + +Отметьте `Показывать меню "Разработка" в строке меню`. Закройте настройки. +
  2. +
  3. Нажмите на колесико и выберите `Показать строку меню`. + +Инструменты будут доступны в появившейся строке меню, в пункте `Разработка`.
  4. +
+ +## Итого + +Мы разобрали, как открывать инструменты разработчика и смотреть ошибки, а также запускать простые команды, не отходя от браузера. + +Далее мы приступим к изучению JavaScript. diff --git a/1-js/1-getting-started/5-devtools/chrome.png b/1-js/1-getting-started/5-devtools/chrome.png new file mode 100755 index 00000000..ed2e8928 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/chrome.png differ diff --git a/1-js/1-getting-started/5-devtools/chrome@2x.png b/1-js/1-getting-started/5-devtools/chrome@2x.png new file mode 100755 index 00000000..ed7909e1 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/chrome@2x.png differ diff --git a/1-js/1-getting-started/5-devtools/firebug-gray.png b/1-js/1-getting-started/5-devtools/firebug-gray.png new file mode 100755 index 00000000..1fd5f8fa Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firebug-gray.png differ diff --git a/1-js/1-getting-started/5-devtools/firefox.png b/1-js/1-getting-started/5-devtools/firefox.png new file mode 100755 index 00000000..5bf06dda Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firefox.png differ diff --git a/1-js/1-getting-started/5-devtools/firefox@2x.png b/1-js/1-getting-started/5-devtools/firefox@2x.png new file mode 100755 index 00000000..bf602cd9 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firefox@2x.png differ diff --git a/1-js/1-getting-started/5-devtools/firefox_console_down.png b/1-js/1-getting-started/5-devtools/firefox_console_down.png new file mode 100755 index 00000000..d17cea7b Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firefox_console_down.png differ diff --git a/1-js/1-getting-started/5-devtools/firefox_console_down@2x.png b/1-js/1-getting-started/5-devtools/firefox_console_down@2x.png new file mode 100755 index 00000000..2e5dd021 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firefox_console_down@2x.png differ diff --git a/1-js/1-getting-started/5-devtools/firefox_console_enable.png b/1-js/1-getting-started/5-devtools/firefox_console_enable.png new file mode 100755 index 00000000..aa9300d9 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firefox_console_enable.png differ diff --git a/1-js/1-getting-started/5-devtools/firefox_console_enable@2x.png b/1-js/1-getting-started/5-devtools/firefox_console_enable@2x.png new file mode 100755 index 00000000..a9eb7f26 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/firefox_console_enable@2x.png differ diff --git a/1-js/1-getting-started/5-devtools/safari.png b/1-js/1-getting-started/5-devtools/safari.png new file mode 100755 index 00000000..7fda7429 Binary files /dev/null and b/1-js/1-getting-started/5-devtools/safari.png differ diff --git a/1-js/1-getting-started/index.md b/1-js/1-getting-started/index.md new file mode 100644 index 00000000..b3e8e77d --- /dev/null +++ b/1-js/1-getting-started/index.md @@ -0,0 +1,3 @@ +# Введение + +Про язык JavaScript и окружение для разработки на нём. \ No newline at end of file diff --git a/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.md b/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.md new file mode 100644 index 00000000..95104042 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.md @@ -0,0 +1,19 @@ +Код страницы: + +```html + + + + + + + + + + + + +``` + diff --git a/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.view/index.html b/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.view/index.html new file mode 100644 index 00000000..a3b0b7b1 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/1-hello-alert/solution.view/index.html @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/1-js/2-first-steps/1-hello-world/1-hello-alert/task.md b/1-js/2-first-steps/1-hello-world/1-hello-alert/task.md new file mode 100644 index 00000000..22890483 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/1-hello-alert/task.md @@ -0,0 +1,9 @@ +# Выведите alert + +[importance 5] + +Сделайте страницу, которая выводит "Я - JavaScript!". + +Создайте ее на диске, откройте в браузере, убедитесь, что все работает. + +[demo src="solution"] diff --git a/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/alert.js b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/alert.js new file mode 100755 index 00000000..5dbc7e7b --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/alert.js @@ -0,0 +1 @@ +alert('Я - JavaScript!'); diff --git a/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/index.html b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/index.html new file mode 100755 index 00000000..7b1dac29 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/index.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/solution.md b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/solution.md new file mode 100644 index 00000000..bef2fc5b --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/solution.md @@ -0,0 +1,12 @@ +Код для HTML-файла: + +```html + +``` + +Для файла `alert.js` из той же директории: + +```js +//+ src="alert.js" +``` + diff --git a/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/task.md b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/task.md new file mode 100644 index 00000000..93cf1b4e --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/2-hello-alert-ext/task.md @@ -0,0 +1,7 @@ +# Вывести alert внешним скриптом + +[importance 5] + +Возьмите решение предыдущей задачи [](/task/hello-alert) и вынесите скрипт во внешний файл `alert.js`, который расположите в той же директории. + +Откройте страницу и проверьте, что вывод сообщения всё ещё работает. \ No newline at end of file diff --git a/1-js/2-first-steps/1-hello-world/3-async-defer-first/solution.md b/1-js/2-first-steps/1-hello-world/3-async-defer-first/solution.md new file mode 100644 index 00000000..089a87e0 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/3-async-defer-first/solution.md @@ -0,0 +1,6 @@ +Ответы: +
    +
  1. Первым выполнится `big.js`, это нормальная последовательность выполнения подряд идущих скриптов.
  2. +
  3. Первым выполнится `small.js`, так как скрипты из-за `async` ведут себя совершенно независимо друг от друга, страница тоже от них не зависит.
  4. +
  5. Первым выполнится `big.js`, так как скрипты, подключённые через `defer`, сохраняют порядок выполнения относительно друг друга.
  6. +
\ No newline at end of file diff --git a/1-js/2-first-steps/1-hello-world/3-async-defer-first/task.md b/1-js/2-first-steps/1-hello-world/3-async-defer-first/task.md new file mode 100644 index 00000000..72405f9a --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/3-async-defer-first/task.md @@ -0,0 +1,27 @@ +# Какой скрипт выполнится первым? + +[importance 4] + +В примере ниже подключены два скрипта `small.js` и `big.js`. + +Если предположить, что `small.js` загружается гораздо быстрее, чем `big.js` -- какой выполнится первым? + +```html + + +``` + +А вот так? + +```html + + +``` + +А так? + +```html + + +``` + diff --git a/1-js/2-first-steps/1-hello-world/4-async-defer-inline-first/solution.md b/1-js/2-first-steps/1-hello-world/4-async-defer-inline-first/solution.md new file mode 100644 index 00000000..288b3b6f --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/4-async-defer-inline-first/solution.md @@ -0,0 +1,17 @@ +**Первым выполнится обычный скрипт.** + +Заметим, что атрибуты `defer` и `async` на обычном скрипте будут проигнорированы. То есть, он работает так же, как и без них: + +```html + +``` + +Далее, обычно скрипты с `async/defer` не тормозят обработку страницы. То есть, браузер начнёт их загружать, а сам пойдёт дальше показывать страницу и выполнять скрипты. + +То есть, обычный скрипт в этом случае, очевидно, выполнится первым. + +...Но более того, даже если скрипты `small.js` и `big.js` не нужно загружать, а браузер берёт их из кеша, то он всё равно устроен так, что выполнит их после основных скриптов страницы. + +Таким образом, первым всегда будет обычный скрипт, а вот относительный порядок `small.js` и `big.js` здесь не регламентирован. diff --git a/1-js/2-first-steps/1-hello-world/4-async-defer-inline-first/task.md b/1-js/2-first-steps/1-hello-world/4-async-defer-inline-first/task.md new file mode 100644 index 00000000..e4013123 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/4-async-defer-inline-first/task.md @@ -0,0 +1,17 @@ +# Что выполнится первым из скриптов? + +[importance 4] + +В этой странице есть три скрипта. + +Какой выполнится первым? + +```html + + + + +``` + diff --git a/1-js/2-first-steps/1-hello-world/article.md b/1-js/2-first-steps/1-hello-world/article.md new file mode 100644 index 00000000..0eed1c60 --- /dev/null +++ b/1-js/2-first-steps/1-hello-world/article.md @@ -0,0 +1,315 @@ +# Привет, мир! + +В этой статье мы создадим простой скрипт и посмотрим, как он работает. +[cut] +## Тег SCRIPT + +[smart header="А побыстрее?"] +В том (и только в том!) случае, если читатель нетерпелив и уже разрабатывал на JavaScript или имеет достаточно опыта в другом программировании, он может не читать каждую статью этого раздела, а прыгнуть сразу на главу [](/javascript-specials). Там будет кратко самое основное. + +Если же у вас есть достаточно времени и желание начать с азов, то читайте дальше :) +[/smart] + +Программы на языке JavaScript можно вставить в любое место HTML при помощи тега `SCRIPT`. Например: + +```html + + + + + + + + + +

Начало документа...

+ +*!* + +*/!* + +

...Конец документа

+ + + +``` + +Этот пример использует следующие элементы: + +
+
<script> ... </script>
+
Тег `script` содержит исполняемый код. Предыдущие стандарты HTML требовали обязательного указания атрибута `type`, но сейчас он уже не нужен. Достаточно просто ` +``` + +Браузер, для которого предназначались такие трюки, очень старый Netscape, давно умер. Поэтому в этих комментариях нет нужды. +
+
+ +Итак, для вставки скрипта мы просто пишем ` +``` + +Здесь `/path/to/script.js` -- это абсолютный путь к файлу, содержащему скрипт (из корня сайта). + +Браузер сам скачает скрипт и выполнит. + +Можно указать и полный URL, например: + +```html + +``` + +Вы также можете использовать путь относительно текущей страницы, в частности `src="jquery.js"` обозначает файл из текущей директории. + +Чтобы подключить несколько скриптов, используйте несколько тегов: + +```html + + +... +``` + +[smart] +Как правило, в HTML пишут только самые простые скрипты, а сложные выносят в отдельный файл. + +Браузер скачает его только первый раз и в дальнейшем, при правильной настройке сервера, будет брать из своего [кеша](http://ru.wikipedia.org/wiki/%D0%9A%D1%8D%D1%88). + +Благодаря этому один и тот же большой скрипт, например, меню или библиотека функций, может использоваться на разных страницах без полной перезагрузки с сервера. + +[/smart] + + +[warn header="Если указан атрибут `src`, то содержимое тега игнорируется."] + +В одном теге `SCRIPT` нельзя одновременно подключить внешний скрипт и указать код. + +Вот так не cработает: + +```html + +``` + +Нужно выбрать: либо `SCRIPT` идёт с `src`, либо содержит код. Тег выше следует разбить на два: один -- с `src`, другой -- с кодом, вот так: + +```html + + +``` + +[/warn] + +## Асинхронные скрипты: defer/async + +Обычно тег ` +*/!* + +

Кролики посчитаны!

+ + + +``` + +Такое поведение называют "синхронным". Как правило, оно вполне нормально, но бывает беда. + +Внешние скрипты ` + +

...Важная информация!

+``` + +В примере выше важная информация не покажется, пока не загрузится внешний скрипт. Но действительно ли он так важен, что мы хотим заставить посетителя ждать? Если это реклама или счётчик посещаемости, то вряд ли. + +Можно поставить все подобные скрипты в конец страницы -- это уменьшит проблему, но не избавит от неё полностью, поскольку если какой-то один скрипт тормозит или завис, то последующие будут его ждать. + +Чтобы это обойти, можно использовать для скриптов атрибуты `async` или `defer`: +
+
Атрибут `async`
+
Поддерживается всеми браузерами, кроме IE9-. Скрипт выполняется полностью асинхронно. То есть, при обнаружении ` + +``` + +А в таком коде (с `defer`) первым сработает всегда `1.js`, а скрипт `2.js`, даже если загрузился раньше, будет его ждать. + +```html + + +``` + +Атрибут `defer` используют в тех случаях, когда второй скрипт `2.js` зависит от первого `1.js`, к примеру -- использует что-то, описанное первым скриптом. + +
+
+ +[warn header="Либо `async` либо `defer`"] +Одновременно указывать `async` и `defer` не имеет смысла. + +В этом случае браузер использует только `async`. +[/warn] + +[warn header="Атрибуты `async/defer` -- только для внешних скриптов"] +Атрибуты `async/defer` работают только в том случае, если назначены на внешние скрипты, т.е. имеющие `src`. + +При попытке назначить их на обычные скрипты <script>...</script>, они будут проигнороированы. +[/warn] + +Тот же пример с `async`: + +```html + +

Начало страницы...

+ + + +

...Важная информация!

+``` + +При запуске вы увидите, что вся страница отобразилась тут же, а реклама будет показана позже, когда загрузится скрипт. + +### Разные типы скриптов + +На странице могут одновременно быть: + + +Если на странице используются разные виды подключения скриптов, то: + + +То есть, такие скрипты: + +```html + + + + +``` + +Выполнятся строго в порядке `1.js` -> `3.js` -> `2.js` -> `4.js`. + +А вот такие: + +```html + + + + +``` + +Выполнятся в неопределённом порядке, смотря какой загрузится первым. Единственно, атрибут `defer` гарантирует, что `4.js` запустится после `2.js`. + +**Большинство современных системы рекламы и счётчиков знают про эти атрибуты и используют их.** + +Конечно, система рекламы должна корректно обрабатывать асинхронную загрузку скрипта, но никаких принципиальных проблем с этим нет, в частности, системы от Google и Яндекс используют такой подход по умолчанию. + +## Задачи + +Очень важно не только читать учебник, но делать что-то самостоятельно. + +Решите задачки, чтобы удостовериться, что вы все правильно поняли. + diff --git a/1-js/2-first-steps/10-bitwise-operators/1-bitwise-operator-value/solution.md b/1-js/2-first-steps/10-bitwise-operators/1-bitwise-operator-value/solution.md new file mode 100644 index 00000000..bf0e32fd --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/1-bitwise-operator-value/solution.md @@ -0,0 +1,6 @@ +
    +
  1. Операция `a^b` ставит бит результата в `1`, если на соответствующей битовой позиции в `a` или `b` (но не одновременно) стоит `1`. + +Так как в `0` везде стоят нули, то биты берутся в точности как во втором аргументе.
  2. +
  3. Первое побитовое НЕ `~` превращает `0` в `1`, а `1` в `0`. А второе НЕ превращает ещё раз, в итоге получается как было.
  4. +
\ No newline at end of file diff --git a/1-js/2-first-steps/10-bitwise-operators/1-bitwise-operator-value/task.md b/1-js/2-first-steps/10-bitwise-operators/1-bitwise-operator-value/task.md new file mode 100644 index 00000000..ea53eeab --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/1-bitwise-operator-value/task.md @@ -0,0 +1,13 @@ +# Побитовый оператор и значение + +[importance 5] + +Почему побитовые операции в примерах ниже не меняют число? Что они делают внутри? + +```js +//+ run +alert( 123 ^ 0 ); // 123 +alert( 0 ^ 123 ); // 123 +alert( ~~123 ); // 123 +``` + diff --git a/1-js/2-first-steps/10-bitwise-operators/2-check-integer/solution.md b/1-js/2-first-steps/10-bitwise-operators/2-check-integer/solution.md new file mode 100644 index 00000000..2a53ef36 --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/2-check-integer/solution.md @@ -0,0 +1,14 @@ +Один из вариантов такой функции: + +```js +//+ run +function isInteger(num) { + return (num ^ 0) === num; +} + +alert( isInteger(1) ); // true +alert( isInteger(1.5) ); // false +alert( isInteger(-0.5) ); // false +``` + +Обратите внимание: `num^0` -- в скобках! Это потому, что приоритет операции `^` очень низкий. Если не поставить скобку, то `===` сработает раньше. Получится `num ^ (0 === num)`, а это уже совсем другое дело. \ No newline at end of file diff --git a/1-js/2-first-steps/10-bitwise-operators/2-check-integer/task.md b/1-js/2-first-steps/10-bitwise-operators/2-check-integer/task.md new file mode 100644 index 00000000..03e8505e --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/2-check-integer/task.md @@ -0,0 +1,14 @@ +# Проверка, целое ли число + +[importance 3] + +Напишите функцию `isInteger(num)`, которая возвращает `true`, если `num` -- целое число, иначе `false`. + +Например: + +```js +alert( isInteger(1) ); // true +alert( isInteger(1.5) ); // false +alert( isInteger(-0.5) ); // false +``` + diff --git a/1-js/2-first-steps/10-bitwise-operators/3-bitwise-symmetry/solution.md b/1-js/2-first-steps/10-bitwise-operators/3-bitwise-symmetry/solution.md new file mode 100644 index 00000000..abc0d296 --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/3-bitwise-symmetry/solution.md @@ -0,0 +1,23 @@ +Операция над числами, в конечном итоге, сводится к битам. + +Посмотрим, можно ли поменять местами биты слева и справа. + +Например, таблица истинности для `^`: + + + + + + + + + + +
`a``b`результат
`0``0``0`
`0``1``1`
`1``0``1`
`1``1``0`
+ +Случаи `0^0` и `1^1` заведомо не изменятся при перемене мест, поэтому нас не интересуют. А вот `0^1` и `1^0` эквивалентны и равны `1`. + +Аналогично можно увидеть, что и другие операторы симметричны. + +Ответ: **да**. + diff --git a/1-js/2-first-steps/10-bitwise-operators/3-bitwise-symmetry/task.md b/1-js/2-first-steps/10-bitwise-operators/3-bitwise-symmetry/task.md new file mode 100644 index 00000000..96b7ef88 --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/3-bitwise-symmetry/task.md @@ -0,0 +1,12 @@ +# Симметричны ли операции ^, |, &? + +[importance 5] + +Верно ли, что для любых `a` и `b` выполняются равенства ниже? + + +Иными словами, при перемене мест -- всегда ли результат остаётся тем же? \ No newline at end of file diff --git a/1-js/2-first-steps/10-bitwise-operators/4-bit-rounding/solution.md b/1-js/2-first-steps/10-bitwise-operators/4-bit-rounding/solution.md new file mode 100644 index 00000000..0b6f6443 --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/4-bit-rounding/solution.md @@ -0,0 +1,29 @@ +Всё дело в том, что побитовые операции преобразуют число в 32-битное целое. + +Обычно число в JavaScript имеет 64-битный формат с плавающей точкой. При этом часть битов (`52`) отведены под цифры, часть (`11`) отведены под хранение номера позиции, на которой стоит десятичная точка, и один бит -- знак числа. + +Это означает, что максимальное целое число, которое можно хранить, занимает `52` бита. + +Число `12345678912345` в двоичном виде: `10110011101001110011110011100101101101011001` (44 цифры). + +Побитовый оператор `^` преобразует его в 32-битное путём отбрасывания десятичной точки и "лишних" старших цифр. При этом, так как число большое и старшие биты здесь ненулевые, то, естественно, оно изменится. + +Вот ещё пример: + +```js +//+ run +// в двоичном виде 1000000000000000000000000000000 (31 цифры) +alert( Math.pow(2, 30) ); // 1073741824 +alert( Math.pow(2, 30) ^ 0 ); // 1073741824, всё ок, длины хватает + +// в двоичном виде 100000000000000000000000000000000 (33 цифры) +alert( Math.pow(2, 32) ); // 4294967296 +alert( Math.pow(2, 32) ^ 0 ); // 0, отброшены старшие цифры, остались нули + +// пограничный случай +// в двоичном виде 10000000000000000000000000000000 (32 цифры) +alert( Math.pow(2, 31) ); // 2147483648 +alert( Math.pow(2, 31) ^ 0 ); // -2147483648, ничего не отброшено, +// но первый бит 1 теперь стоит в начале числа и является знаковым +``` + diff --git a/1-js/2-first-steps/10-bitwise-operators/4-bit-rounding/task.md b/1-js/2-first-steps/10-bitwise-operators/4-bit-rounding/task.md new file mode 100644 index 00000000..a0a19eac --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/4-bit-rounding/task.md @@ -0,0 +1,12 @@ +# Почему результат разный? + +[importance 5] + +Почему результат второго `alert'а` такой странный? + +```js +//+ run +alert( 123456789 ^ 0 ); // 123456789 +alert( 12345678912345 ^ 0 ); // 1942903641 +``` + diff --git a/1-js/2-first-steps/10-bitwise-operators/article.md b/1-js/2-first-steps/10-bitwise-operators/article.md new file mode 100644 index 00000000..f7e1e4d9 --- /dev/null +++ b/1-js/2-first-steps/10-bitwise-operators/article.md @@ -0,0 +1,752 @@ +# Побитовые операторы + +Побитовые операторы интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Они производят операции, используя двоичное представление числа, и возвращают новую последовательность из 32 бит (число) в качестве результата. + +**Эта глава сложная, требует дополнительных знаний в программировании и не очень важная, вы можете пропустить её.** +[cut] + +## Формат 32-битного целого числа со знаком [#signed-format] + +Побитовые операторы в JavaScript работают с 32-битными целыми числами в их двоичном представлении. + +Это представление называется "32-битное целое со знаком, старшим битом слева и дополнением до двойки". + +Разберём, как устроены числа внутри подробнее, это необходимо знать для битовых операций с ними. + + + +## Список операторов + +В следующей таблице перечислены все побитовые операторы. +Далее операторы разобраны более подробно. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ОператорИспользованиеОписание
Побитовое И (AND)a & bСтавит 1 на бит результата, для которого соответствующие биты операндов равны 1.
Побитовое ИЛИ (OR)a | bСтавит 1 на бит результата, для которого хотя бы один из соответствующих битов операндов равен 1.
Побитовое исключающее ИЛИ (XOR)a ^ bСтавит 1 на бит результата, для которого только один из соответствующих битов операндов равен 1 (но не оба).
Побитовое НЕ (NOT)~aЗаменяет каждый бит операнда на противоположный.
Левый сдвиг`a << b`Сдвигает двоичное представление a на b битов влево, добавляя справа нули.
Правый сдвиг, переносящий знак`a >> b`Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты.
Правый сдвиг с заполнением нулями`a >>> b`Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты и добавляя нули слева.
+ +## Описание работы операторов + +Побитовые операторы работают следующим образом: + +
    +
  1. Операнды преобразуются в 32-битные целые числа, представленные последовательностью битов. Дробная часть, если она есть, отбрасывается.
  2. +
  3. Для бинарных операторов -- каждый бит в первом операнде рассматривается вместе с соответствующим битом второго операнда: первый бит с первым, второй со вторым и т.п. Оператор применяется к каждой паре бит, давая соответствующий бит результата.
  4. +
  5. Получившаяся в результате последовательность бит интерпретируется как обычное число.
  6. +
+ +Посмотрим, как работают операторы, на примерах. + +### & (Побитовое И) + +Выполняет операцию И над каждой парой бит. + +Результат `a & b` равен единице только когда оба бита `a` и `b` равны единице. + +Таблица истинности для `&`: + + + + + + + +
`a``b``a & b`
`0``0``0`
`0``1``0`
`1``0``0`
`1``1``1`
+ +Пример: + +```js +9 (по осн. 10) + = 00000000000000000000000000001001 (по осн. 2) +14 (по осн. 10) + = 00000000000000000000000000001110 (по осн. 2) + -------------------------------- +14 & 9 (по осн. 10) + = 00000000000000000000000000001000 (по осн. 2) + = 8 (по осн. 10) +``` + +### | (Побитовое ИЛИ) + +Выполняет операцию ИЛИ над каждой парой бит. Результат `a | b` равен 1, если хотя бы один бит из a,b равен 1. + +Таблица истинности для `|`: + + + + + + + +
`a``b``a | b`
`0``0``0`
`0``1``1`
`1``0``1`
`1``1``1`
+ +Пример: + +```js +9 (по осн. 10) + = 00000000000000000000000000001001 (по осн. 2) +14 (по осн. 10) + = 00000000000000000000000000001110 (по осн. 2) + -------------------------------- +14 | 9 (по осн. 10) + = 00000000000000000000000000001111 (по осн. 2) + = 15 (по осн. 10) +``` + +### ^ (Исключающее ИЛИ) + +Выполняет операцию "Исключающее ИЛИ" над каждой парой бит. + + a Исключающее ИЛИ b равно 1, если только a=1 или только b=1, но не оба одновременно a=b=1. + +Таблица истинности для исключающего ИЛИ: + + + + + + + +
`a``b``a ^ b`
`0``0``0`
`0``1``1`
`1``0``1`
`1``1``0`
+ +Как видно, оно даёт 1, если ЛИБО слева `1`, ЛИБО справа `1`, но не одновременно. Поэтому его и называют "исключающее ИЛИ". + +Пример: + +```js +9 (по осн. 10) + = 00000000000000000000000000001001 (по осн. 2) +14 (по осн. 10) + = 00000000000000000000000000001110 (по осн. 2) + -------------------------------- +14 ^ 9 (по осн. 10) + = 00000000000000000000000000000111 (по осн. 2) + = 7 (по осн. 10) +``` + +[smart header="Исключающее ИЛИ в шифровании"] +Исключающее или можно использовать для шифрования, так как эта операция полностью обратима. Двойное применение исключающего ИЛИ с тем же аргументом даёт исходное число. + +Иначе говоря, верна формула: `a ^ b ^ b == a`. + +Пускай Вася хочет передать Пете секретную информацию `data`. Эта информация заранее превращена в число, например строка интерпретируется как последовательность кодов символов. + +Вася и Петя заранее договариваются о числовом ключе шифрования `key`. + +Алгоритм: + + +Например, пусть в `data` очередное число равно `9`, а ключ `key` равен `1220461917`. + +```js +Данные: 9 в двоичном виде +00000000000000000000000000001001 + +Ключ: 1220461917 в двоичном виде +01001000101111101100010101011101 + +Результат операции 9 ^ key: +01001000101111101100010101010100 +Результат в 10-ной системе (шифровка): +1220461908 +``` + + + +В нашем случае: + +```js +Полученная шифровка в двоичной системе: +9 ^ key = 1220461908 +01001000101111101100010101010100 + +Ключ: 1220461917 в двоичном виде: +01001000101111101100010101011101 + +Результат операции 1220461917 ^ key: +00000000000000000000000000001001 +Результат в 10-ной системе (исходное сообщение): +9 +``` + +Конечно, такое шифрование поддаётся частотному анализу и другим методам дешифровки, поэтому современные алгоритмы используют операцию XOR `^` как одну из важных частей более сложной многоступенчатой схемы. +[/smart] + + + + +### ~ (Побитовое НЕ) + +Производит операцию НЕ над каждым битом, заменяя его на обратный ему. + +Таблица истинности для НЕ: + + + + + +
`a``~a`
`0``1`
`1``0`
+ +Пример: + +```js + 9 (по осн. 10) + = 00000000000000000000000000001001 (по осн. 2) + -------------------------------- +~9 (по осн. 10) + = 11111111111111111111111111110110 (по осн. 2) + = -10 (по осн. 10) +``` + +Из-за внутреннего представления отрицательных чисел получается так, что `~n == -(n+1)`. + +Например: + +```js +//+ run +alert(~3); // -4 +alert(~-1); // 0 +``` + +### << (Битовый сдвиг влево) + +Операторы битового сдвига принимают два операнда. Первый -- это число для сдвига, а второй -- количество битов, которые нужно сдвинуть в первом операнде. + +Оператор `<<` сдвигает первый операнд на указанное число битов влево. Лишние биты отбрасываются, справа добавляются нулевые биты. + +Например, `9 << 2` даст `36`: + +```js + +9 (по осн.10) + = 00000000000000000000000000001001 (по осн.2) + -------------------------------- +9 << 2 (по осн.10) + = 00000000000000000000000000100100 (по осн.2) + = 36 (по осн.10) +``` + +Операция `<< 2` сдвинула и отбросила два левых нулевых бита и добавила справа два новых нулевых. + +[smart header="Левый сдвиг почти равен умножению на 2"] +Битовый сдвиг `<< N` обычно имеет тот же эффект, что и умножение на два `N` раз, например: + +```js +//+ run +alert( 3 << 1 ); // 6, умножение на 2 +alert( 3 << 2 ); // 12, умножение на 2 два раза +alert( 3 << 3 ); // 24, умножение на 2 три раза +``` + +Конечно, следует иметь в виду, что побитовые операторы работают только с 32-битными числами, поэтому верхний порог такого "умножения" ограничен: + +```js +//+ run +*!* +alert(10000000000 << 1); // -1474836480, отброшен крайний-левый бит +*/!* +alert(10000000000 * 2); // 20000000000, обычное умножение +``` + +[/smart] + +### >> (Правый битовый сдвиг, переносящий знак) + +Этот оператор сдвигает биты вправо, отбрасывая лишние. При этом слева добавляется *копия* крайнего-левого бита. + +Знак числа (представленный крайним-левым битом) при этом не меняется, так как новый крайний-левый бит имеет то же значение, что и исходном числе. + +Поэтому он назван "переносящим знак". + +Например, `9 >> 2` даст 2: + +```js +9 (по осн.10) + = 00000000000000000000000000001001 (по осн.2) + -------------------------------- +9 >> 2 (по осн.10) + = 00000000000000000000000000000010 (по осн.2) + = 2 (по осн.10) +``` + +Операция `>> 2` сдвинула вправо и отбросила два правых бита `01` и добавила слева две копии первого бита `00`. + +Аналогично, `-9 >> 2` даст `-3`: + +```js +-9 (по осн.10) + = 11111111111111111111111111110111 (по осн.2) + -------------------------------- +-9 >> 2 (по осн.10) + = 11111111111111111111111111111101 (по осн.2) = -3 (по осн.10) +``` + +Здесь операция `>> 2` сдвинула вправо и отбросила два правых бита `11` и добавила слева две копии первого бита `11`. , Знак числа сохранён, так как крайний-левый (знаковый) бит сохранил значение `1`. + +[smart header="Правый сдвиг почти равен целочисленному делению на 2"] +Битовый сдвиг `>> N` обычно имеет тот же, что и целочисленное деление на два `N` раз: + +```js +//+ run +alert( 100 >> 1 ); // 50, деление на 2 +alert( 100 >> 2 ); // 25, деление на 2 два раза +alert( 100 >> 3 ); // 12, деление на 2 три раза, целая часть от результата +``` + +[/smart] + + +### >>> (Правый сдвиг с заполнением нулями) + +Этот оператор сдвигает биты первого операнда вправо. Лишние биты справа отбрасываются. Слева добавляются нулевые биты. + +Знаковый бит становится равным 0, поэтому результат всегда положителен. + +**Для неотрицательных чисел правый сдвиг с заполнением нулями `>>>` и правый сдвиг с переносом знака `>>` дадут одинаковый результат, т.к в обоих случаях слева добавятся нули.** + +Для отрицательных чисел -- результат работы разный. Например, `-9 >>> 2` даст `1073741821`, в отличие от `-9 >> 2` (дает `-3`): + +```js +-9 (по осн.10) + = 11111111111111111111111111110111 (по осн.2) + -------------------------------- +-9 >>> 2 (по осн.10) + = 00111111111111111111111111111101 (по осн.2) + = 1073741821 (по осн.10) +``` + +## Применение побитовых операторов + +Побитовые операторы используются редко, но всё же используются. + +Случаи применения побитовых операторов, которые мы здесь разберём, составляют большинство всех использований в JavaScript. + +[warn header="Осторожно, приоритеты!"] +В JavaScript побитовые операторы `^`, `&`, `|` выполняются после сравнений `==`. + +Например, в сравнении `a == b^0` будет сначала выполнено сравнение `a == b`, а потом уже операция `^0`, как будто стоят скобки `(a == b)^0`. + +Обычно это не то, чего мы хотим. Чтобы гарантировать желаемый порядок, нужно ставить скобки: `a == (b^0)`. +[/warn] + +### Маска + +Для этого примера представим, что наш скрипт работает с пользователями: + + +У каждого из них есть ряд доступов, которые можно свести в таблицу: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПользовательПросмотр статейИзменение статейПросмотр товаровИзменение товаровУправление правами
ГостьДаНетДаНетНет
РедакторДаДаДаДаНет
АдминДаДаДаДаДа
+ +Если вместо "Да" поставить `1`, а вместо "Нет" -- `0`, то каждый набор доступов описывается числом: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ПользовательПросмотр статейИзменение статейПросмотр товаровИзменение товаровУправление правамиВ 10-ной системе
Гость10100 = 20
Редактор11110 = 30
Админ11111 = 31
+ +**Мы "упаковали" много информации в одно число. Это экономит память. Но, кроме этого, по нему очень легко проверить, имеет ли посетитель заданную *комбинацию доступов*!** + +Для этого посмотрим, как в 2-ной системе представляется каждый доступ в отдельности. + + + +Например, просматривать и изменять статьи позволит доступ `access = 11000`. + +Как правило, доступы задаются в виде констант: + +```js +var ACCESS_ADMIN = 1; // 00001 +var ACCESS_GOODS_CHANGE = 2; // 00010 +var ACCESS_GOODS_VIEW = 4; // 00100 +var ACCESS_ARTICLE_CHANGE = 8; // 01000 +var ACCESS_ARTICLE_VIEW = 16; // 10000 +``` + +Из этих констант получить нужную комбинацию доступов можно при помощи операции `|`. + +```js +var access = ACCESS_ARTICLE_VIEW | ACCESS_ARTICLE_CHANGE; // 11000 +``` + +### Двоичные числа в JavaScript + +Для удобной работы с примерами в этой статье пригодятся две функции. + + + +Например: + +```js +//+ run +var access = parseInt("11000", 2); // получаем число из строки + +alert(access); // 24, число с таким 2-ным представлением + +var access2 = access.toString(2); // обратно двоичную строку из числа + +alert(access2); // 11000 +``` + +### Проверка доступов + +Для того, чтобы понять, есть ли в доступе `access` нужный доступ, например управление правами -- достаточно применить к нему побитовый оператор И (`&`) с соответствующей маской. + +Создадим для примера ряд доступов и проверим их: + +```js +//+ run +var access = parseInt("11111", 2); // 31, все 1 означает, что доступ полный + +alert(access & ACCESS_ADMIN); // если результат не 0, то есть доступ ACCESS_ADMIN +``` + +А теперь та же проверка для посетителя с другими правами: + +```js +//+ run +var access = parseInt("10100"); // 20, нет 1 в конце + +alert(access & ACCESS_ADMIN); // 0, нет доступа к управлению правами +``` + +Такая проверка работает, потому что оператор И ставит `1` на те позиции результата, на которых в обоих операндах стоит `1`. + +Так что `access & 1` для любого числа `access` поставит все биты в ноль, кроме самого правого. А самый правый станет `1` только если он равен `1` в `access`. + +Для полноты картины также проверим, даёт ли доступ `11111` право на изменение товаров. Для этого нужно применить к доступу оператор И (`&`) с `00010` (=`2` в 10-ной системе). + +```js +//+ run +var adminAccess = 31; // 111*!*1*/!*1 + +alert(adminAccess & ACCESS_GOODS_CHANGE); // не 0, есть доступ к изменению товаров +``` + +**Можно проверить один из нескольких доступов.** + +Например, проверим, есть ли права на просмотр ИЛИ изменение товаров. Соответствующие права задаются битом `1` на втором и третьем месте с конца, что даёт число `00110` (=`6` в 10-ной системе). + +```js +//+ run +var check = ACCESS_GOODS_VIEW | ACCESS_GOODS_CHANGE; // 6, 00110 + +var access = 30; // 11*!*11*/!*0; + +alert(access & check); // не 0, значит есть доступ к просмотру ИЛИ изменению + +access = parseInt("11100", 2); + +alert(access & check); // не 0, есть доступ к просмотру ИЛИ изменению +``` + +Как видно из примера выше, если в аргументе `check` стоит ИЛИ из нескольких доступов `ACCESS_*`, то и результат проверки скажет, есть ли хотя бы один из них. А какой -- нужно смотреть отдельной проверкой, если это важно. + +**Итак, маска даёт возможность удобно "паковать" много битовых значений в одно число при помощи ИЛИ `|`, а также, при помощи оператора И (`&`), проверять маску на комбинацию установленных битов.** + +### Маски в функциях + +Зачастую маски используют в функциях, чтобы одним параметром передать несколько "флагов", т.е. однобитных значений. + +Например: + +```js +// найти пользователей с правами на изменение товаров или администраторов +findUsers(ACCESS_GOODS_CHANGE | ACCESS_ADMIN); +``` + +### Округление + +Так как битовые операции отбрасывают десятичную часть, то их можно использовать для округления. Достаточно взять любую операцию, которая не меняет значение числа. + +Например, двойное НЕ (`~`): + +```js +//+ run +alert( ~~12.345 ); // 12 +``` + +Подойдёт и Исключающее ИЛИ (`^`) с нулём: + +```js +//+ run +alert( 12.345^0 ); // 12 +``` + +Последнее даже более удобно, поскольку отлично читается: + +```js +//+ run +alert( 12.3 * 14.5 ^ 0); // (=178) "12.3 умножить на 14.5 *!*и округлить*/!*" +``` + +У побитовых операторов достаточно низкий приоритет, он меньше чем у остальной арифметики: + +```js +//+ run +alert( 1.1 + 1.2 ^ 0 ); // 2, сложение выполнится раньше округления +``` + +### Проверка на -1 + +[Внутренний формат](#signed-format) чисел устроен так, что для смены знака нужно все биты заменить на противоположные ("обратить") и прибавить `1`. + +Обращение битов -- это побитовое НЕ (`~`). То есть, при таком формате представления числа `-n = ~n + 1`. Или, если перенести единицу: `~n = -(n+1)`. + +Как видно из последнего равенства, `~n == 0` только если `n == -1`. Поэтому можно легко проверить равенство `n == -1`: + +```js +//+ run +var n = 5; + +if (~n) { // сработает, т.к. ~n = -(5+1) = -6 + alert("n не -1"); // выведет! +} +``` + + + +```js +//+ run +var n = -1; + +if (~n) { // не сработает, т.к. ~n = -(-1+1) = 0 + alert("...ничего не выведет..."); +} +``` + +Проверка на `-1` пригождается, например, при поиске символа в строке. Вызов `str.indexOf("подстрока")` возвращает позицию подстроки в `str`, или `-1` если не нашёл. + +```js +//+ run +var str = "Проверка"; + +if (~str.indexOf("верка")) { // Сочетание "if (~...indexOf)" читается как "если найдено" + alert('найдено!'); +} +``` + +### Умножение и деление на степени 2 + +Оператор `a << b`, сдвигая биты, по сути умножает `a` на 2b. + +Например: + +```js +//+ run +alert( 1 << 2 ); // 1*(2*2) = 4 +alert( 1 << 3 ); // 1*(2*2*2) = 8 +alert( 3 << 3 ); // 3*(2*2*2) = 24 +``` + +При этом следует иметь в виду, что максимальный верхний порог такого умножения меньше, чем обычно, так как побитовый оператор оперирует 32-битными целыми, в то время как обычные операторы оперируют числами длиной 64 бита. + +**Оператор `a >> b`, сдвигая биты, производит целочисленное деление `a` на 2b.** + +```js +//+ run +alert( 8 >> 2 ); // 2 = 8/4, убрали 2 нуля в двоичном представлении +alert( 11 >> 2 ); // 2, целочисленное деление (менее значимые биты просто отброшены) +``` + +## Итого + + + +Как правило, битовое представление числа используется для: + +[head] + +[/head] \ No newline at end of file diff --git a/1-js/2-first-steps/11-uibasic/1-simple-page/solution.md b/1-js/2-first-steps/11-uibasic/1-simple-page/solution.md new file mode 100644 index 00000000..53bc8af2 --- /dev/null +++ b/1-js/2-first-steps/11-uibasic/1-simple-page/solution.md @@ -0,0 +1,27 @@ +JS-код: + +```js +//+ demo run +var name = prompt("Ваше имя?", ""); +alert(name); +``` + +Полная страница: + +```html + + + + + + + + + + + +``` + diff --git a/1-js/2-first-steps/11-uibasic/1-simple-page/task.md b/1-js/2-first-steps/11-uibasic/1-simple-page/task.md new file mode 100644 index 00000000..6d2ca2e1 --- /dev/null +++ b/1-js/2-first-steps/11-uibasic/1-simple-page/task.md @@ -0,0 +1,8 @@ +# Простая страница + +[importance 4] + +Создайте страницу, которая спрашивает имя и выводит его. + +[demo /] + diff --git a/1-js/2-first-steps/11-uibasic/article.md b/1-js/2-first-steps/11-uibasic/article.md new file mode 100644 index 00000000..e1b00705 --- /dev/null +++ b/1-js/2-first-steps/11-uibasic/article.md @@ -0,0 +1,110 @@ +# Взаимодействие с пользователем: alert, prompt, confirm + + +В этом разделе мы рассмотрим базовые UI операции: `alert`, `prompt` и `confirm`, которые позволяют работать с данными, полученными от пользователя. +[cut] +## alert + +Синтаксис: + +```js +alert(сообщение) +``` + +`alert` выводит на экран окно с сообщением и приостанавливает выполнение скрипта, пока пользователь не нажмет "ОК". + +```js +//+ run +alert("Привет"); +``` + +Окно сообщения, которое выводится, является *модальным окном*. Слово "модальное" означает, что посетитель не может взаимодействовать со страницей, нажимать другие кнопки и т.п., пока не разберется с окном. В данном случае - пока не нажмет на "OK". + +## prompt + +Функция prompt принимает два аргумента: + +```js +result = prompt(title, default); +``` + +Она выводит модальное окно с заголовком `title`, полем для ввода текста, заполненным строкой по умолчанию `default` и кнопками OK/CANCEL. + +Пользователь должен либо что-то ввести и нажать OK, либо отменить ввод кликом на CANCEL или нажатием [key Esc] на клавиатуре. + +**Вызов `prompt` возвращает то, что ввел посетитель -- строку или специальное значение `null`, если ввод отменен.** + +[warn header="Safari 5.1+ не возвращает `null`"] +Единственный браузер, который не возвращает `null` при отмене ввода -- это Safari. При отсутствии ввода он возвращает пустую строку. Предположительно, это ошибка в браузере. + +Если нам важен этот браузер, то пустую строку нужно обрабатывать точно так же, как и `null`, т.е. считать отменой ввода. +[/warn] + +Как и в случае с `alert`, окно `prompt` модальное. + +```js +//+ run +var years = prompt('Сколько вам лет?', 100); + +alert('Вам ' + years + ' лет!') +``` + +[warn header="Всегда указывайте `default`"] +Вообще, второй `default` может отсутствовать. Однако при этом IE вставит в диалог значение по умолчанию `"undefined"`. + +Запустите этот код в IE, чтобы понять о чем речь: + +```js +//+ run +var test = prompt("Тест"); +``` + +Поэтому рекомендуется *всегда* указывать второй аргумент: + +```js +//+ run +var test = prompt("Тест", ''); // <-- так лучше +``` + +[/warn] + + +## confirm + +Синтаксис: + +```js +result = confirm(question); +``` + +`confirm` выводит окно с вопросом `question` с двумя кнопками: OK и CANCEL. + +**Результатом будет `true` при нажатии OK и `false` - при CANCEL([key Esc]).** + +Например: + +```js +//+ run +var isAdmin = confirm("Вы - администратор?"); + +alert(isAdmin); +``` + +## Особенности встроенных функций + +Место, где выводится модальное окно с вопросом, и внешний вид окна выбирает браузер. Разработчик не может на это влиять. + +С одной стороны -- это недостаток, т.к. нельзя вывести окно в своем дизайне. + +С другой стороны, преимущество этих функций по сравнению с другими, более сложными методами взаимодействия, которые мы изучим в дальнейшем -- как раз в том, что они очень просты. + +Это самый простой способ вывести сообщение или получить информацию от посетителя. Поэтому их используют в тех случаях, когда простота важна, а всякие "красивости" особой роли не играют. + + +## Резюме + + \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/1-if-zero-string/solution.md b/1-js/2-first-steps/12-ifelse/1-if-zero-string/solution.md new file mode 100644 index 00000000..0e71c2b9 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/1-if-zero-string/solution.md @@ -0,0 +1,13 @@ +**Да, выведется,** т.к. внутри `if` стоит строка `"0"`. + +Любая строка, кроме пустой (а здесь она не пустая), в логическом контексте является `true`. + +Можно запустить и проверить: + +```js +//+ run +if ("0") { + alert('Привет'); +} +``` + diff --git a/1-js/2-first-steps/12-ifelse/1-if-zero-string/task.md b/1-js/2-first-steps/12-ifelse/1-if-zero-string/task.md new file mode 100644 index 00000000..2b1bd3a7 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/1-if-zero-string/task.md @@ -0,0 +1,12 @@ +# if (строка с нулём) + +[importance 5] + +Выведется ли `alert`? + +```js +if ("0") { + alert('Привет'); +} +``` + diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2.png b/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2.png new file mode 100755 index 00000000..77e0fd12 Binary files /dev/null and b/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2.png differ diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2/index.html b/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2/index.html new file mode 100755 index 00000000..588be43f --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/2-check-standard/ifelse_task2/index.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/solution.md b/1-js/2-first-steps/12-ifelse/2-check-standard/solution.md new file mode 100644 index 00000000..aacb5966 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/2-check-standard/solution.md @@ -0,0 +1,6 @@ + + +```html + +``` + diff --git a/1-js/2-first-steps/12-ifelse/2-check-standard/task.md b/1-js/2-first-steps/12-ifelse/2-check-standard/task.md new file mode 100644 index 00000000..f5aa5eb6 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/2-check-standard/task.md @@ -0,0 +1,13 @@ +# Проверка стандарта + +[importance 2] + +Используя конструкцию `if..else`, напишите код, который будет спрашивать: "Каково "официальное" название JavaScript?". + +Если посетитель вводит "EcmaScript", то выводить "Верно!", если что-то другое -- выводить "Не знаете? "EcmaScript"!". + +Блок-схема: + + + +[demo src="ifelse_task2"] \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/2-check-standardifelse_task2/index.html b/1-js/2-first-steps/12-ifelse/2-check-standardifelse_task2/index.html new file mode 100755 index 00000000..588be43f --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/2-check-standardifelse_task2/index.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/1-js/2-first-steps/12-ifelse/3-sign/if_sign/index.html b/1-js/2-first-steps/12-ifelse/3-sign/if_sign/index.html new file mode 100755 index 00000000..cff65b6c --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/3-sign/if_sign/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/1-js/2-first-steps/12-ifelse/3-sign/solution.md b/1-js/2-first-steps/12-ifelse/3-sign/solution.md new file mode 100644 index 00000000..61af24e2 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/3-sign/solution.md @@ -0,0 +1,15 @@ + + +```js +//+ run +var value = prompt('Введите число', 0); + +if (value > 0) { + alert(1); +} else if (value < 0) { + alert(-1); +} else { + alert(0); +} +``` + diff --git a/1-js/2-first-steps/12-ifelse/3-sign/task.md b/1-js/2-first-steps/12-ifelse/3-sign/task.md new file mode 100644 index 00000000..9d24ede9 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/3-sign/task.md @@ -0,0 +1,12 @@ +# Получить знак числа + +[importance 2] + +Используя конструкцию `if..else`, напишите код, который получает значение `prompt`, а затем выводит `alert`: + + +[demo src="if_sign"] \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/4-check-login/ifelse_task.png b/1-js/2-first-steps/12-ifelse/4-check-login/ifelse_task.png new file mode 100755 index 00000000..e9a9270d Binary files /dev/null and b/1-js/2-first-steps/12-ifelse/4-check-login/ifelse_task.png differ diff --git a/1-js/2-first-steps/12-ifelse/4-check-login/solution.md b/1-js/2-first-steps/12-ifelse/4-check-login/solution.md new file mode 100644 index 00000000..edafff62 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/4-check-login/solution.md @@ -0,0 +1,33 @@ + + +```js +//+ run demo +var userName = prompt('Кто пришёл?', ''); + +if ( userName == 'Админ' ) { + + var pass = prompt('Пароль?', ''); + + if ( pass == 'Чёрный Властелин' ) { + alert('Добро пожаловать!'); + } else if ( pass == null ) { // (*) + alert('Вход отменён'); + } else { + alert('Пароль неверен'); + } + +} else if ( userName == null ) { // (**) + alert('Вход отменён'); + +} else { + + alert('Я вас не знаю'); + +} +``` + +Обратите внимание на проверку `if` в строках `(*)` и `(**)`. Везде, кроме Safari, нажатие "Отмена" возвращает `null`, а вот Safari возвращает при отмене пустую строку, поэтому в браузере Safari можно было бы добавить дополнительную проверку на неё. + +Впрочем, такое поведение Safari является некорректным, надеемся, что скоро его исправят. + +Кроме того, обратите внимание на дополнительные вертикальные отступы внутри `if`. Они не обязательны, но полезны для лучшей читаемости кода. \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/4-check-login/task.md b/1-js/2-first-steps/12-ifelse/4-check-login/task.md new file mode 100644 index 00000000..a158cb4b --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/4-check-login/task.md @@ -0,0 +1,17 @@ +# Проверка логина + +[importance 3] + +Напишите код, который будет спрашивать логин (`prompt`). + +Если посетитель вводит "Админ", то спрашивать пароль, если нажал отмена (escape) -- выводить "Вход отменён", если вводит что-то другое -- "Я вас не знаю". + +Пароль проверять так. Если введён пароль "Чёрный Властелин", то выводить "Добро пожаловать!", иначе -- "Пароль неверен", при отмене -- "Вход отменён". + +Блок-схема: + + + +Для решения используйте вложенные блоки `if`. Обращайте внимание на стиль и читаемость кода. + +[demo /] \ No newline at end of file diff --git a/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/solution.md b/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/solution.md new file mode 100644 index 00000000..1799c70b --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/solution.md @@ -0,0 +1,6 @@ + + +```js +result = (a + b < 4) ? 'Мало' : 'Много'; +``` + diff --git a/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/task.md b/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/task.md new file mode 100644 index 00000000..dc75b1fb --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/5-rewrite-if-question/task.md @@ -0,0 +1,14 @@ +# Перепишите 'if' в '?' + +[importance 5] + +Перепишите `if` с использованием оператора `'?'`: + +```js +if (a + b < 4) { + result = 'Мало'; +} else { + result = 'Много'; +} +``` + diff --git a/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/solution.md b/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/solution.md new file mode 100644 index 00000000..42b7b628 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/solution.md @@ -0,0 +1,9 @@ + + +```js +var message = (login == 'Вася') ? 'Привет' : + (login == 'Директор') ? 'Здравствуйте' : + (login == '') ? 'Нет логина' : + ''; +``` + diff --git a/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/task.md b/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/task.md new file mode 100644 index 00000000..b3babf08 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/6-rewrite-if-else-question/task.md @@ -0,0 +1,22 @@ +# Перепишите 'if..else' в '?' + +[importance 5] + +Перепишите `if..else` с использованием нескольких операторов `'?'`. + +Для читаемости -- оформляйте код в несколько строк. + +```js +var message; + +if (login == 'Вася') { + message = 'Привет'; +} else if (login == 'Директор') { + message = 'Здравствуйте'; +} else if (login == '') { + message = 'Нет логина'; +} else { + message = ''; +} +``` + diff --git a/1-js/2-first-steps/12-ifelse/article.md b/1-js/2-first-steps/12-ifelse/article.md new file mode 100644 index 00000000..9547b6d1 --- /dev/null +++ b/1-js/2-first-steps/12-ifelse/article.md @@ -0,0 +1,227 @@ +# Условные операторы: if, '?' + +Иногда, в зависимости от условия, нужно выполнить различные действия. Для этого используется оператор `if`. +[cut] +Например: + +```js +//+ run +var year = prompt('В каком году появилась спецификация ECMA-262 5.1?', ''); + +if (year != 2011) alert('А вот и неправильно!'); +``` + +## Оператор if + +Оператор `if` ("если") получает условие, в примере выше это `year != 2011`. Он вычисляет его, и если результат -- `true`, то выполняет команду. + +Если нужно выполнить более одной команды -- они оформляются блоком кода в фигурных скобках: + +```js +if (year != 2011) { + alert('А вот..'); + alert('..и неправильно!'); +} +``` + +**Рекомендуется использовать фигурные скобки всегда, даже когда команда одна.** Это улучшает читаемость кода. + + +## Преобразование к логическому типу + +Оператор `if (...)` вычисляет и преобразует выражение в скобках к логическому типу. + +**В логическом контексте число `0`, пустая строка `""`, `null` и `undefined`, а также `NaN` являются `false`, остальные значения -- `true`.** + +Например, такое условие никогда не выполнится: + +```js +if (0) { // 0 преобразуется к false + ... +} +``` + +... А такое -- выполнится всегда: + +```js +if (1) { // 1 преобразуется к true + ... +} +``` + +Вычисление условия в проверке `if (year != 2011)` может быть вынесено в отдельную переменную: + +```js +var cond = (year != 2011); // true/false + +if (cond) { + ... +} +``` + +## Неверное условие, else + +Необязательный блок `else` ("иначе") выполняется, если условие неверно: + +```js +//+ run +var year = prompt('Введите год ECMA-262 5.1', ''); + +if (year == 2011) { + alert('Да вы знаток!'); +} else { + alert('А вот и неправильно!'); // любое значение, кроме 2011 +} +``` + +## Несколько условий, else if + +Бывает нужно проверить несколько вариантов условия. Для этого используется блок `else if ...`. Например: + +```js +//+ run +var year = prompt('В каком году появилась спецификация ECMA-262 5.1?', ''); + +if (year < 2011) { + alert('Это слишком рано..'); +} else if (year > 2011) { + alert('Это поздновато..'); +} else { + alert('Да, точно в этом году!'); +} +``` + +В примере выше JavaScript сначала проверит первое условие, если оно ложно -- перейдет ко второму -- и так далее, до последнего `else`. + + +## Оператор вопросительный знак '?' +Иногда нужно в зависимости от условия присвоить переменную. Например: + +```js +//+ run +var access; +var age = prompt('Сколько вам лет?', ''); + +*!* +if (age > 14) { + access = true; +} else { + access = false; +} +*/!* + +alert(access); +``` + +Оператор вопросительный знак `'?'` позволяет делать это короче и проще. + +Он состоит из трех частей: + +```js +условие ? значение1 : значение2 +``` + +Проверяется условие, затем если оно верно -- возвращается `значение1 `, если неверно -- `значение2`, например: + +```js +access = (age > 14) ? true : false; +``` + +Оператор `'?'` выполняется позже большинства других, в частности -- позже сравнений, поэтому скобки можно не ставить: + +```js +access = age > 14 ? true : false; +``` + +...Но когда скобки есть -- код лучше читается. Так что рекомендуется их писать. + +[smart] +В данном случае можно было бы обойтись и без оператора `'?'`, т.к. сравнение само по себе уже возвращает `true/false`: + +```js +access = age > 14; +``` + +[/smart] + +[smart header="\"Тернарный оператор\""] +Вопросительный знак -- единственный оператор, у которого есть аж три аргумента, в то время как у обычных операторов их один-два. +Поэтому его называют *"тернарный оператор"*. +[/smart] + + +## Несколько операторов '?' + +Несколько операторов `if..else` можно заменить последовательностью операторов `'?'`. Например: + +```js +//+ run +var a = prompt('a?', 1); + +*!* +var res = (a == 1) ? 'значение1' : + (a == 2) ? 'значение2' : + (a > 2) ? 'значение3' : + 'значение4'; +*/!* + +alert(res); +``` + +Поначалу может быть сложно понять, что происходит. Однако, внимательно приглядевшись, мы замечаем, что это *обычный `if..else`*! + +Вопросительный знак проверяет сначала `a == 1`, если верно -- возвращает `значение1`, если нет -- идет проверять `a == 2`. Если это верно -- возвращает `значение2`, иначе проверка `a > 2` и `значение3`... Наконец, если ничего не верно, то `значение4`. + +Альтернативный вариант с `if..else`: + +```js +var res; + +if (a == 1) { + res = 'значение1'; +} else if (a == 2) { + res = 'значение2'; +} else if (a > 2) { + res = 'значение3'; +} else { + res = 'значение4'; +} +``` + +## Нетрадиционное использование '?' + +Иногда оператор вопросительный знак `'?'` используют как замену `if`: + +```js +//+ run +var company = prompt('Какая компания создала JavaScript?', ''); + +*!* +(company == 'Netscape') ? + alert('Да, верно') : alert('Неправильно'); +*/!* +``` + +Работает это так: в зависимости от условия, будет выполнена либо первая, либо вторая часть после `'?'`. + +Результат выполнения не присваивается в переменную, так что пропадёт (впрочем, `alert` ничего не возвращает). + +**Рекомендуется не использовать вопросительный знак таким образом.** + +Несмотря на то, что с виду такая запись короче `if`, она является существенно менее читаемой. + +Вот, для сравнения, то же самое с `if`: + +```js +//+ run +var company = prompt('Какая компания создала JavaScript?', ''); + +*!* +if (company == 'Netscape') { + alert('Да, верно'); +} else { + alert('Неправильно'); +} +*/!* +``` + diff --git a/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/solution.md b/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/solution.md new file mode 100644 index 00000000..4756a6ce --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/solution.md @@ -0,0 +1,7 @@ +Ответ: `2`, это первое значение, которое в логическом контексте даст `true`. + +```js +//+ run +alert( null || 2 || undefined ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/task.md b/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/task.md new file mode 100644 index 00000000..f6e93650 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/1-alert-null-2-undefined/task.md @@ -0,0 +1,10 @@ +# Что выведет alert (ИЛИ)? + +[importance 5] + +Что выведет код ниже? + +```js +alert( null || 2 || undefined ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/2-alert-or/solution.md b/1-js/2-first-steps/13-logical-ops/2-alert-or/solution.md new file mode 100644 index 00000000..30fce045 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/2-alert-or/solution.md @@ -0,0 +1,15 @@ +Ответ: сначала `1`, затем `2`. + +```js +//+ run +alert( alert(1) || 2 || alert(3) ); +``` + +Вызов `alert` не возвращает значения, или, иначе говоря, возвращает `undefined`. + +
    +
  1. Первый оператор ИЛИ `||` выполнит первый `alert(1)`, получит `undefined` и пойдёт дальше, ко второму операнду.
  2. +
  3. Так как второй операнд `2` является истинным, то вычисления завершатся, результатом `undefined || 2` будет `2`, которое будет выведено внешним `alert( .... )`.
  4. +
+ +Второй оператор `||` не будет выполнен, выполнение до `alert(3)` не дойдёт, поэтому `3` выведено не будет. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/2-alert-or/task.md b/1-js/2-first-steps/13-logical-ops/2-alert-or/task.md new file mode 100644 index 00000000..1d4ed593 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/2-alert-or/task.md @@ -0,0 +1,10 @@ +# Что выведет alert (ИЛИ)? + +[importance 3] + +Что выведет код ниже? + +```js +alert( alert(1) || 2 || alert(3) ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/solution.md b/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/solution.md new file mode 100644 index 00000000..c91e674a --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/solution.md @@ -0,0 +1,7 @@ +Ответ: `null`, это первое ложное значение из списка. + +```js +//+ run +alert( 1 && null && 2 ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/task.md b/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/task.md new file mode 100644 index 00000000..38fee457 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/3-alert-1-null-2/task.md @@ -0,0 +1,10 @@ +# Что выведет alert (И)? + +[importance 5] + +Что выведет код ниже? + +```js +alert( 1 && null && 2 ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/4-alert-and/solution.md b/1-js/2-first-steps/13-logical-ops/4-alert-and/solution.md new file mode 100644 index 00000000..83a88c73 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/4-alert-and/solution.md @@ -0,0 +1,10 @@ +Ответ: `1`, а затем `undefined`. + +```js +//+ run +alert( alert(1) && alert(2) ); +``` + +Вызов `alert` не возвращает значения, или, иначе говоря, возвращает `undefined`. + +Поэтому до правого `alert` дело не дойдёт, вычисления закончатся на левом. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/4-alert-and/task.md b/1-js/2-first-steps/13-logical-ops/4-alert-and/task.md new file mode 100644 index 00000000..2d1594d9 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/4-alert-and/task.md @@ -0,0 +1,10 @@ +# Что выведет alert (И)? + +[importance 3] + +Что выведет код ниже? + +```js +alert( alert(1) && alert(2) ); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/5-check-if-in-range/solution.md b/1-js/2-first-steps/13-logical-ops/5-check-if-in-range/solution.md new file mode 100644 index 00000000..87c733b2 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/5-check-if-in-range/solution.md @@ -0,0 +1,6 @@ + + +```js +if (age >= 14 && age <= 90) +``` + diff --git a/1-js/2-first-steps/13-logical-ops/5-check-if-in-range/task.md b/1-js/2-first-steps/13-logical-ops/5-check-if-in-range/task.md new file mode 100644 index 00000000..df16ad99 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/5-check-if-in-range/task.md @@ -0,0 +1,7 @@ +# Проверка if внутри диапазона + +[importance 3] + +Напишите условие `if` для проверки того факта, что переменная `age` находится между `14` и `90` включительно. + +"Включительно" означает, что концы промежутка включены, то есть `age` может быть равна `14` или `90`. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/6-check-if-out-range/solution.md b/1-js/2-first-steps/13-logical-ops/6-check-if-out-range/solution.md new file mode 100644 index 00000000..a901721c --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/6-check-if-out-range/solution.md @@ -0,0 +1,12 @@ +Первый вариант: + +```js +if ( !(age >= 14 && age <= 90) ) +``` + +Второй вариант: + +```js +if (age < 14 || age > 90) +``` + diff --git a/1-js/2-first-steps/13-logical-ops/6-check-if-out-range/task.md b/1-js/2-first-steps/13-logical-ops/6-check-if-out-range/task.md new file mode 100644 index 00000000..1d833664 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/6-check-if-out-range/task.md @@ -0,0 +1,7 @@ +# Проверка if вне диапазона + +[importance 3] + +Напишите условие `if` для проверки того факта, что `age` НЕ находится между 14 и 90 включительно. + +Сделайте два варианта условия: первый с использованием оператора НЕ `!`, второй - без этого оператора. \ No newline at end of file diff --git a/1-js/2-first-steps/13-logical-ops/7-if-question/solution.md b/1-js/2-first-steps/13-logical-ops/7-if-question/solution.md new file mode 100644 index 00000000..90f7a877 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/7-if-question/solution.md @@ -0,0 +1,21 @@ +Ответ: первое и третье выполнятся. + +Детали: + +```js +//+ run +// Выполнится +// Результат -1 || 0 = -1, в логическом контексте true +if (-1 || 0) alert('первое'); + +// Не выполнится +// -1 && 0 = 0, в логическом контексте false +if (-1 && 0) alert('второе'); + +// Выполнится +// оператор && имеет больший приоритет, чем || +// так что -1 && 1 выполнится раньше +// вычисления: null || -1 && 1 -> null || 1 -> 1 +if (null || -1 && 1) alert('третье'); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/7-if-question/task.md b/1-js/2-first-steps/13-logical-ops/7-if-question/task.md new file mode 100644 index 00000000..bbf094d8 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/7-if-question/task.md @@ -0,0 +1,14 @@ +# Вопрос про "if" + +[importance 5] + +Какие из этих `if` верны, т.е. выполнятся? + +Какие конкретно значения будут результатами выражений в условиях `if(...)`? + +```js +if (-1 || 0) alert('первое'); +if (-1 && 0) alert('второе'); +if (null || -1 && 1) alert('третье'); +``` + diff --git a/1-js/2-first-steps/13-logical-ops/article.md b/1-js/2-first-steps/13-logical-ops/article.md new file mode 100644 index 00000000..27742817 --- /dev/null +++ b/1-js/2-first-steps/13-logical-ops/article.md @@ -0,0 +1,249 @@ +# Логические операторы + +В JavaScript поддерживаются операторы `||` (ИЛИ), `&&` (И) и `!` (НЕ). + +Они называются *"логическими"*, но в JavaScript могут применяться к значениям любого типа и возвращают также значения любого типа. +[cut] + +## || (ИЛИ) + +Оператор ИЛИ выглядит как двойной символ вертикальной черты: + +```js +result = a || b; +``` + +**Логическое ИЛИ в классическом программировании работает следующим образом: "если *хотя бы один* из аргументов `true`, то возвращает `true`, иначе -- `false`".** + +Получается следующая таблица результатов: + +```js +//+ run +alert( true || true ); // true +alert( false || true ); // true +alert( true || false); // true +alert( false || false); // false +``` + +При вычислении ИЛИ в JavaScript можно использовать любые значения. В этом случае они будут интерпретироваться как логические. + +Например, число `1` будет воспринято как `true`, а `0` -- как `false`: + +```js +//+ run +if ( 1 || 0 ) { // сработает как if( true || false ) + alert('верно'); +} +``` + +Обычно оператор ИЛИ используется в `if`, чтобы проверить, выполняется ли хотя бы одно из условий, например: + +```js +//+ run +var hour = 9; + +*!* +if (hour < 10 || hour > 18) { +*/!* + alert('Офис до 10 или после 18 закрыт'); +} +``` + +Можно передать и больше условий: + +```js +//+ run +var hour = 12, isWeekend = true; + +if (hour < 10 || hour > 18 || isWeekend) { + alert('Офис до 10 или после 18 или в выходной закрыт'); +} +``` + +## Короткий цикл вычислений + +JavaScript вычисляет несколько ИЛИ слева направо. При этом, чтобы экономить ресурсы, используется так называемый *"короткий цикл вычисления"*. + +Допустим, вычисляются несколько ИЛИ подряд: `a || b || c || ...`. Если первый аргумент -- `true`, то результат заведомо будет `true` (хотя бы одно из значений -- `true`), и остальные значения игнорируются. + +Это особенно заметно, когда выражение, переданное в качестве второго аргумента, имеет *сторонний эффект* -- например, присваивает переменную. + +При запуске примера ниже присвоение `x` не произойдёт: + +```js +//+ run +var x; + +*!*true*/!* || (x = 1); // просто вычислим ИЛИ, без if + +alert(x); // undefined, x не присвоен +``` + +...А в примере ниже первый аргумент -- `false`, так что ИЛИ попытается вычислить второй, запустив тем самым присваивание: + +```js +//+ run +var x; + +*!*false*/!* || (x = 1); +alert(x); // 1 +``` + +## Значение ИЛИ + +Итак, как мы видим, оператор ИЛИ вычисляет ровно столько значений, сколько необходимо -- до первого `true`. + +**Оператор ИЛИ возвращает то значение, на котором остановились вычисления.** + +Примеры: + +```js +//+ run +alert( 1 || 0 ); // 1 +alert( true || 'неважно что'); // true + +alert( null || 1 ); // 1 +alert( undefined || 0 ); // 0 +``` + +Это используют, в частности, чтобы выбрать первое "истинное" значение из списка: + +```js +//+ run +var undef; // переменная не присвоена, т.е. равна undefined +var zero = 0; +var emptyStr = ""; +var msg = "Привет!"; + +*!* +var result = undef || zero || emptyStr || msg || 0; +*/!* + +alert(result); // выведет "Привет!" - первое значение, которое является true +``` + +## && (И) + + +Оператор И пишется как два амперсанда `&&`: + +```js +result = a && b; +``` + +**В классическом программировании И возвращает `true`, если оба аргумента истинны, а иначе -- `false`** + +```js +//+ run +alert( true && true ); // true +alert( false && true ); // false +alert( true && false); // false +alert( false && false); // false +``` + +Пример: + +```js +//+ run +var hour = 12, minute = 30; + +if (hour == 12 && minute == 30) { + alert('Время 12:30'); +} +``` + +Как и в ИЛИ, допустимы любые значения: + +```js +//+ run +if ( 1 && 0 ) { // вычислится как true && false + alert('не сработает, т.к. условие ложно'); +} +``` + +К И применим тот же принцип "короткого цикла вычислений", но немного по-другому, чем к ИЛИ. + +**Если левый аргумент -- `false`, оператор И возвращает его и заканчивает вычисления, а иначе -- вычисляет и возвращает правый аргумент.** + +Например: + +```js +//+ run +// Первый аргумент - true, +// Поэтому возвращается второй аргумент +alert(1 && 0); // 0 +alert(1 && 5); // 5 + +// Первый аргумент - false, +// Он и возвращается, а второй аргумент игнорируется +alert(null && 5); // null +alert(0 && "не важно"); // 0 +``` + +**Приоритет оператора И `&&` больше, чем ИЛИ `||`, т.е. он выполняется раньше.** + +Поэтому в следующем коде сначала будет вычислено правое И: `1 && 0 = 0`, а уже потом -- ИЛИ. + +```js +//+ run +alert(5 || 1 && 0); // 5 +``` + +[warn header="Не используйте `&&` вместо `if`"] + +Оператор `&&` в простых случаях можно использовать вместо `if`, например: + +```js +//+ run +var x = 1; + +(x > 0) && alert('Больше'); +``` + +Действие в правой части `&&` выполнится только в том случае, если до него дойдут вычисления. То есть, если в левой части будет `true`. + +Получился аналог: + +```js +//+ run +var x = 1; + +if (x > 0) { + alert('Больше'); +} +``` + +Однако, как правило, `if` лучше читается и воспринимается. Он более очевиден, поэтому лучше использовать его. Это, впрочем, относится и к другим неочевидным применениям возможностей языка. +[/warn] + +## ! (НЕ) + +Оператор НЕ -- самый простой. Он получает один аргумент. Синтаксис: + +```js +var result = !value; +``` + +Действия `!`: + +
    +
  1. Сначала приводит аргумент к логическому типу `true/false`.
  2. +
  3. Затем возвращает противоположное значение.
  4. +
+ +Например: + +```js +//+ run +alert( !true ) // false +alert( !0 ) // true +``` + +**В частности, двойное НЕ используются для преобразования значений к логическому типу:** + +```js +//+ run +alert( !!"строка" ) // true +alert( !!null ) // false +``` + diff --git a/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/solution.md b/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/solution.md new file mode 100644 index 00000000..553696ef --- /dev/null +++ b/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/solution.md @@ -0,0 +1,31 @@ + + +```js +"" + 1 + 0 = "10" // (1) +"" - 1 + 0 = -1 // (2) +true + false = 1 +6 / "3" = 2 +"2" * "3" = 6 +4 + 5 + "px" = "9px" +"$" + 4 + 5
 = "$45" +"4" - 2
 = 2 +"4px" - 2
 = NaN +7 / 0
 = Infinity +parseInt("09")
 = "0" или "9" // (3) +" -9\n" + 5 = " -9\n5" +" -9\n" - 5 = -14 +5 && 2
 = 2 +2 && 5
 = 5 +5 || 0
 = 5 +0 || 5 = 5 +null + 1 = 1 // (4) +undefined + 1 = NaN // (5) +``` + +
    +
  1. Оператор `"+"` в данном случае прибавляет `1` как строку, и затем `0`.
  2. +
  3. Оператор `"-"` работает только с числами, так что он сразу приводит `""` к `0`.
  4. +
  5. В некоторых браузерах `parseInt` без второго аргумента интерпретирует `09` как восьмиричное число.
  6. +
  7. `null` при численном преобразовании становится `0`
  8. +
  9. `undefined` при численном преобразовании становится `NaN`
  10. +
\ No newline at end of file diff --git a/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/task.md b/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/task.md new file mode 100644 index 00000000..b5519c80 --- /dev/null +++ b/1-js/2-first-steps/14-types-conversion/1-primitive-conversions-questions/task.md @@ -0,0 +1,28 @@ +# Вопросник по преобразованиям, для примитивов + +[importance 5] + +Подумайте, какой результат будет у выражений ниже. Тут не только преобразования типов. Когда закончите -- сверьтесь с решением. + +```js +"" + 1 + 0 +"" - 1 + 0 +true + false +6 / "3" +"2" * "3" +4 + 5 + "px" +"$" + 4 + 5
 +"4" - 2
 +"4px" - 2
 +7 / 0
 +parseInt("09")
 +" -9\n" + 5 +" -9\n" - 5 +5 && 2
 +2 && 5
 +5 || 0
 +0 || 5 +null + 1 +undefined + 1 +``` + diff --git a/1-js/2-first-steps/14-types-conversion/article.md b/1-js/2-first-steps/14-types-conversion/article.md new file mode 100644 index 00000000..076e54fb --- /dev/null +++ b/1-js/2-first-steps/14-types-conversion/article.md @@ -0,0 +1,210 @@ +# Преобразование типов для примитивов + +Система преобразования типов в JavaScript очень проста, но отличается от других языков. Поэтому она часто служит "камнем преткновения" для приходящих из других языков программистов. +[cut] +Всего есть три преобразования: +
    +
  1. Cтроковое преобразование.
  2. +
  3. Числовое преобразование.
  4. +
  5. Преобразование к логическому значению.
  6. +
+ +**Эта глава описывает преобразование только примитивных значений, объекты разбираются далее в учебнике.** + + +## Строковое преобразование + +Строковое преобразование происходит, когда требуется представление чего-либо в виде строки. Например, его производит функция `alert`. + +```js +//+ run +var a = true; + +alert(a); // "true" +``` + +Можно также осуществить преобразование явным вызовом `String(val)`: + +```js +//+ run +alert( String(null) === "null" ); // true +``` + +Также для явного преобразования применяется оператор `"+"`, у которого один из аргументов строка. В этом случае он приводит к строке и другой аргумент, например: + +```js +//+ run +alert( true + "test" ); // "truetest" +alert( "123" + undefined); // "123undefined" +``` + +## Численное преобразование + +Численное преобразование происходит в математических функциях и выражениях, а также при сравнении данных различных типов (кроме сравнений `===`, `!==`). + +Для преобразования к числу в явном виде можно вызвать `Number(val)`, либо, что короче, поставить перед выражением оператор унарный плюс `"+"`: + +```js +var a = +"123"; // 123 +var a = Number("123"); // 123, тот же эффект +``` + + + + + + + +
ЗначениеПреобразуется в...
`undefined``NaN`
`null``0`
`true / false``1 / 0`
СтрокаПробельные символы по краям обрезаются.
Далее, если остаётся пустая строка, то `0`.
Из непустой строки "считывается" число, при ошибке результат: `NaN`.
+ +Например: + +```js +//+ run +alert( +" \n 123 \n \n"); // 123 +``` + +Ещё примеры: + + +### Специальные значения + +Посмотрим на поведение специальных значений более внимательно. + +**Интуитивно, значения `null/undefined` ассоциируются с нулём, но при преобразованиях ведут себя иначе.** + +Специальные значения преобразуются к числу так: + + + + +
ЗначениеПреобразуется в...
`undefined``NaN`
`null``0`
+ +Это преобразование осуществляется при арифметических операциях и сравнениях `> >= < <=`, но не при проверке равенства `==`. Алгоритм проверки равенства для этих значений в спецификации прописан отдельно (пункт [11.9.3](http://es5.github.com/x11.html#x11.9.3)). В нём считается, что `null` и `undefined` равны `"=="` между собой, но эти значения не равны никакому другому значению. + +Это ведёт к забавным последствиям. + +Например, `null` не подчиняется законам математики -- он "больше либо равен нулю": `null>=0`, но не больше и не равен: + +```js +//+ run +alert(null >= 0); // true, т.к. null преобразуется к 0 +alert(null > 0); // false (не больше), т.к. null преобразуется к 0 +alert(null == 0 ); // false (и не равен!), т.к. == рассматривает null особо. +``` + +Значение `undefined` вообще вне сравнений: + +```js +//+ run +alert(undefined > 0); // false, т.к. undefined -> NaN +alert(undefined == 0); // false, т.к. это undefined (без преобразования) +alert(undefined < 0); // false, т.к. undefined -> NaN +``` + +**Для более очевидной работы кода и во избежание ошибок лучше не давать специальным значениям участвовать в сравнениях `> >= < <=`.** + +Используйте в таких случаях переменные-числа или приводите к числу явно. + +## Логическое преобразование + +Преобразование к `true/false` происходит в логическом контексте, таком как `if(obj)`, `while(obj)` и при применении логических операторов. + +Все значения, которые интуитивно "пусты", становятся `false`. Их несколько: `0`, пустая строка, `null`, `undefined` и `NaN`. + +Остальное, в том числе и любые объекты -- `true`. + +Полная таблица преобразований: + + + + + + + +
ЗначениеПреобразуется в...
`undefined`, `null``false`
ЧислаВсе `true`, кроме `0`, `NaN` -- `false`.
СтрокиВсе `true`, кроме пустой строки `""` -- `false`
ОбъектыВсегда `true`
+ +**Для явного преобразования используется двойное логическое отрицание `!!value` или вызов `Boolean(value)`.** + +[warn header="Обратите внимание: строка `\"0\"` становится `true`"] +В отличие от многих языков программирования (например PHP), `"0"` в JavaScript является `true`, как и строка из пробелов: + +```js +//+ run +alert( !!"0" ); // true +alert( !!" " ); // любые непустые строки, даже из пробелов - true! +``` + +[/warn] + + +Логическое преобразование интересно тем, как оно сочетается с численным. + +**Два значения могут быть равны, но одно из них в логическом контексте `true`, другое -- `false`**. + +Например, равенства в следующем примере верны, так как происходит численное преобразование: + +```js +//+ run +alert( 0 == "\n0\n" ); // true +alert( false == " " ); // true +``` + +...А в логическом контексте левая часть даст `false`, правая -- `true`: + +```js +//+ run +if ("\n0\n") { + alert("true, совсем не как 0!"); +} +``` + +С точки зрения преобразования типов в JavaScript это совершенно нормально. При равенстве -- численное преобразование, а в `if` -- логическое, только и всего. + +## Итого + +В JavaScript есть три преобразования: + +
    +
  1. Строковое: `String(value)` -- в строковом контексте или при сложении со строкой
  2. +
  3. Численное: `Number(value)` -- в численном контексте, включая унарный плюс `+value`.
  4. +
  5. Логическое: `Boolean(value)` -- в логическом контексте, можно также сделать двойным НЕ: `!!value`.
  6. +
+ +**Сравнение не осуществляет преобразование типов в следующих случаях:** + + diff --git a/1-js/2-first-steps/15-while-for/1-loop-last-value/solution.md b/1-js/2-first-steps/15-while-for/1-loop-last-value/solution.md new file mode 100644 index 00000000..6bdf4691 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/1-loop-last-value/solution.md @@ -0,0 +1,26 @@ +Ответ: `1`. + +```js +//+ run +var i = 3; + +while(i) { + alert(i--); +} +``` + +Каждое выполнение цикла уменьшает `i`. Проверка `while(i)` даст сигнал "стоп" при `i = 0`. + +Соответственно, шаги цикла: + +```js +var i = 3 +alert(i--); // выведет 3, затем уменьшит i до 2 + +alert(i--) // выведет 2, затем уменьшит i до 1 + +alert(i--) // выведет 1, затем уменьшит i до 0 + +// все, проверка while(i) не даст выполняться циклу дальше +``` + diff --git a/1-js/2-first-steps/15-while-for/1-loop-last-value/task.md b/1-js/2-first-steps/15-while-for/1-loop-last-value/task.md new file mode 100644 index 00000000..46b79f7e --- /dev/null +++ b/1-js/2-first-steps/15-while-for/1-loop-last-value/task.md @@ -0,0 +1,14 @@ +# Последнее значение цикла + +[importance 3] + +Какое последнее значение выведет этот код? Почему? + +```js +var i = 3; + +while(i) { + alert(i--); +} +``` + diff --git a/1-js/2-first-steps/15-while-for/2-which-value-while/solution.md b/1-js/2-first-steps/15-while-for/2-which-value-while/solution.md new file mode 100644 index 00000000..5d0afef5 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/2-which-value-while/solution.md @@ -0,0 +1,31 @@ +
    +
  1. **От 1 до 4** + +```js +//+ run +var i = 0; +while (++i < 5) alert(i); +``` + +Первое значение: `i=1`, так как операция `++i` сначала увеличит `i`, а потом уже произойдёт сравнение и выполнение `alert`. + +Далее `2,3,4..` Значения выводятся одно за другим. Для каждого значения сначала происходит увеличение, а потом -- сравнение, так как `++` стоит перед переменной. + +При `i=4` произойдет увеличение `i` до `5`, а потом сравнение `while(5 < 5)` -- это неверно. Поэтому на этом цикл остановится, и значение `5` выведено не будет. +
  2. +
  3. **От 1 до 5** + +```js +//+ run +var i = 0; +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. +
\ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/2-which-value-while/task.md b/1-js/2-first-steps/15-while-for/2-which-value-while/task.md new file mode 100644 index 00000000..8d477177 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/2-which-value-while/task.md @@ -0,0 +1,23 @@ +# Какие значения i выведет цикл while? + +[importance 4] + +Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом. +
    +
  1. Префиксный вариант + +```js +var i = 0; +while (++i < 5) alert(i); +``` + +
  2. +
  3. Постфиксный вариант + +```js +var i = 0; +while (i++ < 5) alert(i); +``` + +
  4. +
\ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/3-which-value-for/solution.md b/1-js/2-first-steps/15-while-for/3-which-value-for/solution.md new file mode 100644 index 00000000..734224cf --- /dev/null +++ b/1-js/2-first-steps/15-while-for/3-which-value-for/solution.md @@ -0,0 +1,17 @@ +**Ответ: от 0 до 4 в обоих случаях.** + +```js +//+ run +for(var i=0; i<5; ++i) alert(i); + +for(var i=0; i<5; i++) alert(i); +``` + +Такой результат обусловлен алгоритмом работы `for`: +
    +
  1. Выполнить присвоение `i=0`
  2. +
  3. Проверить `i<5`
  4. +
  5. Если верно - выполнить тело цикла `alert(i)`, затем выполнить `i++`
  6. +
+ +Увеличение `i++` выполняется отдельно от проверки условия (2), значение `i` при этом не используется, поэтому нет никакой разницы между `i++` и `++i`. \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/3-which-value-for/task.md b/1-js/2-first-steps/15-while-for/3-which-value-for/task.md new file mode 100644 index 00000000..a08c4743 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/3-which-value-for/task.md @@ -0,0 +1,21 @@ +# Какие значения i выведет цикл for? + +[importance 4] + +Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом. +
    +
  1. Постфиксная форма: + +```js +for(var i=0; i<5; i++) alert(i); +``` + +
  2. +
  3. Префиксная форма: + +```js +for(var i=0; i<5; ++i) alert(i); +``` + +
  4. +
\ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/4-for-even/solution.md b/1-js/2-first-steps/15-while-for/4-for-even/solution.md new file mode 100644 index 00000000..993f351d --- /dev/null +++ b/1-js/2-first-steps/15-while-for/4-for-even/solution.md @@ -0,0 +1,12 @@ + + +```js +//+ run demo +for (var i = 2; i <= 10; i++) { + if ( i % 2 == 0) { + alert(i); + } +} +``` + +Чётность проверяется по остатку при делении на `2`, используя оператор "деление с остатком" `%`: `i % 2`. \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/4-for-even/task.md b/1-js/2-first-steps/15-while-for/4-for-even/task.md new file mode 100644 index 00000000..d20029d0 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/4-for-even/task.md @@ -0,0 +1,7 @@ +# Выведите чётные числа + +[importance 5] + +При помощи цикла `for` выведите чётные числа от `2` до `10`. + +[demo /] \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/5-replace-for-while/solution.md b/1-js/2-first-steps/15-while-for/5-replace-for-while/solution.md new file mode 100644 index 00000000..d0c6d428 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/5-replace-for-while/solution.md @@ -0,0 +1,11 @@ + + +```js +//+ run +var i = 0; +while (i < 3) { + alert("номер " + i + "!"); + i++; +} +``` + diff --git a/1-js/2-first-steps/15-while-for/5-replace-for-while/task.md b/1-js/2-first-steps/15-while-for/5-replace-for-while/task.md new file mode 100644 index 00000000..4898d8b9 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/5-replace-for-while/task.md @@ -0,0 +1,13 @@ +# Замените for на while + +[importance 5] + +Перепишите код, заменив цикл `for` на `while`, без изменения поведения цикла. + +```js +//+ run +for (var i = 0; i < 3; i++) { + alert("номер " + i + "!"); +} +``` + diff --git a/1-js/2-first-steps/15-while-for/6-repeat-until-correct/solution.md b/1-js/2-first-steps/15-while-for/6-repeat-until-correct/solution.md new file mode 100644 index 00000000..311bb68b --- /dev/null +++ b/1-js/2-first-steps/15-while-for/6-repeat-until-correct/solution.md @@ -0,0 +1,18 @@ + + +```js +//+ run demo +var num; + +do { + num = prompt("Введите число больше 100?", 0); +} while(num <= 100 && num != null); +``` + +Цикл `do..while` повторяется, пока верны две проверки: +
    +
  1. Проверка `num <= 100` -- то есть, введённое число всё еще меньше `100`.
  2. +
  3. Проверка `num != null` -- значение `null` означает, что посетитель нажал "Отмена", в этом случае цикл тоже нужно прекратить.
  4. +
+ +Кстати, сравнение `num <= 100` при вводе `null` даст `true`, так что вторая проверка необходима. \ No newline at end of file diff --git a/1-js/2-first-steps/15-while-for/6-repeat-until-correct/task.md b/1-js/2-first-steps/15-while-for/6-repeat-until-correct/task.md new file mode 100644 index 00000000..6fa1a462 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/6-repeat-until-correct/task.md @@ -0,0 +1,11 @@ +# Повторять цикл, пока ввод неверен + +[importance 5] + +Напишите цикл, который предлагает `prompt` ввести число, большее `100`. Если посетитель ввел другое число -- попросить ввести еще раз, и так далее. + +Цикл должен спрашивать число пока либо посетитель не введет число, большее `100`, либо не нажмет кнопку Cancel (ESC). + +Предполагается, что посетитель вводит только числа. + +[demo /] diff --git a/1-js/2-first-steps/15-while-for/article.md b/1-js/2-first-steps/15-while-for/article.md new file mode 100644 index 00000000..e1a60635 --- /dev/null +++ b/1-js/2-first-steps/15-while-for/article.md @@ -0,0 +1,179 @@ +# Циклы while, for + +При написании скриптов зачастую встает задача сделать однотипное действие много раз. + +Например, вывести товары из списка один за другим. Или просто перебрать все числа от 1 до 10 и для каждого выполнить одинаковый код. + +Для многократного повторения одного участка кода - предусмотрены *циклы*. +[cut] +## Цикл while + +Цикл `while` имеет вид: + +```js +while (условие) { + // код, тело цикла +} +``` + +Пока `условие` верно -- выполняется код из тела цикла. + +Например, цикл ниже выводит `i` пока `i < 3`: + +```js +//+ run +var i = 0; +while (i < 3) { + alert(i); + i++; +} +``` + +**Повторение цикла по-научному называется *"итерация"*. Цикл в примере выше совершает три итерации.** + +Если бы `i++` в коде выше не было, то цикл выполнялся бы (в теории) вечно. На практике, браузер выведет сообщение о "зависшем" скрипте и посетитель его остановит. + +**Бесконечный цикл** можно сделать и проще: + +```js +while (true) { + // ... +} +``` + +**Условие в скобках интерпретируется как логическое значение, поэтому вместо `while (i!=0)` обычно пишут `while (i)`**: + +```js +//+ run +var i = 3; +*!* +while (i) { // при i=0 значение в скобках будет false и цикл остановится +*/!* + alert(i); + i--; +} +``` + +## Цикл do..while + +Проверку условия можно поставить *под* телом цикла, используя специальный синтаксис `do..while`: + +```js +do { + // тело цикла +} while (условие); +``` + +Цикл, описанный, таким образом, сначала выполняет тело, а затем проверяет условие. + +Например: + +```js +//+ run +var i = 0; +do { + alert(i); + i++; +} while (i < 3); +``` + +Синтаксис `do..while` редко используется, т.к. обычный `while` нагляднее -- в нём не приходится искать глазами условие и ломать голову, почему оно проверяется именно в конце. + + +## Цикл for + +Чаще всего применяется цикл `for`. Выглядит он так: + +```js +for (начало; условие; шаг) { + // ... тело цикла ... +} +``` + +Пример цикла, который выполняет `alert(i)` для `i` от `0` до `2` включительно (до `3`): + +```js +//+ run +var i; + +for (i=0; i<3; i++) { + alert(i); +} +``` + +Здесь: + + +Цикл выполняется так: + +
    +
  1. Начало: `i=0` выполняется один-единственный раз, при заходе в цикл.
  2. +
  3. Условие: `i<3` проверяется перед каждой итерацией и при входе в цикл, если оно нарушено, то происходит выход.
  4. +
  5. Тело: `alert(i)`.
  6. +
  7. Шаг: `i++` выполняется после *тела* на каждой итерации, но перед проверкой условия.
  8. +
  9. Идти на шаг 2.
  10. +
+ +Иными, словами, поток выполнения: `начало` -> (если `условие` -> `тело` -> `шаг`) -> (если `условие` -> `тело` -> `шаг`) -> ... и так далее, пока верно `условие`. + +[smart] +В цикле также можно определить переменную: + +```js +//+ run +for (*!*var*/!* i=0; i<3; i++) { + alert(i); // 0, 1, 2 +} +``` + +Эта переменная будет видна и за границами цикла, в частности, после окончания цикла `i` станет равно `3`. +[/smart] + +## Пропуск частей for + +Любая часть `for` может быть пропущена. + +Например, можно убрать `начало`. Цикл в примере ниже полностью идентичен приведённому выше: + +```js +//+ run +var i = 0; + +for (; i<3; i++) { + alert(i); // 0, 1, 2 +} +``` + +Можно убрать и `шаг`: + +```js +//+ run +var i = 0; + +for (; i<3;) { + alert(i); + // цикл превратился в аналог while (i<3) +} +``` + +А можно и вообще убрать все, получив бесконечный цикл: + +```js +for (;;) { + // будет выполняться вечно +} +``` + +При этом сами точки с запятой `;` обязательно должны присутствовать, иначе будет ошибка синтаксиса. + +[smart header="`for..in`"] +Существует также специальная конструкция `for..in` для перебора свойств объекта. + +Мы познакомимся с ней позже, когда будем [говорить об объектах](#for..in). +[/smart] + diff --git a/1-js/2-first-steps/16-break-continue/1-list-primes/solution.md b/1-js/2-first-steps/16-break-continue/1-list-primes/solution.md new file mode 100644 index 00000000..7a566562 --- /dev/null +++ b/1-js/2-first-steps/16-break-continue/1-list-primes/solution.md @@ -0,0 +1,28 @@ +# Схема решения + +```js +Для всех i от 1 до 10 { + проверить, делится ли число i на какое-либо из чисел до него + если делится, то это i не подходит, берем следующее + если не делится, то i - простое число +} +``` + +# Решение + +Решение с использованием метки: + +```js +//+ run +nextPrime: +for(var i=2; i<10; i++) { + + for(var j=2; jn>1 - простое, если при делении на любое число от `2` до `n-1` есть остаток. + +**Создайте код, который выводит все простые числа из интервала от `2` до `10`.** Результат должен быть: `2,3,5,7`. + +P.S. Код также должен легко модифицироваться для любых других интервалов. + diff --git a/1-js/2-first-steps/16-break-continue/article.md b/1-js/2-first-steps/16-break-continue/article.md new file mode 100644 index 00000000..7a4b2adc --- /dev/null +++ b/1-js/2-first-steps/16-break-continue/article.md @@ -0,0 +1,198 @@ +# Директивы break и continue + +Для более гибкого управления циклом используются директивы `break` и `continue`. +[cut] +## Выход: break + +Выйти из цикла можно не только при проверке условия но и, вообще, в любой момент. Эту возможность обеспечивает директива `break`. + +Например, бесконечный цикл в примере прекратит выполнение при `i==5`: + +```js +var i=0; + +while(1) { + i++; + + *!*if (i==5) break;*/!* + + alert(i); +} + +alert('Последняя i = '+ i ); // 5 (*) +``` + +Выполнение продолжится со строки `(*)`, следующей за циклом. + +## Следующая итерация: continue [#continue] + +Директива `continue` прекращает выполнение *текущей итерации* цикла. Например, цикл ниже не выводит четные значения: + +```js +//+ run +for (var i = 0; i < 10; i++) { + + *!*if (i % 2 == 0) continue;*/!* + + alert(i); +} +``` + +Для четных `i` срабатывает `continue`, выполнение блока прекращается и управление передается на `for`. + +[smart header="Совет по стилю"] + +Как правило, `continue` и используют, чтобы не обрабатывать определенные значения в цикле. + +Цикл, который обрабатывает только часть значений, мог бы выглядеть так: + +```js +for (var i = 0; i < 10; i++) { + + if ( checkValue(i) ) { + // функция checkValue проверяет, подходит ли i + + // ... + // ... обработка + // ... этого + // ... значения + // ... цикла + // ... + + } +} +``` + +Все хорошо, но мы получили *дополнительный уровень вложенности фигурных скобок, без которого можно и нужно обойтись*. + +Гораздо лучше здесь использовать `continue`: + +```js +for (var i = 0; i < 10; i++) { + + *!*if ( !checkValue(i) ) continue;*/!* + + // здесь мы точно знаем, что i подходит + + // ... + // ... обработка + // ... этого + // ... значения + // ... цикла + // ... + +} +``` + +[/smart] + +[warn header="Нельзя использовать break/continue справа от оператора '?'"] +Обычно мы можем заменить `if` на оператор вопросительный знак `'?'`. + +То есть, запись: + +```js +if (условие) { + a(); +} else { + b(); +} +``` + +..Аналогична записи: + +```js +условие ? a() : b(); +``` + +В обоих случаях в зависимости от условия выполняется либо `a()` либо `b()`. + +Но разница состоит в том, что оператор вопросительный знак `'?'`, использованный во второй записи, возвращает значение. + +**Синтаксические конструкции, которые не возвращают значений, нельзя использовать в операторе `'?'`.** К таким относятся большинство конструкций и, в частности, `break/continue`. + +Поэтому такой код приведёт к ошибке: + +```js +(i > 5) ? alert(i) : *!*continue*/!*; +``` + +[/warn] + +## Метки + +Бывает нужно выйти одновременно из нескольких уровней цикла. + +Представим, что нужно ввести значения точек. У каждой точки есть две координаты `(i, j)`. Цикл для ввода значений `i,j = 0..2` может выглядеть так: + +```js +//+ run +for (var i = 0; i < 3; i++) { + + for (var j = 0; j < 3; j++) { + + var input = prompt("Значение в координатах " + i + "," + j, ""); + + if (input == null) *!*break*/!*; // (*) + + } +} +alert('Готово!'); +``` + +Здесь `break` используется, чтобы прервать ввод, если посетитель нажал на `Отмена`. Но обычный вызов `break` в строке `(*)` не может прервать два цикла сразу. Как же прервать ввод полностью? Один из способов -- поставить *метку*. + +Метка имеет вид `"имя:"`, имя должно быть уникальным. Она ставится перед циклом, вот так: + +```js +outer: for (var i = 0; i < 3; i++) { ... } +``` + +Можно также выносить ее на отдельную строку. Вызов `break outer` прерывает управление цикла с такой меткой, вот так: + +```js +//+ run +outer: +for (var i = 0; i < 3; i++) { + + for (var j = 0; j < 3; j++) { + + var input = prompt('Значение в координатах '+i+','+j, ''); + + if (input == null) *!*break outer*/!*; // (*) + + } +} +alert('Готово!'); +``` + +Директива `continue` также может быть использована с меткой. Управление перепрыгнет на следующую итерацию цикла с меткой. + +**Метки можно ставить в том числе на блок, без цикла:** + +```js +//+ run +my: { + + for (;;) { + for (i=0; i<10; i++) { + if (i>4) break my; + } + } + + some_code; // произвольный участок кода + +} +alert("После my"); // (*) +``` + +В примере выше, `break` перепрыгнет через `some_code`, выполнение продолжится сразу после блока `my`, со строки `(*)`. Возможность ставить метку на блоке используется редко. Обычно метки ставятся перед циклом. + +[smart header="Goto?"] +В некоторых языках программирования есть оператор `goto`, который может передавать управление на любой участок программы. + +Операторы `break/continue` более ограниченны. Они работают только внутри циклов, и метка должна быть не где угодно, а выше по уровню вложенности. + +В JavaScript нет `goto`. +[/smart] + diff --git a/1-js/2-first-steps/17-switch/1-rewrite-switch-if-else/solution.md b/1-js/2-first-steps/17-switch/1-rewrite-switch-if-else/solution.md new file mode 100644 index 00000000..98ca59a1 --- /dev/null +++ b/1-js/2-first-steps/17-switch/1-rewrite-switch-if-else/solution.md @@ -0,0 +1,20 @@ +Если совсем точно следовать условию, то сравнение должно быть строгим `'==='`. + +В реальном случае, скорее всего, подойдёт обычное сравнение `'=='`. + +```js +if(browser == 'IE') { + alert('О, да у вас IE!'); +} else if (browser == 'Chrome' + || browser == 'Firefox' + || browser == 'Safari' + || browser == 'Opera') { + alert('Да, и эти браузеры мы поддерживаем'); +} else { + alert('Мы надеемся, что и в вашем браузере все ок!'); +} +``` + +Обратите внимание: конструкция `browser == 'Chrome' || browser == 'Firefox' ...` разбита на несколько строк для лучшей читаемости. + +Но всё равно запись через `switch` нагляднее. \ No newline at end of file diff --git a/1-js/2-first-steps/17-switch/1-rewrite-switch-if-else/task.md b/1-js/2-first-steps/17-switch/1-rewrite-switch-if-else/task.md new file mode 100644 index 00000000..f9d936ea --- /dev/null +++ b/1-js/2-first-steps/17-switch/1-rewrite-switch-if-else/task.md @@ -0,0 +1,24 @@ +# Напишите "if", аналогичный "switch" + +[importance 5] + +Напишите `if..else`, соответствующий следующему `switch`: + +```js +switch(browser) { + case 'IE': + alert('О, да у вас IE!'); + break; + + case 'Chrome': + case 'Firefox': + case 'Safari': + case 'Opera': + alert('Да, и эти браузеры мы поддерживаем'); + break; + + default: + alert('Мы надеемся, что и в вашем браузере все ок!'); +} +``` + diff --git a/1-js/2-first-steps/17-switch/2-rewrite-if-switch/solution.md b/1-js/2-first-steps/17-switch/2-rewrite-if-switch/solution.md new file mode 100644 index 00000000..149bcfa1 --- /dev/null +++ b/1-js/2-first-steps/17-switch/2-rewrite-if-switch/solution.md @@ -0,0 +1,27 @@ +Первые две проверки -- обычный `case`, третья разделена на два `case`: + +```js +//+ run +var a = +prompt('a?', ''); + +switch(a) { + case 0: + alert(0); + break; + + case 1: + alert(1); + break; + + case 2: + case 3: + alert('2,3'); +*!* + break; +*/!* +} +``` + +Обратите внимание: `break` внизу не обязателен, но ставится по "правилам хорошего тона". + +Допустим, он не стоит. Есть шанс, что в будущем нам понадобится добавить в конец ещё один `case`, например `case 4`, и мы, вполне вероятно, забудем этот `break` поставить. В результате выполнение `case 2`/`case 3` продолжится на `case 4` и будет ошибка. diff --git a/1-js/2-first-steps/17-switch/2-rewrite-if-switch/task.md b/1-js/2-first-steps/17-switch/2-rewrite-if-switch/task.md new file mode 100644 index 00000000..a967428e --- /dev/null +++ b/1-js/2-first-steps/17-switch/2-rewrite-if-switch/task.md @@ -0,0 +1,22 @@ +# Переписать if'ы в switch + +[importance 4] + +Перепишите код с использованием одной конструкции `switch`: + +```js +//+ run +var a = +prompt('a?', ''); + +if (a == 0) { + alert(0); +} +if (a == 1) { + alert(1); +} + +if (a == 2 || a == 3) { + alert('2,3'); +} +``` + diff --git a/1-js/2-first-steps/17-switch/article.md b/1-js/2-first-steps/17-switch/article.md new file mode 100644 index 00000000..b94a6f9e --- /dev/null +++ b/1-js/2-first-steps/17-switch/article.md @@ -0,0 +1,185 @@ +# Конструкция switch + +Конструкция `switch` заменяет собой сразу несколько `if`. + +Это -- более наглядный способ сравнить выражение сразу с несколькими вариантами. +[cut] +## Синтаксис + +Выглядит она так: + +```js +switch(x) { + case 'value1': // if (x === 'value1') + ... + [break] + + case 'value2': // if (x === 'value2') + ... + [break] + + default: + ... + [break] +} +``` + + + +При этом `case` называют *вариантами `switch`*. + +## Пример работы + +Пример использования `switch` (сработавший код выделен): + +```js +//+ run +var a = 2+2; + +switch (a) { + case 3: + alert('Маловато'); + break; +*!* + case 4: + alert('В точку!'); + break; +*/!* + case 5: + alert('Перебор'); + break; + default: + alert('Я таких значений не знаю'); +} +``` + +Будет выведено только одно значение, соответствующее `4`. После чего `break` прервёт выполнение. + +**Если его не прервать -- оно пойдёт далее, при этом остальные проверки игнорируются.** + +Например: + +```js +//+ run +var a = 2+2; + +switch (a) { + case 3: + alert('Маловато'); +*!* + case 4: + alert('В точку!'); + case 5: + alert('Перебор'); + default: + alert('Я таких значений не знаю'); +*/!* +} +``` + +В примере выше последовательно выполнятся три `alert`. + +```js +alert('В точку!'); +alert('Перебор'); +alert('Я таких значений не знаю'); +``` + +**В `case` могут быть любые выражения**, в том числе включающие в себя переменные и функции. + +Например: + +```js +//+ run +var a = 1; +var b = 0; + +switch(a) { +*!* + case b+1: + alert(1); + break; +*/!* + + default: + alert('нет-нет, выполнится вариант выше') +} +``` + +## Группировка case + +Несколько значений case можно группировать. + +В примере ниже `case 3` и `case 5` выполняют один и тот же код: + +```js +//+ run +var a = 2+2; + +switch (a) { + case 4: + alert('Верно!'); + break; + +*!* + case 3: // (*) + case 5: // (**) + alert('Неверно!'); + alert('Немного ошиблись, бывает.'); + break; +*/!* + + default: + alert('Странный результат, очень странный'); +} +``` + +При `case 3` выполнение идёт со строки `(*)`, при `case 5` -- со строки `(**)`. + +## Тип имеет значение + +Следующий пример принимает значение от посетителя. + +```js +//+ run +var arg = prompt("Введите arg?") +switch(arg) { + case '0': + case '1': + alert('Один или ноль'); + + case '2': + alert('Два'); + break; + + case 3: + alert('Никогда не выполнится'); + + case null: + alert('Отмена'); + break; + + default: + alert('Неизвестное значение: ' + arg) +} +``` + +Что оно выведет при вводе чисел 0, 1, 2, 3? Подумайте и *потом* читайте дальше... + + + diff --git a/1-js/2-first-steps/18-function-basics/1-if-else-required/solution.md b/1-js/2-first-steps/18-function-basics/1-if-else-required/solution.md new file mode 100644 index 00000000..be1daacc --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/1-if-else-required/solution.md @@ -0,0 +1 @@ +Оба варианта функции работают одинаково, отличий нет. \ No newline at end of file diff --git a/1-js/2-first-steps/18-function-basics/1-if-else-required/task.md b/1-js/2-first-steps/18-function-basics/1-if-else-required/task.md new file mode 100644 index 00000000..217ea049 --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/1-if-else-required/task.md @@ -0,0 +1,35 @@ +# Обязателен ли "else"? + +[importance 4] + +Следующая функция возвращает `true`, если параметр `age` больше `18`. +В ином случае она задает вопрос посредством вызова `confirm` и возвращает его результат. + +```js +function checkAge(age) { + if (age > 18) { + return true; +*!* + } else { + // ... + return confirm('Родители разрешили?'); + } +*/!* +} +``` + +Будет ли эта функция работать как-то иначе, если убрать `else`? + +```js +function checkAge(age) { + if (age > 18) { + return true; + } +*!* + // ... + return confirm('Родители разрешили?'); +*/!* +} +``` + +Есть ли хоть одно отличие в поведении этого варианта? \ No newline at end of file diff --git a/1-js/2-first-steps/18-function-basics/2-rewrite-function-question-or/solution.md b/1-js/2-first-steps/18-function-basics/2-rewrite-function-question-or/solution.md new file mode 100644 index 00000000..f021c7e4 --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/2-rewrite-function-question-or/solution.md @@ -0,0 +1,16 @@ +Используя оператор `'?'`: + +```js +function checkAge(age) { + return (age > 18) ? true : confirm('Родители разрешили?'); +} +``` + +Используя оператор `||` (самый короткий вариант): + +```js +function checkAge(age) { + return (age > 18) || confirm('Родители разрешили?'); +} +``` + diff --git a/1-js/2-first-steps/18-function-basics/2-rewrite-function-question-or/task.md b/1-js/2-first-steps/18-function-basics/2-rewrite-function-question-or/task.md new file mode 100644 index 00000000..a2523c4f --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/2-rewrite-function-question-or/task.md @@ -0,0 +1,23 @@ +# Перепишите функцию, используя оператор '?' или '||' + +[importance 4] + +Следующая функция возвращает `true`, если параметр `age` больше `18`. +В ином случае она задает вопрос `confirm` и возвращает его результат. + +```js +function checkAge(age) { + if (age > 18) { + return true; + } else { + return confirm('Родители разрешили?'); + } +} +``` + +Перепишите функцию, чтобы она делала то же самое, но без `if`, в одну строку. +Сделайте два варианта функции `checkAge`: +
    +
  1. Используя оператор `'?'`
  2. +
  3. Используя оператор `||`
  4. +
diff --git a/1-js/2-first-steps/18-function-basics/3-min/solution.md b/1-js/2-first-steps/18-function-basics/3-min/solution.md new file mode 100644 index 00000000..91809daa --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/3-min/solution.md @@ -0,0 +1,21 @@ +Вариант решения с использованием `if`: + +```js +function min(a, b) { + if (a < b) { + return a; + } else { + return b; + } +} +``` + +Вариант решения с оператором `'?'`: + +```js +function min(a, b) { + return a < b ? a : b; +} +``` + +P.S. Случай равенства `a == b` здесь отдельно не рассматривается, так как при этом неважно, что возвращать. \ No newline at end of file diff --git a/1-js/2-first-steps/18-function-basics/3-min/task.md b/1-js/2-first-steps/18-function-basics/3-min/task.md new file mode 100644 index 00000000..8d042382 --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/3-min/task.md @@ -0,0 +1,16 @@ +# Функция min + +[importance 1] + +Задача "Hello World" для функций :) + +Напишите функцию `min(a,b)`, которая возвращает меньшее из чисел `a,b`. + +Пример вызовов: + +```js +min(2, 5) == 2 +min(3, -1) == -1 +min(1, 1) == 1 +``` + diff --git a/1-js/2-first-steps/18-function-basics/4-pow/solution.md b/1-js/2-first-steps/18-function-basics/4-pow/solution.md new file mode 100644 index 00000000..7d818e30 --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/4-pow/solution.md @@ -0,0 +1,34 @@ + + +```js +//+ run demo +/** + * Возводит x в степень n (комментарий JSDoc) + * + * @param {number} x число, которое возводится в степень + * @param {number} n степень, должна быть целым числом больше 1 + * + * @return {number} x в степени n + */ +function pow(x, n) { + var result = x; + + for(var i = 1; i < n; i++) { + result *= x; + } + + return result; +} + +var x = prompt("x?", ''); +var n = prompt("n?", ''); + +if (n <= 1) { + alert('Степень ' + n + + 'не поддерживается, введите целую степень, большую 1' + ); +} else { + alert( pow(x, n) ); +} +``` + diff --git a/1-js/2-first-steps/18-function-basics/4-pow/task.md b/1-js/2-first-steps/18-function-basics/4-pow/task.md new file mode 100644 index 00000000..522badc3 --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/4-pow/task.md @@ -0,0 +1,17 @@ +# Функция pow(x,n) + +[importance 4] + +Напишите функцию `pow(x,n)`, которая возвращает `x` в степени `n`. Иначе говоря, умножает `x` на себя `n` раз и возвращает результат. + +```js +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)`. + +[demo /] + +P.S. В этой задаче функция обязана поддерживать только натуральные значения `n`, т.е. целые от `1` и выше. \ No newline at end of file diff --git a/1-js/2-first-steps/18-function-basics/article.md b/1-js/2-first-steps/18-function-basics/article.md new file mode 100644 index 00000000..c1322276 --- /dev/null +++ b/1-js/2-first-steps/18-function-basics/article.md @@ -0,0 +1,457 @@ +# Функции + +Зачастую нам надо повторять одно и то же действие во многих частях программы. + +Например, красиво вывести сообщение необходимо при приветствии посетителя, при выходе посетителя с сайта, еще где-нибудь. + +Чтобы не повторять один и тот же код во многих местах, придуманы функции. Функции являются основными "строительными блоками" программы. +[cut] +Примеры встроенных функций вы уже видели -- это `alert(message)`, `prompt(message, default)` и `confirm(question)`. Но можно создавать и свои. + +## Объявление + +Пример объявления функции: + +```js +function showMessage() { + alert('Привет всем присутствующим!'); +} +``` + +Вначале идет ключевое слово `function`, после него *имя функции*, затем *список параметров* в скобках (в примере выше он пустой) и *тело функции* -- код, который выполняется при её вызове. + +Объявленная функция доступна по имени, например: + +```js +//+ run +function showMessage() { + alert('Привет всем присутствующим!'); +} + +*!* +showMessage(); +showMessage(); +*/!* +``` + +Этот код выведет сообщение два раза. Уже здесь видна **главная цель создания функций: избавление от дублирования кода**. + +Если понадобится поменять сообщение или способ его вывода -- достаточно изменить его в одном месте: в функции, которая его выводит. + +## Локальные переменные + +Функция может содержать *локальные* переменные, объявленные через `var`. Такие переменные видны только внутри функции: + +```js +//+ run +function showMessage() { +*!* + var message = 'Привет, я - Вася!'; // локальная переменная +*/!* + + alert(message); +} + +showMessage(); // 'Привет, я - Вася!' + +alert(message); // <-- будет ошибка, т.к. переменная видна только внутри +``` + +**Блоки `if/else`, `switch`, `for`, `while`, `do..while` не влияют на область видимости переменных.** + +При объявлении переменной в таких блоках, она всё равно будет видна во всей функции. + +Например: + +```js +function count() { + for (*!*var*/!* i=0; i<3; i++) { + *!*var*/!* j = i * 2; + } + +*!* + alert(i); // i=3, на этом значении цикл остановился + alert(j); // j=4, последнее значение, на котором цикл сработал, было i=2 +*/!* +} +``` + +**Неважно, где именно в функции и сколько раз объявляется переменная. Любое объявление срабатывает один раз и распространяется на всю функцию.** + +Объявления переменных в примере выше можно передвинуть вверх, это ни на что не повлияет: + +```js +function count() { +*!* + var i, j; // передвинули объявления var в начало +*/!* + for (i=0; i<3; i++) { + j = i * 2; + } + + alert(i); // i=3 + alert(j); // j=4 +} +``` + +## Внешние переменные + +Функция может обратиться ко внешней переменной, например: + +```js +//+ run +var *!*userName*/!* = 'Вася'; + +function showMessage() { + var message = 'Привет, я ' + *!*userName*/!*; + alert(message); +} + +showMessage(); // Привет, я Вася +``` + +Доступ возможен не только на чтение, но и на запись. При этом, так как переменная внешняя, то изменения будут видны и снаружи функции: + +```js +//+ run +var *!*userName*/!* = 'Вася'; + +function showMessage() { +*!* + userName = 'Петя'; // (1) присвоение во внешнюю переменную +*/!* + var message = 'Привет, я ' + *!*userName*/!*; + alert(message); +} + +showMessage(); + +*!* +alert(userName); // Петя, значение внешней переменной изменено функцией +*/!* +``` + +Конечно, если бы внутри функции, в строке `(1)`, была бы объявлена своя локальная переменная `var userName`, то все обращения использовали бы её, и внешняя переменная осталась бы неизменной. + +[summary] +**Переменные, объявленные на уровне всего скрипта, называют *"глобальными переменными"*.** + +Делайте глобальными только те переменные, которые действительно имеют общее значение для вашего проекта. + +Пусть каждая функция работает "в своей песочнице". +[/summary] + + +[warn header="Внимание: неявное объявление глобальных переменных!"] + +В старом стандарте JavaScript существовала возможность неявного объявления переменных присвоением значения. + +Например: + +```js +//+ run +function showMessage() { + message = 'Привет'; // без var! +} + +showMessage(); + +alert(message); // Привет +``` + +В коде выше переменная `message` нигде не объявлена, а сразу присваивается. Скорее всего, программист просто забыл поставить `var`. + +При `use strict` такой код привёл бы к ошибке, но без него переменная будет создана автоматически, причём в примере выше она создаётся не в функции, а на уровне всего скрипта. + +Избегайте этого. + +Здесь опасность даже не в автоматическом создании переменной, а в том, что глобальные переменные должны использоваться тогда, когда действительно нужны "общескриптовые" параметры. + +Забыли `var` в одном месте, потом в другом -- в результате одна функция неожиданно поменяла глобальную переменную, которую использует другая. Возможна ошибка и потеря времени на поиск проблемы. +[/warn] + +В будущем, когда мы лучше познакомимся с основами JavaScript, в главе [](/closures), мы более детально рассмотрим внутренние механизмы работы переменных и функций. + +## Параметры + +При вызове функции ей можно передать данные, которые та использует по своему усмотрению. + +Например, этот код выводит два сообщения: + +```js +//+ run +function showMessage(*!*from, text*/!*) { // параметры from, text + + from = "** " + from + " **"; // здесь может быть сложный код оформления + + alert(from + ': ' + text); +} + +*!* +showMessage('Маша', 'Привет!'); +showMessage('Маша', 'Как дела?'); +*/!* +``` + +**Параметры копируются в локальные переменные функции**. + +Например, в коде ниже есть внешняя переменная `from`, значение которой при запуске функции копируется в параметр функции с тем же именем. Далее функция работает уже с параметром: + +```js +//+ run +function showMessage(from, text) { +*!* + from = '**' + from + '**'; // меняем локальную переменную (1) +*/!* + alert(from + ': ' + text); +} + +var from = "Маша"; + +showMessage(from, "Привет"); + +alert(from); // "Маша", без изменений, так как в строке (1) была изменена копия значения +``` + +Здесь есть небольшая тонкость при работе с объектами. Как мы помним, в переменной хранится ссылка на объект. Поэтому функция, получив параметр-объект, работает с самим этим объектом: + +Например, в коде ниже функция по ссылке меняет содержимое объекта `user`: + +```js +//+ run +function makeAdmin(user) { + user.isAdmin = true; +} + +var user = { name: "Вася" }; + +makeAdmin(user); +alert(user.isAdmin); // true +``` + +## Стиль объявления функций + +В объявлении функции есть правила для расстановки пробелов. Они отмечены стрелочками: + + + +Конечно, вы можете ставить пробелы и по-другому, но эти правила используются в большинстве JavaScript-фреймворков. +## Аргументы по умолчанию + +Функцию можно вызвать с любым количеством аргументов. + +Если параметр не передан при вызове -- он считается равным `undefined`. + +Например, функцию показа сообщения `showMessage(from, text)` можно вызвать с одним аргументом: + +```js +showMessage("Маша"); +``` + +При этом можно проверить, и если параметр не передан -- присвоить ему значение "по умолчанию": + +```js +//+ run +function showMessage(from, text) { +*!* + if (text === undefined) { + text = 'текст не передан'; + } +*/!* + + alert(from + ": " + text); +} + +showMessage("Маша", "Привет!"); // Маша: Привет! +*!* +showMessage("Маша"); // Маша: текст не передан +*/!* +``` + +**При объявлении функции необязательные аргументы, как правило, располагают в конце списка.** + +Для указания значения "по умолчанию", то есть, такого, которое используется, если аргумент не указан, используется два способа: + +
    +
  1. Можно проверить, равен ли аргумент `undefined`, и если да -- то записать в него значение по умолчанию. Этот способ продемонстрирован в примере выше.
  2. +
  3. Использовать оператор `||`: + +```js +//+ run +function showMessage(from, text) { + text = text || 'текст не передан'; + + ... +} +``` + +Второй способ считает, что аргумент отсутствует, если передана пустая строка, `0`, или вообще любое значение, которое в булевом виде является `false`. +
  4. +
+ +Если аргументов передано больше, чем надо, например `showMessage("Маша", "привет", 1, 2, 3)`, то ошибки не будет. Но, чтобы получить такие "лишние" аргументы, нужно будет прочитать их из специального объекта `arguments`, который мы рассмотрим в главе [](/arguments-pseudoarray). + +## Возврат значения + +Функция может возвратить результат, который будет передан в вызвавший её код. + +Например, создадим функцию `calcD`, которая будет возвращать дискриминант квадратного уравнения по формуле b2 - 4ac: + +```js +//+ run +function calcD(a, b, c) { + *!*return*/!* b*b - 4*a*c; +} + +var test = calcD(-4, 2, 1); +alert(test); // 20 +``` + +**Для возврата значения используется директива `return`.** + +Она может находиться в любом месте функции. Как только до нее доходит управление -- функция завершается и значение передается обратно. + +Вызовов `return` может быть и несколько, например: + +```js +//+ run +function checkAge(age) { + if (age > 18) { + return true; + } else { + return confirm('Родители разрешили?'); + } +} + +var age = prompt('Ваш возраст?'); + +if (checkAge(age)) { + alert('Доступ разрешен'); +} else { + alert('В доступе отказано'); +} +``` + +Директива `return` может также использоваться без значения, чтобы прекратить выполнение и выйти из функции. + +Например: + +```js +function showMovie(age) { + if (!checkAge(age)) { +*!* + return; +*/!* + } + + alert("Фильм не для всех"); // (*) + // ... +} +``` + +В коде выше, если сработал `if`, то строка `(*)` и весь код под ней никогда не выполнится, так как `return` завершает выполнение функции. + +[smart header="Значение функции без `return` и с пустым `return`"] +В случае, когда функция не вернула значение или `return` был без аргументов, считается что она вернула `undefined`: + +```js +//+ run +function doNothing() { /* пусто */ } + +alert( doNothing() ); // undefined +``` + +Обратите внимание, никакой ошибки нет. Просто возвращается `undefined`. + +Ещё пример, на этот раз с `return` без аргумента: + +```js +//+ run +function doNothing() { + return; +} + +alert( doNothing() === undefined ); // true +``` + +[/smart] + +## Выбор имени функции [#function-naming] + +Имя функции следует тем же правилам, что и имя переменной. Основное отличие -- оно должно быть глаголом, т.к. функция -- это действие. + +Как правило, используются глагольные префиксы, обозначающие общий характер действия, после которых следует уточнение. + +Функции, которые начинаются с `"show"` -- что-то показывают: + +```js +showMessage(..) // префикс show, "показать" сообщение +``` + +Функции, начинающиеся с `"get"` -- получают, и т.п.: + +```js +getAge(..) // get, "получает" возраст +calcD(..) // calc, "вычисляет" дискриминант +createForm(..) // create, "создает" форму +checkPermission(..) // check, "проверяет" разрешение, возвращает true/false +``` + +Это очень удобно, поскольку взглянув на функцию -- мы уже примерно представляем, что она делает, даже если функцию написал совсем другой человек, а в отдельных случаях -- и какого вида значение она возвращает. + +[smart header="Одна функция -- одно действие"] + +Функция должна делать только то, что явно подразумевается её названием. И это должно быть одно действие. + +Если оно сложное и подразумевает поддействия -- может быть имеет смысл выделить их в отдельные функции? Зачастую это имеет смысл, чтобы лучше структурировать код. + +**...Но самое главное -- в функции не должно быть ничего, кроме самого действия и поддействий, неразрывно связанных с ним.** + +Например, функция проверки данных (скажем, `"validate"`) не должна показывать сообщение об ошибке. Её действие -- проверить. +[/smart] + + +[smart header="Сверхкороткие имена функций"] +Имена функций, которые используются *очень часто*, иногда делают сверхкороткими. + +Например, во фреймворке [jQuery](http://jquery.com) есть функция `$`, во фреймворке [Prototype](http://prototypejs.com) -- функция `$$`, а в библиотеке [LoDash](http://lodash.com/) очень активно используется функция с названием из одного символа подчеркивания `_`. +[/smart] + +## Итого + +Объявление функции имеет вид: + +```js +function имя(параметры, через, запятую) { + код функции +} +``` + + + +При обращении к необъявленной переменной функция будет искать внешнюю переменную с таким именем, но лучше, если функция использует только локальные переменные: + + + + + +Именование функций: + + + +Функции являются основными строительными блоками скриптов. Мы будем неоднократно возвращаться к ним и изучать все более и более глубоко. + + diff --git a/1-js/2-first-steps/18-function-basics/style.png b/1-js/2-first-steps/18-function-basics/style.png new file mode 100755 index 00000000..d0a49dcc Binary files /dev/null and b/1-js/2-first-steps/18-function-basics/style.png differ diff --git a/1-js/2-first-steps/19-recursion/1-sum-to/solution.md b/1-js/2-first-steps/19-recursion/1-sum-to/solution.md new file mode 100644 index 00000000..4fba66fe --- /dev/null +++ b/1-js/2-first-steps/19-recursion/1-sum-to/solution.md @@ -0,0 +1,45 @@ +Решение **с использованием цикла**: + +```js +//+ run +function sumTo(n) { + var sum = 0; + for(var i=1; i<=n; i++) { + sum += i; + } + return sum; +} + +alert( sumTo(100) ); +``` + +Решение через **рекурсию**: + +```js +//+ run +function sumTo(n) { + if (n == 1) return 1; + return n + sumTo(n-1); +} + +alert( sumTo(100) ); +``` + +Решение **по формуле**: `sumTo(n) = n*(n+1)/2`: + +```js +//+ run +function sumTo(n) { + return n*(n+1)/2; +} + +alert( sumTo(100) ); +``` + +P.S. Надо ли говорить, что решение по формуле работает быстрее всех? Это очевидно. Оно использует всего три операции для любого `n`, а цикл и рекурсия требуют как минимум `n` операций сложения. + +Вариант с циклом -- второй по скорости. Он быстрее рекурсии, так как операций сложения столько же, но нет дополнительных вычислительных затрат на организацию вложенных вызовов. + +Рекурсия в данном случае работает медленнее всех. + +P.P.S. Существует ограничение глубины вложенных вызовов, поэтому рекурсивный вызов `sumTo(100000)` выдаст ошибку. \ No newline at end of file diff --git a/1-js/2-first-steps/19-recursion/1-sum-to/task.md b/1-js/2-first-steps/19-recursion/1-sum-to/task.md new file mode 100644 index 00000000..0d122262 --- /dev/null +++ b/1-js/2-first-steps/19-recursion/1-sum-to/task.md @@ -0,0 +1,33 @@ +# Вычислить сумму чисел до данного + +[importance 5] + +Напишите функцию `sumTo(n)`, которая для данного `n` вычисляет сумму чисел от 1 до `n`, например: + +```js +sumTo(1) = 1 +sumTo(2) = 2 + 1 = 3 +sumTo(3) = 3 + 2 + 1 = 6 +sumTo(4) = 4 + 3 + 2 + 1 = 10 +... +sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050 +``` + +Сделайте три варианта решения: +
    +
  1. С использованием цикла.
  2. +
  3. Через рекурсию, т.к. `sumTo(n) = n + sumTo(n-1)` для `n > 1`.
  4. +
  5. С использованием формулы для суммы [арифметической прогрессии](http://ru.wikipedia.org/wiki/%D0%90%D1%80%D0%B8%D1%84%D0%BC%D0%B5%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D1%8F).
  6. +
+ +Пример работы вашей функции: + +```js +function sumTo(n) { /*... ваш код ... */ } + +alert( sumTo(100) ); // 5050 +``` + +**Какой вариант решения самый быстрый? Самый медленный? Почему?** + +**Можно ли при помощи рекурсии посчитать `sumTo(100000)`? Если нет, то почему?** diff --git a/1-js/2-first-steps/19-recursion/2-factorial/solution.md b/1-js/2-first-steps/19-recursion/2-factorial/solution.md new file mode 100644 index 00000000..dcc08cc3 --- /dev/null +++ b/1-js/2-first-steps/19-recursion/2-factorial/solution.md @@ -0,0 +1,25 @@ +По свойствам факториала, как описано в условии, `n!` можно записать как `n * (n-1)!`. + +То есть, результат функции для `n` можно получить как `n`, умноженное на результат функции для `n-1`, и так далее до `1!`: + +```js +//+ run +function factorial(n) { + return (n!=1) ? n*factorial(n-1) : 1; +} + +alert( factorial(5) ); // 120 +``` + +Базисом рекурсии является значение `1`. А можно было бы сделать базисом и `0`. Тогда код станет чуть короче: + +```js +//+ run +function factorial(n) { + return n ? n*factorial(n-1) : 1; +} + +alert( factorial(5) ); // 120 +``` + +В этом случае вызов `factorial(1)` сведется к `1*factorial(0)`, будет дополнительный шаг рекурсии. diff --git a/1-js/2-first-steps/19-recursion/2-factorial/task.md b/1-js/2-first-steps/19-recursion/2-factorial/task.md new file mode 100644 index 00000000..47ef048a --- /dev/null +++ b/1-js/2-first-steps/19-recursion/2-factorial/task.md @@ -0,0 +1,29 @@ +# Вычислить факториал + +[importance 4] + +*Факториа́л числа* -- это число, умноженное на "себя минус один", затем на "себя минус два" и так далее, до единицы. Обозначается `n!` + +Определение факториала можно записать как: + +```js +n! = n*(n-1)*(n-2)*...*1 +``` + +Примеры значений для разных `n`: + +```js +1! = 1 +2! = 2*1 = 2 +3! = 3*2*1 = 6 +4! = 4*3*2*1 = 24 +5! = 5*4*3*2*1 = 120 +``` + +Задача -- написать функцию `factorial(n)`, которая возвращает факториал числа `n!`, используя рекурсивный вызов. + +```js +alert( factorial(5) ); // 120 +``` + +Подсказка: обратите внимание, что `n!` можно записать как `n * (n-1)!`, например `3! = 3*2! = 3*2*1! = 6` \ No newline at end of file diff --git a/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/solution.md b/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/solution.md new file mode 100644 index 00000000..4e6b2301 --- /dev/null +++ b/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/solution.md @@ -0,0 +1,94 @@ +# Вычисление рекурсией (медленное) + +Решение по формуле, используя рекурсию: + +```js +//+ run +function fib(n) { + return n <= 1 ? n : fib(n-1) + fib(n-2); +} + +alert( fib(3) ); // 2 +alert( fib(7) ); // 13 +// fib(77); // не запускаем, подвесит браузер +``` + +При больших значениях `n` оно будет работать очень медленно. Например, `fib(77)` уже будет вычисляться очень долго. + +Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений вычисляются много раз. Например, посмотрим на отрывок вычислений: + +```js +... +fib(5) = fib(4) + fib(3) +fib(4) = fib(3) + fib(2) +... +``` + +Здесь видно, что значение `fib(3)` нужно одновременно и для `fib(5)` и для `fib(4)`. В коде оно будет вычислено два раза, совершенно независимо. + +Можно это оптимизировать, запоминая уже вычисленные значения, получится гораздо быстрее. Альтернативный вариант -- вообще отказаться от рекурсии, а вместо этого в цикле начать с первых значений `1`, `2`, затем из них получить `fib(3)`, далее `fib(4)`, затем `fib(5)` и так далее, до нужного значения. + +Это решение будет наиболее эффективным. Попробуйте его написать. + +# Алгоритм вычисления в цикле + +Будем идти по формуле слева-направо: + +```js +var a = 1, b = 1; // начальные значения +var c = a + b; // 2 + +/* переменные на начальном шаге: +a b c +1, 1, 2 +*/ +``` + +Теперь следующий шаг, присвоим `a` и `b` текущие 2 числа и получим новое следующее в `c`: + +```js +a = b, b = c; +c = a + b; + +/* стало так (еще число): + a b c +1, 1, 2, 3 +*/ +``` + +Следующий шаг даст нам еще одно число последовательности: + +```js +a = b, b = c; +c = a + b; + +/* стало так (еще число): + a b c +1, 1, 2, 3, 5 +*/ +``` + +Повторять в цикле до тех пор, пока не получим нужное значение. Это гораздо быстрее, чем рекурсия, хотя бы потому что ни одно из чисел не вычисляется дважды. + +P.S. Этот подход к вычислению называется [динамическое программирование снизу-вверх](http://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5). + +# Код для вычисления в цикле + +```js +//+ run +function fib(n) { + var a = 1, b = 1; + for (var i = 3; i <= n; i++) { + var c = a + b; + a = b; + b = c; + } + return b; +} + +alert( fib(3) ); // 2 +alert( fib(7) ); // 13 +alert( fib(77)); // 5527939700884757 +``` + +Цикл здесь начинается с `i=3`, так как первое и второе числа Фибоначчи заранее записаны в переменные `a=1`, `b=1`. \ No newline at end of file diff --git a/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/task.md b/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/task.md new file mode 100644 index 00000000..a174a148 --- /dev/null +++ b/1-js/2-first-steps/19-recursion/3-fibonacci-numbers/task.md @@ -0,0 +1,23 @@ +# Числа Фибоначчи + +[importance 5] + +Последовательность [чисел Фибоначчи](http://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%A4%D0%B8%D0%B1%D0%BE%D0%BD%D0%B0%D1%87%D1%87%D0%B8) имеет формулу Fn = Fn-1 + Fn-2. То есть, следующее число получается как сумма двух предыдущих. + +Первые два числа равны `1`, затем `2(1+1)`, затем `3(1+2)`, `5(2+3)` и так далее: `1, 1, 2, 3, 5, 8, 13, 21...`. + +Числа Фибоначчи тесно связаны с [золотым сечением](http://ru.wikipedia.org/wiki/%D0%97%D0%BE%D0%BB%D0%BE%D1%82%D0%BE%D0%B5_%D1%81%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5) и множеством природных явлений вокруг нас. + +Напишите функцию `fib(n)`, которая возвращает `n-е` число Фибоначчи. Пример работы: + +```js +function fib(n) { /* ваш код */ } + +alert( fib(3) ); // 2 +alert( fib(7) ); // 13 +alert( fib(77)); // 5527939700884757 +``` + +**Все запуски функций из примера выше должны срабатывать быстро.** + + \ No newline at end of file diff --git a/1-js/2-first-steps/19-recursion/article.md b/1-js/2-first-steps/19-recursion/article.md new file mode 100644 index 00000000..7225e526 --- /dev/null +++ b/1-js/2-first-steps/19-recursion/article.md @@ -0,0 +1,273 @@ +# Рекурсия, стек + +В коде функции могут вызывать другие функции для выполнения подзадач. Частный случай подвызова -- когда функция вызывает сама себя. Это называется *рекурсией*. + +В этой главе мы рассмотрим, как рекурсия устроена изнутри, и как её можно использовать. +[cut] + + +## Реализация pow(x, n) через рекурсию + +Чтобы возвести `x` в натуральную степень `n` -- можно умножить его на себя `n` раз в цикле: + +```js +function pow(x, n) { + var result = x; + for(var i=1; ixn = x * xn-1, т.е. можно вынести один `x` из-под степени. Иначе говоря, значение функции `pow(x,n)` получается из `pow(x, n-1)` умножением на `x`. + +Этот процесс можно продолжить. Например, вычислим `pow(2, 4)`: + +```js +pow(2, 4) = 2 * pow(2, 3) = 2 * 2 * pow(2, 2) = 2 * 2 * 2 * pow(2, 1) = 2 * 2 * 2 * 2; +``` + +Процесс перехода от `n` к `n-1` останавливается на `n==1`, так как очевидно, что `pow(x,1) == x`. + +Код для такого вычисления: + +```js +//+ run +function pow(x, n) { + if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1) + return x * pow(x, n-1); + } else { + return x; + } +} + +alert( pow(2, 3) ); // 8 +``` + +Говорят, что "функция `pow` *рекурсивно вызывает сама себя*" при `n != 1`. + +Значение, на котором рекурсия заканчивается называют *базисом рекурсии*. В примере выше базисом является `1`. + +Общее количество вложенных вызовов называют *глубиной рекурсии*. В случае со степенью, всего будет `n` вызовов. Максимальная глубина рекурсии ограничена и составляет около `10000`, но это число зависит от конкретного интерпретатора JavaScript и может быть в 10 раз меньше. + +**Рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его -- еще к более простому, и так далее, пока значение не станет очевидно.** + +## Контекст выполнения, стек + +Теперь мы посмотрим, как работают рекурсивные вызовы. Для этого мы рассмотрим, как вообще работают функции, что происходит при вызове. + +**У каждого вызова функции есть свой "контекст выполнения" (execution context).** + +Контекст выполнения -- это служебная информация, которая соответствует текущему запуску функции. Она включает в себя локальные переменные функции. + +Например, для вызова: + +```js +//+ run +function pow(x, n) { + if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1) + return x * pow(x, n-1); + } else { + return x; + } +} + +*!* +alert( pow(2, 3) ); // (*) +*/!* +``` + +При запуске функции `pow` в строке `(*)` будет создан контекст выполнения, который будет хранить переменные `x = 2, n = 3`. Мы схематично обозначим его так: + + + +Далее функция `pow` начинает выполняться. Вычисляется выражение `n != 1` -- оно равно `true`, ведь в текущем контексте `n=3`. Поэтому задействуется первая ветвь `if` : + +```js + if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1) +*!* + return x * pow(x, n-1); +*/!* + } else { + return x; + } +``` + +Чтобы вычислить выражение `x * pow(x, n-1)`, требуется произвести запуск `pow` с новыми аргументами. + +**При любом вложенном вызове JavaScript запоминает место, где он остановился в текущей функции в специальной внутренней структуре данных -- "стеке контекстов".** + +Это как если бы мы куда-то ехали, но очень захотелось поесть. Можно остановиться у кафе, оставить машину, отойти, а потом, через некоторое время, вернуться к ней и продолжить дорогу. + +Так и здесь -- мы запомним, где остановились в этой функции, пойдём выполним вложенный вызов, затем вернёмся и продолжим дорогу. + +**После того, как текущий контекст выполнения сохранён в стеке контекстов, JavaScript приступает к выполнению вложенного вызова.** + +В данном случае вызывается та же `pow`, однако, это абсолютно неважно. Для любых функций процесс одинаков. + +**Создаётся новый контекст выполнения, и управление переходит в подвызов, а когда он завершён -- старый контекст достаётся из стека и выполнение внешней функции возобновляется.** + +## Разбор примера + +Разберём происходящее более подробно, начиная с вызова `(*)`: + +```js +//+ run +function pow(x, n) { + if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1) + return x * pow(x, n-1); + } else { + return x; + } +} + +*!* +alert( pow(2, 3) ); // (*) +*/!* +``` + +
+
`pow(2, 3)`
+
Запускается функция `pow`, с аргументами `x=2`, `n=3`. Эти переменные хранятся в контексте выполнения, схематично изображённом ниже: + +
    +
  • Контекст: { x: 2, n: 3 }
  • +
+Выполнение в этом контексте продолжается, пока не встретит вложенный вызов в строке 3. +
+
`pow(2, 2)`
+
В строке `3` происходит вложенный вызов `pow` с аргументами `x=2`, `n=2`. Для этой функции создаётся новый текущий контекст (выделен красным), а предыдущий сохраняется в "стеке": + +
    +
  • Контекст: { x: 2, n: 3 }
  • +
  • Контекст: { x: 2, n: 2 }
  • +
+ +
+
`pow(2, 1)`
+
Опять вложенный вызов в строке `3`, на этот раз -- с аргументами `x=2`, `n=1`. Создаётся новый текущий контекст, предыдущий добавляется в стек: +
    +
  • Контекст: { x: 2, n: 3 }
  • +
  • Контекст: { x: 2, n: 2 }
  • +
  • Контекст: { x: 2, n: 1 }
  • +
+
+
Выход из `pow(2, 1)`.
+
При выполнении `pow(2, 1)`, в отличие от предыдущих запусков, выражение `n != 1` будет равно `false`, поэтому сработает вторая ветка `if..else`: + +```js +function pow(x, n) { + if (n != 1) { + return x * pow(x, n-1); + } else { +*!* + return x; // первая степень числа равна самому числу +*/!* + } +} +``` + +Здесь вложенных вызовов нет, так что функция заканчивает свою работу, возвращая `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**. + +Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке. + +[smart] +В самом конце, как и в самом начале, выполнение попадает во внешний код, который находится вне любых функций. + +Контекст, который соответствует самому внешнему коду, называют *"глобальный контекст"*. Естественно, он является начальной и конечной точкой любых вложенных подвызовов. +[/smart] + +Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних вызовов в стеке, в данном случае это приводит к тому, что возведение в степень `n` хранит в памяти `n` различных контекстов. + +Реализация степени через цикл гораздо более экономна: + +```js +function pow(x, n) { + var result = x; + for(var i=1; i +.function-execution-context { + margin: 0; + padding: 0; + overflow: auto; +} + +.function-execution-context li { + float: left; + clear: both; + border: 1px solid black; + font-family: "PT Mono", monospace; + padding: 3px 5px; +} + + +.function-execution-context li:last-child { + color: red; +} + +[/head] \ No newline at end of file diff --git a/1-js/2-first-steps/2-structure/article.md b/1-js/2-first-steps/2-structure/article.md new file mode 100644 index 00000000..2dc6d34e --- /dev/null +++ b/1-js/2-first-steps/2-structure/article.md @@ -0,0 +1,113 @@ +# Структура кода + +В этой главе мы рассмотрим общую структуру кода, команды и их разделение. +[cut] +## Команды + +Например, можно вместо одного вызова `alert` сделать два: + +```js +//+ run +alert('Привет'); alert('Мир'); +``` + +Как правило, новая команда занимает отдельную строку -- так код лучше читается: + +```js +//+ run +alert('Привет'); +alert('Мир'); +``` + +Точку с запятой *во многих случаях* можно не ставить, если есть переход на новую строку. + +Так тоже будет работать: + +```js +//+ run +alert('Привет') +alert('Мир') +``` + +В этом случае JavaScript интерпретирует переход на новую строчку как разделитель команд и автоматически вставляет "виртуальную" точку с запятой между ними. + +**Однако, важно то, что "во многих случаях" не означает "всегда"!** + +Например, запустите этот код: + +```js +//+ run +alert(3 + +1 ++ 2); +``` + +Выведет 6. + +То есть, точка с запятой не ставится. Почему? Интуитивно понятно, что здесь дело в "незавершённом выражении", конца которого JavaScript ждёт с первой строки и поэтому не ставит точку с запятой. И здесь это, пожалуй, хорошо и приятно. + +**Но в некоторых важных ситуациях JavaScript "забывает" вставить точку с запятой там, где она нужна.** + +Таких ситуаций не так много, но они всё же есть, и ошибки, которые при этом появляются, достаточно сложно обнаруживать и исправлять. + +**Поэтому рекомендуется точки с запятой ставить. Сейчас это, фактически, стандарт, которому следуют все большие проекты.** + + +## Комментарии + +Со временем программа становится большой и сложной. Появляется необходимость добавить *комментарии*, которые объясняют, что происходит и почему. + +Комментарии могут находиться в любом месте программы и никак не влияют на ее выполнение. Интерпретатор JavaScript попросту игнорирует их. + +*Однострочные комментарии* начинаются с двойного слэша `//`. Текст считается комментарием до конца строки: + +```js +//+ run +// Команда ниже говорит "Привет" +alert('Привет'); + +alert('Мир'); // Второе сообщение выводим отдельно +``` + +*Многострочные комментарии* начинаются слешем-звездочкой "/*" и заканчиваются звездочкой-слэшем "*/", вот так: + +```js +//+ run +/* Пример с двумя сообщениями. +Это - многострочный комментарий. +*/ +alert('Привет'); +alert('Мир'); +``` + +Все содержимое комментария игнорируется. Если поместить код внутрь /* ... */ или после `//` -- он не выполнится. + +```js +//+ run +/* Закомментировали код +alert('Привет'); +*/ +alert('Мир'); +``` + +[smart header="Используйте горячие клавиши!"] +В большинстве редакторов комментарий можно поставить горячей клавишей, обычно это Ctrl + / для однострочных и что-то вроде Ctrl + Shift + / -- для многострочных комментариев (нужно выделить блок и нажать сочетание клавиш). Детали смотрите в руководстве по редактору. +[/smart] + +[warn header="Вложенные комментарии не поддерживаются!"] +В этом коде будет ошибка: + +```js +//+ run +/* + /* вложенный комментарий ?!? */ +*/ +alert('Мир'); +``` + +[/warn] + + +Не бойтесь комментариев. Чем больше кода в проекте -- тем они важнее. Что же касается увеличения размера кода -- это не страшно, т.к. существуют инструменты сжатия JavaScript, которые при публикации кода легко их удалят. + +На следующих занятиях мы поговорим о переменных, блоках и других структурных элементах программы на JavaScript. \ No newline at end of file diff --git a/1-js/2-first-steps/20-function-declaration-expression/article.md b/1-js/2-first-steps/20-function-declaration-expression/article.md new file mode 100644 index 00000000..29c50197 --- /dev/null +++ b/1-js/2-first-steps/20-function-declaration-expression/article.md @@ -0,0 +1,364 @@ +# Функциональные выражения + +В JavaScript функция является значением, таким же как строка или число. + +## Функция -- это значение + +Как и любое значение, объявленную функцию можно вывести, вот так: + +```js +//+ run +function sayHi() { + alert( "Привет" ); +} + +*!* +alert( sayHi ); // выведет код функции +*/!* +``` + +Обратим внимание на то, что в последней строке после `sayHi` нет скобок. То есть, функция не вызывается, а просто выводится на экран. + +**Функцию можно скопировать в другую переменную:** + +```js +//+ run +function sayHi() { // (1) + alert( "Привет" ); +} + +var func = sayHi; // (2) +func(); // Привет // (3) + +sayHi = null; +sayHi(); // ошибка (4) +``` + +
    +
  1. Объявление `(1)` как бы говорит интерпретатору "создай функцию и помести её в переменную `sayHi`
  2. +
  3. В строке `(2)` мы копируем функцию в новую переменную `func`. Ещё раз обратите внимание: после `sayHi` нет скобок. Если бы они были, то вызов `var func = sayHi()` записал бы в `func` *результат* работы `sayHi()` (кстати, чему он равен? правильно, `undefined`, ведь внутри `sayHi` нет `return`).
  4. +
  5. На момент `(3)` функцию можно вызывать и как `sayHi()` и как `func()`
  6. +
  7. ...Однако, в любой момент значение переменной можно поменять. При этом, если оно не функция, то вызов `(4)` выдаст ошибку.
  8. +
+ +Обычные значения, такие как числа или строки, представляют собой *данные*. А функцию можно воспринимать как *действие*. Это действие, как правило, хранится в переменной, но его можно скопировать или переместить из неё. + + +## Объявление Function Expression [#function-expression] + +Функцию можно создать и присвоить переменной в любом месте кода. + +Для этого используется объявление "Function Expression" (функциональное выражение), которое выглядит так: + +```js +//+ run +var f = function(параметры) { + // тело функции +}; +``` + +Например: + +```js +//+ run +var sayHi = function(person) { + alert("Привет, " + person); +}; + +sayHi('Вася'); +``` + +## Сравнение с Function Declaration + +"Классическое" объявление функции, о котором мы говорили до этого, вида `function имя(параметры) {...}`, называется в спецификации языка "Function Declaration". + +
    +
  • **Function Declaration** -- функция, объявленная в основном потоке кода.
  • +
  • **Function Expression** -- объявление функции в контексте какого-либо выражения, например присваивания.
  • +
+ +Несмотря на немного разный вид, по сути две эти записи делают одно и то же: + +```js +// Function Declaration +function sum(a, b) { + return a + b; +} + +// Function Expression +var sum = function(a, b) { + return a + b; +} +``` + +Оба этих объявления говорят интерпретатору: "объяви переменную `sum`, создай функцию с указанными параметрами и кодом и сохрани её в `sum`". + +**При этом название переменной, в которую записана функция, обычно называют "именем функции". Говорят: "функция sum". Но при этом имя к функции никак не привязано.** + +Это всего лишь имя переменной, в которой *в данный момент* находится функция. + +Функция может быть в процессе работы скрипта скопирована в другую переменную, а из этой удалена, передана в другое место кода, и так далее, как мы видели выше. + +**Основное отличие между ними: функции, объявленные как Function Declaration, создаются интерпретатором *до выполнения кода*.** + +Поэтому их можно вызвать *до* объявления, например: + +```js +//+ run +*!* +sayHi("Вася"); // Привет, Вася +*/!* + +function sayHi(name) { + alert("Привет, " + name); +} +``` + +А если бы это было объявление Function Expression, то такой вызов бы не сработал: + +```js +//+ run +*!* +sayHi("Вася"); // ошибка! +*/!* + +var sayHi = function(name) { + alert("Привет, " + name); +} +``` + +Это из-за того, что JavaScript перед запуском кода ищет в нём Function Declaration (они начинаются в основном потоке с `function`) и обрабатывает их. + +А Function Expression создаются при выполнении выражения, в котором созданы, в данном случае -- функция будет создана при операции присваивания `sayHi = ...`. + +**Как правило, возможность Function Declaration вызвать функцию до объявления -- это удобно, так как даёт больше свободы в том, как организовать свой код.** + + +### Условное объявление функции [#bad-conditional-declaration] + +В некоторых случаях "дополнительное удобство" Function Declaration может сослужить плохую службу. + +Например, попробуем, в зависимости от условия, объявить функцию `sayHi` по-разному: + +```js +//+ run +var age = 20; + +if (age >= 18) { + function sayHi() { alert('Прошу вас!'); } +} else { + function sayHi() { alert('До 18 нельзя'); } +} + +sayHi(); +``` + +[smart header="Зачем условное объявление?"] +Конечно, можно произвести проверку условия внутри функцию. + +Но вынос проверки вовне даёт очевидный выигрыш в производительности в том случае, когда проверку достаточно произвести только один раз, и её результат никогда не изменится. + +Например, мы можем проверить, поддерживает ли браузер определённые современные возможности, и если да -- функция будет использовать их, а если нет -- реализовать её другим способом. При этом проверка будет осуществляться один раз, на этапе объявления функции, а не при каждом запуске функции. +[/smart] + +При запуске примера выше в любом браузере, кроме Firefox, мы увидим, что условное объявление не работает. Срабатывает `"До 18 нельзя"`, несмотря на то, что `age = 20`. + +В чём дело? Чтобы ответить на этот вопрос -- вспомним, как работают функции. + +
    +
  1. Function Declaration обрабатываются перед запуском кода. Интерпретатор сканирует код и создает из таких объявлений функции. При этом второе объявление перезапишет первое. +
  2. +
  3. Дальше, во время выполнения, объявления Function Declaration игнорируются (они уже были обработаны). Это как если бы код был таким: + +```js +function sayHi() { alert('Прошу вас!'); } +function sayHi() { alert('До 18 нельзя'); } + +var age = 20; + +if (age >= 18) { + /* объявление было обработано ранее */ +} else { + /* объявление было обработано ранее */ +} + +*!* +sayHi(); // "До 18 нельзя", сработает всегда вторая функция +*/!* +``` + +...То есть, от `if` здесь уже ничего не зависит. По-разному объявить функцию, в зависимости от условия, не получилось. +
  4. +
+ +Такое поведение соответствует современному стандарту. На момент написания этого раздела ему следуют все браузеры, кроме, как ни странно, Firefox. + +**Вывод: для условного объявления функций Function Declaration не годится.** + +А что, если использовать Function Expression? + +```js +//+ run +var age = prompt('Сколько вам лет?'); + +var sayHi; + +if (age >= 18) { + sayHi = function() { alert('Прошу Вас!'); } +} else { + sayHi = function() { alert('До 18 нельзя'); } +} + +sayHi(); +``` + +Или даже так: + +```js +//+ run +var age = prompt('Сколько вам лет?'); + +var sayHi = (age >= 18) ? + function() { alert('Прошу Вас!'); } : + function() { alert('До 18 нельзя'); }; + +sayHi(); +``` + +Оба этих варианта работают правильно, поскольку, в зависимости от условия, создаётся именно та функция, которая нужна. + +### Анонимные функции + +Взглянем ещё на один пример. + +Функция `test(f, yes, no)` получает три функции, вызывает первую и, в зависимости от её результата, вызывает вторую или третью: + +```js +//+ run +*!* +function test(f, yes, no) { + if (f()) yes() + else no(); +} +*/!* + +// вспомогательные функции +function f1() { + return confirm("OK?"); +} + +function f2() { + alert("Вы согласились."); +} + +function f3() { + alert("Вы отменили выполнение."); +} + +// использование +test(f1, f2, f3); +``` + +В этом примере для нас, наверно, нет ничего нового. Подумаешь, объявили функции `f1`, `f2`, `f3`, передали их в качестве параметров другой функции (ведь функция -- обычное значение), вызвали те, которые нужны... + +А вот то же самое, но более коротко: + +```js +//+ run +function test(f, yes, no) { + if (f()) yes() + else no(); +} + +*!* +test( + function() { return confirm("OK?"); }, + function() { alert("Вы согласились."); }, + function() { alert("Вы отменили выполнение."); } +); +*/!* +``` + +Здесь функции объявлены прямо внутри вызова `test(...)`, даже без присвоения им имени. + +**Функциональное выражение, которое не записывается в переменную, называют [анонимной функцией](http://ru.wikipedia.org/wiki/%D0%90%D0%BD%D0%BE%D0%BD%D0%B8%D0%BC%D0%BD%D0%B0%D1%8F_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F).** + +Действительно, зачем нам записывать функцию в переменную, если мы не собираемся вызывать её ещё раз? Можно просто объявить непосредственно там, где функция нужна. + +Такого рода код возникает естественно, он соответствует "духу" JavaScript. + +## new Function + +Существует ещё один способ создания функции, который используется очень редко. + +Он выглядит так: + +```js +//+ run +var sum = new Function('a,b', ' return a+b; '); + +var result = sum(1,2); +alert(result); // 3 +``` + +То есть, функция создаётся вызовом `new Function(params, code)`: +
+
`params`
+
Параметры функции через запятую в виде строки.
+
`code`
+
Код функции в виде строки.
+
+ +Этот способ позволяет конструировать строку с кодом функции динамически, во время выполнения программы. Это, скорее, исключение, чем правило, но также бывает востребовано. Пример использования -- динамическая компиляция шаблонов на JavaScript, мы встретимся с ней позже, при работе с интерфейсами. + +## Итого + +Функции в JavaScript являются значениями. Их можно присваивать, передавать, создавать в любом месте кода. + +
    +
  • Если функция объявлена в *основном потоке кода*, то это Function Declaration.
  • +
  • Если функция создана как *часть выражения*, то это Function Expression.
  • +
+ +Между этими двумя основными способами создания функций есть следующие различия: + + + + + + + + + + + + + + + + + + + + + + +
Function DeclarationFunction Expression
Время созданияДо выполнения первой строчки кода.Когда управление достигает строки с функцией.
Можно вызвать до объявления `Да` (т.к. создаётся заранее)`Нет`
Условное объявление в `if``Не работает``Работает`
+ +Иногда в коде начинающих разработчиков можно увидеть много Function Expression. Почему-то, видимо, не очень понимая происходящее, функции решают создавать как `var func = function()`, но в большинстве случаев обычное объявление функции -- лучше. + +**Если нет явной причины использовать Function Expression -- предпочитайте Function Declaration.** + +Сравните по читаемости: + +```js +// Function Expression +var f = function() { ... } + +// Function Declaration +function f() { ... } +``` + +Function Declaration короче и лучше читается. Дополнительный бонус -- такие функции можно вызывать до того, как они объявлены. + +Используйте Function Expression только там, где это действительно нужно и удобно. diff --git a/1-js/2-first-steps/21-named-function-expression/1-nfe-check/solution.md b/1-js/2-first-steps/21-named-function-expression/1-nfe-check/solution.md new file mode 100644 index 00000000..f8cb2fb5 --- /dev/null +++ b/1-js/2-first-steps/21-named-function-expression/1-nfe-check/solution.md @@ -0,0 +1,21 @@ +**Первый код выведет `function ...`, второй -- ошибку во всех браузерах, кроме IE8-.** + +```js +//+ run untrusted +// обычное объявление функции (Function Declaration) +function g() { return 1; }; + +alert(g); // функция +``` + +Во втором коде скобки есть, значит функция внутри является не `Function Declaration`, а частью выражения, то есть `Named Function Expression`. Его имя видно только внутри, снаружи переменная `g` не определена. + +```js +//+ run untrusted +// Named Function Expression! +(function g() { return 1; }); + +alert(g); // Ошибка! +``` + +Все браузеры, кроме IE8-, поддерживают это ограничение видимости и выведут ошибку, `"undefined variable"`. \ No newline at end of file diff --git a/1-js/2-first-steps/21-named-function-expression/1-nfe-check/task.md b/1-js/2-first-steps/21-named-function-expression/1-nfe-check/task.md new file mode 100644 index 00000000..1e7deaa8 --- /dev/null +++ b/1-js/2-first-steps/21-named-function-expression/1-nfe-check/task.md @@ -0,0 +1,20 @@ +# Проверка на NFE + +[importance 5] + +Каков будет результат выполнения кода? + +```js +function g() { return 1; } + +alert(g); +``` + +А такого? Будет ли разница, если да -- почему? + +```js +(function g() { return 1; }); + +alert(g); +``` + diff --git a/1-js/2-first-steps/21-named-function-expression/article.md b/1-js/2-first-steps/21-named-function-expression/article.md new file mode 100644 index 00000000..241c5bef --- /dev/null +++ b/1-js/2-first-steps/21-named-function-expression/article.md @@ -0,0 +1,136 @@ +# Именованные функциональные выражения + +Обычно то, что называют "именем функции", на самом деле -- всего лишь имя переменной, в которую присвоена функция. К самой функции это "имя" никак не привязано. + +Однако, есть в JavaScript способ указать имя, действительно привязанное к функции. Оно называется "Named Function Expression" (сокращённо NFE) или, по-русски, *"именованное функциональное выражение"*. +[cut] + + +## Named Function Expression [#functions-nfe] + +Простейший пример NFE выглядит так: + +```js +var f = function *!*sayHi*/!*(...) { /* тело функции */ }; +``` + +Проще говоря, NFE -- это `Function Expression` с дополнительным именем (в примере выше `sayHi`). + +Что же это за имя, которое идёт в дополнение к переменной `f`, и зачем оно? + +**Имя функционального выражения (`sayHi`) имеет особый смысл. Оно доступно только изнутри самой функции.** + +**Это ограничение видимости входит в стандарт JavaScript и поддерживается всеми браузерами, кроме IE8-.** + +Например: + +```js +//+ run +var f = function sayHi(name) { + alert(sayHi); // изнутри функции - видно (выведет код функции) +}; + +alert(sayHi); // снаружи - не видно (ошибка: undefined variable 'sayHi') +``` + +**Кроме того, имя NFE нельзя перезаписать:** + +```js +//+ run +var test = function sayHi(name) { +*!* + sayHi = "тест"; // перезапись +*/!* + alert(sayHi); // function... (перезапись не удалась) +}; + +test(); +``` + +В режиме `use strict` код выше выдал бы ошибку. + +**Как правило, имя NFE используется для единственной цели -- позволить изнутри функции вызвать саму себя.** + +[smart header="Устаревшее специальное значение `arguments.callee`"] +Если вы работали с JavaScript, то, возможно, знаете, что для этой цели также служило специальное значение `arguments.callee`. + +Если это название вам ни о чём не говорит -- всё в порядке, читайте дальше, мы обязательно обсудим его [в отдельной главе](#arguments-callee). + +Если же вы в курсе, то стоит иметь в виду, что оно официально исключено из современного стандарта. А NFE -- это наше настоящее. +[/smart] + +## Пример использования + +NFE используется в первую очередь в тех ситуациях, когда функцию нужно передавать в другое место кода или перемещать из одной переменной в другую. + +**Внутреннее имя позволяет функции надёжно обращаться к самой себе, где бы она ни находилась.** + +Вспомним, к примеру, функцию-факториал из задачи [](/task/factorial): + +```js +//+ run +function f(n) { + return n ? n*f(n-1) : 1; +}; + +alert( f(5) ); // 120 +``` + +Попробуем перенести её в другую переменную `g`: + +```js +//+ run +function f(n) { + return n ? n*f(n-1) : 1; +}; + +*!* +var g = f; +f = null; +*/!* + +alert( g(5) ); // запуск функции с новым именем - ошибка при выполнении! +``` + +Ошибка возникла потому что функция из своего кода обращается к своему старому имени `f`. А этой функции уже нет, `f = null`. + +Для того, чтобы функция всегда надёжно работала, объявим её как Named Function Expression: + +```js +//+ run +var f = function *!*factorial*/!*(n) { + return n ? n**!*factorial*/!*(n-1) : 1; +}; + +var g = f; // скопировали ссылку на функцию-факториал в g +f = null; + +*!* +alert( g(5) ); // 120, работает! +*/!* +``` + +[warn header="В браузере IE8- создаются две функции"] + +Как мы говорили выше, в браузере IE до 9 версии имя NFE видно везде, что является ошибкой с точки зрения стандарта. + +...Но на самом деле ситуация еще забавнее. Старый IE создаёт в таких случаях целых две функции: одна записывается в переменную `f`, а вторая -- в переменную `factorial`. + +Например: + +```js +//+ run +var f = function factorial(n) { /*...*/ }; + +// в IE8- false +// в остальных браузерах ошибка, т.к. имя factorial не видно +alert(f === factorial); +``` + +Все остальные браузеры полностью поддерживают именованные функциональные выражения. +[/warn] + +## Итого + +Если функция задана как Function Expression, её можно дать имя. Оно будет доступно только внутри функции (кроме IE8-) и предназначено для надёжного рекурсивного вызова функции, даже если она записана в другую переменную. + diff --git a/1-js/2-first-steps/22-javascript-specials/article.md b/1-js/2-first-steps/22-javascript-specials/article.md new file mode 100644 index 00000000..859e24da --- /dev/null +++ b/1-js/2-first-steps/22-javascript-specials/article.md @@ -0,0 +1,418 @@ +# Всё вместе: особенности JavaScript + +В этой главе приводятся основные особенности JavaScript, на уровне базовых конструкций, типов, синтаксиса. + +Она будет особенно полезна, если ранее вы программировали на другом языке, ну или как повторение важных моментов раздела. + +Все очень компактно, со ссылками на развёрнутые описания. + +[cut] + +## Структура кода + +Операторы разделяются точкой с запятой: + +```js +//+ run +alert('Привет'); alert('Мир'); +``` + +Как правило, перевод строки тоже подразумевает точку с запятой. Так тоже будет работать: + +```js +//+ run +alert('Привет') +alert('Мир') +``` + +..Однако, иногда JavaScript не вставляет точку с запятой. Например: + +```js +//+ run +var a = 2 ++3 + +alert(a); // 5 +``` + +Бывают случаи, когда это ведёт к ошибкам, которые достаточно трудно найти и исправить. Правила, когда точка с запятой ставится, а когда нет -- конечно, есть в спецификации языка, но запомнить их поначалу сложно, так как они неочевидны, придуманы "не для людей". + +**Поэтому в JavaScript рекомендуется точки с запятой ставить. Сейчас это, фактически, общепринятый стандарт.** + +Поддерживаются однострочные комментарии `// ...` и многострочные `/* ... */`: + +Подробнее: [](/structure). + +## Переменные и типы + +
    +
  • Объявляются директивой `var`. Могут хранить любое значение: + +```js +var x = 5; +x = "Петя"; +``` + +
  • +
  • Есть 5 "примитивных" типов и объекты: + +```js +x = 1; // число +x = "Тест"; // строка, кавычки могут быть одинарные или двойные +x = true; // булево значение true/false +x = null; // спец. значение (само себе тип) +x = undefined; // спец. значение (само себе тип) +``` + +Также есть специальные числовые значения `Infinity` (бесконечность) и `NaN`. + +Значение `NaN` обозначает ошибку и является результатом числовой операции, если она некорректна. +
  • +
  • **Значение `null` не является "ссылкой на нулевой адрес/объект" или чем-то подобным. Это просто специальное значение.** + +Оно присваивается, если мы хотим указать, что значение переменной неизвестно. + +Например: + +```js +var age = null; // возраст неизвестен +``` + +
  • +
  • **Значение `undefined` означает "переменная не присвоена".** + +Например: + +```js +var x; +alert( x ); // undefined +``` + +Можно присвоить его и явным образом: `x = undefined`, но так делать не рекомендуется. + +Про объекты мы поговорим в главе [](/object), они в JavaScript сильно отличаются от большинства других языков. +
  • +
  • В имени переменной могут быть использованы любые буквы или цифры, но цифра не может быть первой. Символы доллар `$` и подчёркивание `_` допускаются наравне с буквами.
  • +
+ +Подробнее: [](/variables), [](/types-intro). + + +## Методы и свойства + +Все значения в JavaScript, за исключением `null` и `undefined`, содержат набор вспомогательных функций и значений, доступных "через точку". + +Такие функции называют "методами", а значения -- "свойствами". + +Например: + +```js +//+ run +alert( "Привет, мир!".length ); // 12 +``` + +Еще у строк есть *метод* `toUpperCase()`, который возвращает строку в верхнем регистре: + +```js +//+ run +var hello = "Привет, мир!"; + +*!* +alert( hello.toUpperCase() ); // "ПРИВЕТ, МИР!" +*/!* +``` + +Подробнее: [](/properties-and-methods). + + +## Строгий режим + +Для того, чтобы интерпретатор работал в режиме максимального соответствия современному стандарту, нужно начинать скрипт директивой `'use strict';` + +```js +'use strict'; + +... +``` + +Эта директива может также указываться в начале функций. При этом функция будет выполняться в режиме соответствия, а на внешний код такая директива не повлияет. + +Одно из важных изменений в современном стандарте -- все переменные нужно объявлять через `var`. Есть и другие, которые мы изучим позже, вместе с соответствующими возможностями языка. + + + +## Взаимодействие с посетителем + +Простейшие функции для взаимодействия с посетителем в браузере: + +
+
["prompt(вопрос[, по_умолчанию])"](https://developer.mozilla.org/en/DOM/window.prompt)
+
Задать `вопрос` и возвратить введённую строку, либо `null`, если посетитель нажал "Отмена".
+
["confirm(вопрос)"](https://developer.mozilla.org/en/DOM/window.confirm)
+
Задать `вопрос` и предложить кнопки "Ок", "Отмена". Возвращает, соответственно, `true/false`.
+
["alert(сообщение)"](https://developer.mozilla.org/en/DOM/window.alert)
+
Вывести сообщение на экран.
+
+ +Все эти функции являются *модальными*, т.е. не позволяют посетителю взаимодействовать со страницей до ответа. + +Например: + +```js +//+ run +var userName = prompt("Введите имя?", "Василий"); +var isTeaWanted = confirm("Вы хотите чаю?"); + +alert( "Посетитель: " + userName); +alert( "Чай: " + isTeaWanted); +``` + +Подробнее: [](/uibasic). + +## Особенности операторов + +
    +
  • **Для сложения строк используется оператор `+`.** + +Если хоть один аргумент -- строка, то другой тоже приводится к строке: + +```js +//+ run +alert( 1 + 2 ); // 3, число +alert( '1' + 2 ); // '12', строка +alert( 1 + '2' ); // '12', строка +``` + +
  • +
  • **Сравнение `===` проверяет точное равенство, включая одинаковый тип.** Это самый очевидный и надёжный способ сравнения. + +**Остальные сравнения `== < <= > >=` осуществляют числовое приведение типа:** + +```js +//+ run +alert( 0 == false ); // true +alert( true > 0 ); // true +``` + +Исключение -- сравнение двух строк (см. далее). + +**Исключение: значения `null` и `undefined` ведут себя в сравнениях не как ноль.** +
      +
    • Они равны `null == undefined` друг другу и не равны ничему ещё. В частности, не равны нулю.
    • +
    • В других сравнениях (кроме `===`) значение `null` преобразуется к нулю, а `undefined` -- становится `NaN` ("ошибка").
    • +
    + +Такое поведение может привести к неочевидным результатам, поэтому лучше всего использовать для сравнения с ними `===`. Оператор `==` тоже можно, если не хотите отличать `null` от `undefined`. + +Например, забавное следствие этих правил для `null`: + +```js +//+ run +alert( null > 0 ); // false, т.к. null преобразовано к 0 +alert( null >= 0 ); // true, т.к. null преобразовано к 0 +alert( null == 0 ); // false, в стандарте явно указано, что null равен лишь undefined +``` + +С точки зрения здравого смысла такое невозможно. Значение `null` не равно нулю и не больше, но при этом `null >= 0` возвращает `true`! +
  • + + + +
  • **Сравнение строк -- лексикографическое, символы сравниваются по своим unicode-кодам.** + +Поэтому получается, что строчные буквы всегда больше, чем прописные: + +```js +//+ run +alert('а' > 'Я'); // true +``` + +
  • +
+ +Подробнее: [](/operators), [](/comparison). + +## Логические операторы + +В JavaScript есть логические операторы: И (обозначается `&&`), ИЛИ (обозначается `||`) и НЕ (обозначается `!`). Они интерпретируют любое значение как логическое. + +Не стоит путать их с [побитовыми операторами](/bitwise-operators) И, ИЛИ, НЕ, которые тоже есть в JavaScript и работают с числами на уровне битов. + +Как и в большинстве других языков, в логических операторах используется "короткий цикл" вычислений. Например, вычисление выражения `1 && 0 && 2` остановится после первого И `&&`, т.к. понятно что результат будет ложным (ноль интерпретируется как `false`). + +**Результатом логического оператора служит последнее значение в коротком цикле вычислений.** + +Можно сказать и по-другому: значения хоть и интерпретируются как логические, но то, которое в итоге определяет результат, возвращается без преобразования. + +Например: + +```js +//+ run +alert( 0 && 1 ); // 0 +alert( 1 && 2 && 3 ); // 3 +alert( null || 1 || 2 ); // 1 +``` + +Подробнее: [](/logical-ops). + +## Циклы + +
    +
  • Поддерживаются три вида циклов: + +```js +// 1 +while (условие) { + ... +} + +// 2 +do { + ... +} while(условие); + +// 3 +for (var i = 0; i < 10; i++) { + ... +} +``` + +
  • +
  • Переменную можно объявлять прямо в цикле, но видна она будет и за его пределами.
  • +
  • Поддерживаются директивы `break/continue` для выхода из цикла/перехода на следующую итерацию. + +Для выхода одновременно из нескольких уровней цикла можно задать метку. + +Синтаксис: "`имя_метки:`", ставится она только перед циклами и блоками, например: + +```js +*!*outer:*/!* +for(;;) { + ... + for(;;) { + ... + *!*break outer;*/!* + } +} +``` + +Переход на метку возможен только изнутри цикла, и только на внешний блок по отношению к данному циклу. В произвольное место программы перейти нельзя. +
  • +
+ +Подробнее: [](/break-continue). + +## Конструкция switch + +При сравнениях в конструкции `switch` используется оператор `===`. + +Например: + +```js +//+ run +var age = prompt('Ваш возраст', 18); + +switch (age) { + case 18: + alert('Никогда не сработает'); // результат prompt - строка, а не число + + case "18": // вот так - сработает! + alert('Вам 18 лет!'); + break; + + default: + alert('Любое значение, не совпавшее с case'); +} +``` + +Подробнее: [](/switch). + +## Функции + +Синтаксис функций в JavaScript: + +```js +//+ run +// function имя(список параметров) { тело } +function sum(a, b) { + var result = a + b; + + return result; +} + +// использование: +alert( sum(1, 2) ); // 3 +``` + +
    +
  • `sum` -- имя функции, ограничения на имя функции -- те же, что и на имя переменной.
  • +
  • Переменные, объявленные через `var` внутри функции, видны везде внутри этой функции, блоки `if`, `for` и т.п. на видимость не влияют.
  • +
  • Параметры передаются "по значению", т.е. копируются в локальные переменные `a`, `b`, за исключением объектов, которые передаются "по ссылке", их мы подробно обсудим в главе [](/object). +
  • +
  • Функция без `return` считается возвращающей `undefined`. Вызов `return` без значения также возвращает `undefined`: + +```js +//+ run +function f() { } +alert( f() ); // undefined +``` + +
  • +
+ +Подробнее: [](/function-basics). + +## Function Declaration и Expression + +Функция в JavaScript является обычным значением. + +Её можно создать в любом месте кода и присвоить в переменную, вот так: + +```js +//+ run +var sum = function(a, b) { + var result = a + b; + + return result; +} + +alert( sum(1, 2) ); // 3 +``` + +Такой синтаксис, при котором функция объявляется в контексте выражения (в данном случае, выражения присваивания), называется Function Expression, а обычный синтаксис, при котором функция объявляется в основном потоке кода -- Function Declaration. + +Функции, объявленные через Function Declaration, отличаются от Function Expression тем, что интерпретатор создаёт их при входе в область видимости (в начале выполнения скрипта), так что они работают до объявления. + +Обычно это удобно, но может быть проблемой, если нужно объявить функцию в зависимости от условия. В этом случае, а также в других ситуациях, когда хочется создать функцию "здесь и сейчас", используют Function Expression. + +Детали: [](/function-declaration-expression). + +## Named Function Expression + +Если объявление функции является частью какого-либо выражения, например `var = function...` или любого другого, то это Function Expression. + +В этом случае имя, которое можно (но не обязательно) указать после `function`, будет видно только внутри этой функции и позволяет обратиться к функции изнутри себя. Обычно оно используется для рекурсивных вызовов. + +Например, создадим функцию для вычисления факториала как Function Expression и дадим ей имя `me`: + +```js +//+ run +var factorial = function me(n) { + return (n == 1) ? n : n * me(n-1); +} + +alert( factorial(5) ); // 120 +*!* +alert( me ); // ошибка, нет такой переменной +*/!* +``` + +Ограничение видимости для имени не работает в IE8-, но вызов с его помощью работает во всех браузерах. + +Более развёрнуто: [](/named-function-expression). + +## Итого + +В этой главе мы повторили основные особенности JavaScript, знание которых необходимо для обхода большинства "граблей", да и просто для написания хорошего кода. + +Это, конечно, лишь основы. Дальше вы узнаете много других особенностей и приёмов программирования на этом языке. diff --git a/1-js/2-first-steps/3-variables/1-hello-variables/solution.md b/1-js/2-first-steps/3-variables/1-hello-variables/solution.md new file mode 100644 index 00000000..1eab8bc7 --- /dev/null +++ b/1-js/2-first-steps/3-variables/1-hello-variables/solution.md @@ -0,0 +1,13 @@ +Каждая строчка решения соответствует одному шагу задачи: + +```js +//+ run +var admin, name; // две переменных через запятую + +name = "Василий"; + +admin = name; + +alert(admin); // "Василий" +``` + diff --git a/1-js/2-first-steps/3-variables/1-hello-variables/task.md b/1-js/2-first-steps/3-variables/1-hello-variables/task.md new file mode 100644 index 00000000..42438a4b --- /dev/null +++ b/1-js/2-first-steps/3-variables/1-hello-variables/task.md @@ -0,0 +1,10 @@ +# Работа с переменными + +[importance 2] + +
    +
  1. Объявите две переменные: `admin` и `name`.
  2. +
  3. Запишите в `name` строку `"Василий"`.
  4. +
  5. Скопируйте значение из `name` в `admin`.
  6. +
  7. Выведите `admin` (должно вывести "Василий").
  8. +
\ No newline at end of file diff --git a/1-js/2-first-steps/3-variables/article.md b/1-js/2-first-steps/3-variables/article.md new file mode 100644 index 00000000..6dcad365 --- /dev/null +++ b/1-js/2-first-steps/3-variables/article.md @@ -0,0 +1,252 @@ +# Переменные + +В зависимости от того, для чего вы делаете скрипт, понадобится работать с информацией. + +Если это электронный магазин - то это товары, корзина. Если чат - посетители, сообщения и так далее. + +Чтобы хранить информацию, используются *переменные*. +[cut] +## Переменная + +*Переменная* состоит из имени и выделенной области памяти, которая ему соответствует.. + +Для *объявления* или, другими словами, *создания переменной* используется ключевое слово `var`: + +```js +var message; +``` + +После объявления, можно записать в переменную данные: + +```js +var message; +message = 'Привет'; // сохраним в переменной строку +``` + +Эти данные будут сохранены в соответствующей области памяти и в дальнейшем доступны при обращении по имени: + +```js +//+ run +var message; +message = 'Привет'; + +alert(message); // выведет содержимое переменной +``` + +Для краткости можно совместить объявление переменной и запись данных: + +```js +var message = 'Привет'; +``` + +Можно даже объявить несколько переменных сразу: + +```js +var user = 'Вася', age = 25, message = 'Привет'; +``` + +### Аналогия из жизни + +Проще всего понять переменную, если представить ее как "коробку" для данных, с уникальным именем. + +Например, переменная `message` - это коробка, в которой хранится значение `"Привет"`: + + + +В коробку можно положить любое значение, а позже - поменять его. Значение в переменной можно изменять сколько угодно раз: + +```js +//+ run +var message; + +message = 'Привет'; + +message = 'Мир'; // заменили значение + +alert(message); +``` + + + +При изменении значения старое содержимое переменной удаляется. + +[smart] +Существуют [функциональные](http://ru.wikipedia.org/wiki/%D0%AF%D0%B7%D1%8B%D0%BA_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F) языки программирования, в которых значение переменной менять нельзя. Например, [Scala](http://www.scala-lang.org/) или [Erlang](http://www.erlang.org/). + +В таких языках положил один раз значение в коробку - и оно хранится там вечно, ни удалить ни изменить. А нужно что-то другое сохранить - изволь создать новую коробку (объявить новую переменную), повторное использование невозможно. + +С виду -- не очень удобно, но, как ни странно, и на таких языках вполне можно успешно программировать. Более того, оказывается, что в ряде областей, например в распараллеливании вычислений, они имеют преимущества. Изучение какого-нибудь функционального языка рекомендуется для расширения кругозора. +[/smart] + +### Имена переменных [#variable-naming] + +На имя переменной в JavaScript наложены всего два ограничения. +
    +
  1. Имя может состоять из: букв, цифр, символов `$` и `_`
  2. +
  3. Первый символ не должен быть цифрой.
  4. +
+ +Например: + +```js +var myName; +var test123; +``` + +**Что особенно интересно -- доллар `'$'` и знак подчеркивания `'_'` являются такими же обычными символами, как буквы:** + +```js +//+ run untrusted +var $ = 1; // объявили переменную с именем '$' +var _ = 2; // переменная с именем '_' + +alert($ + _); // 3 +``` + +А такие переменные были бы неправильными: + +```js +var 1a; // начало не может быть цифрой + +var my-name; // дефис '-' не является разрешенным символом +``` + +[smart header="Регистр букв имеет значение"] +Переменные `apple` и `AppLE` - две разные переменные. +[/smart] + +[smart header="Русские буквы допустимы, но не рекомендуются"] + +В названии переменных можно использовать и русские буквы, например: + +```js +//+ run +var имя = "Вася"; +alert(имя); // "Вася" +``` + +Технически, ошибки здесь нет, но на практике сложилась традиция использовать в именах только английские буквы. +[/smart] + +[warn header="Зарезервированные имена"] + +Существует список зарезервированных слов, которые нельзя использовать для переменных, так как они используются самим языком, например: `var, class, return, export` и др. + +Некоторые такие слова, например, `export`, не используются в современном JavaScript, но они зарезервированы на будущее. + +Например, такой пример выдаст синтаксическую ошибку: + +```js +//+ run +var class = 5; // ошибка, слово зарезервировано +alert(class); +``` + +[/warn] + + +### Копирование значений + +Переменные в JavaScript могут хранить не только строки, но и другие данные, например, числа. + +Объявим две переменные, положим в одну -- строку, а в другую -- число: + +```js +var num = 100500; +var message = 'Привет'; +``` + +При этом в любой момент значение можно скопировать из одной переменной в другую: + +```js +var num = 100500; +var message = 'Привет'; + +*!* +message = num; +*/!* +``` + +При копировании значение из `num` перезаписывает текущее в `message`. + +**В "коробке" `message` изменится значение**: + + + +После этого присваивания в обеих коробках `num` и `message` находится одно и то же значение `100500`. + +## Важность директивы var + +В JavaScript вы можете создать переменную и без `var`, достаточно просто присвоить ей значение: + +```js +x = "value"; // переменная создана, если ее не было +``` + +Технически, это не вызовет ошибки, но делать так все-таки не стоит. + +**Всегда определяйте переменные через `var`**. + +Это хороший тон в программировании и помогает избежать ошибок. + +[warn header="Ошибка в IE8- без `var`"] +Если же вы собираетесь поддерживать IE8-, то у меня для вас ещё одна причина всегда использовать `var`. + +Следущий документ в IE8- ничего не выведет, будет ошибка: + +```html +
+ +``` + +Это потому, что переменная `test` не объявлена через `var` и совпадает с `id` элемента `
`. Даже не спрашивайте почему -- это ошибка в браузере IE до версии 9. + +Самое "забавное" -- то, что такая ошибка присвоения значений будет только в IE8- и только если на странице присутствует элемент с совпадающим с именем `id`. + +Такие ошибки особенно "весело" исправлять и отлаживать. + +Вывод простой -- всегда объявляем переменные через `var`, и сюрпризов не будет. Даже в старых IE. +[/warn] + +## Константы + +*Константа* -- это переменная, которая никогда не меняется. Как правило, их называют большими буквами, через подчёркивание. Например: + +```js +//+ run +var COLOR_RED = "#F00"; +var COLOR_GREEN = "#0F0"; +var COLOR_BLUE = "#00F"; +var COLOR_ORANGE = "#FF7F00"; + +var color = COLOR_ORANGE; +alert(color); // #FF7F00 +``` + +Технически, константа является обычной переменной, то есть её *можно* изменить. Но мы *договариваемся* этого не делать. + +Зачем нужны константы? Почему бы просто не писать `var color = "#FF7F00"`? + +
    +
  1. Во-первых, константа `COLOR_ORANGE` -- это понятное имя. По присвоению `var color="#FF7F00"` непонятно, что цвет -- оранжевый. Иными словами, константа `COLOR_ORANGE` является "понятным псевдонимом" для значения `#FF7F00`.
  2. +
  3. Во-вторых, опечатка в строке, особенно такой сложной как `#FF7F00`, может быть не замечена, а в имени константы её допустить куда сложнее.
  4. +
+ +**Константы используют вместо строк и цифр, чтобы сделать программу понятнее и избежать ошибок.** + +## Итого + +
    +
  • В JavaScript можно объявлять переменные для хранения данных. Это делается при помощи `var`.
  • +
  • Технически, можно просто записать значение и без объявления переменной, однако по ряду причин это не рекомендуется.
  • +
  • Вместе с объявлением можно сразу присвоить значение: `var x = 10`.
  • +
  • Переменные, которые названы `БОЛЬШИМИ_БУКВАМИ`, являются константами, то есть никогда не меняются. Как правило, они используются для удобства, чтобы было меньше ошибок.
  • +
+ + + diff --git a/1-js/2-first-steps/3-variables/box-message-hello.png b/1-js/2-first-steps/3-variables/box-message-hello.png new file mode 100755 index 00000000..1a0a7efa Binary files /dev/null and b/1-js/2-first-steps/3-variables/box-message-hello.png differ diff --git a/1-js/2-first-steps/3-variables/box-trans1.png b/1-js/2-first-steps/3-variables/box-trans1.png new file mode 100755 index 00000000..84335a33 Binary files /dev/null and b/1-js/2-first-steps/3-variables/box-trans1.png differ diff --git a/1-js/2-first-steps/3-variables/box-trans2.png b/1-js/2-first-steps/3-variables/box-trans2.png new file mode 100755 index 00000000..48dd70ac Binary files /dev/null and b/1-js/2-first-steps/3-variables/box-trans2.png differ diff --git a/1-js/2-first-steps/4-variable-names/1-declare-variables/solution.md b/1-js/2-first-steps/4-variable-names/1-declare-variables/solution.md new file mode 100644 index 00000000..692a5bc7 --- /dev/null +++ b/1-js/2-first-steps/4-variable-names/1-declare-variables/solution.md @@ -0,0 +1,13 @@ +Каждая строчка решения соответствует одному шагу задачи: + +```js +//+ run +var ourPlanetName = "Земля"; // буквально "название нашей планеты" + +var userName = "Петя"; // "имя посетителя" +``` + +Названия переменных можно бы сократить, например, до `planet` и `name`, но тогда станет менее понятно, о чем речь. + +Насколько допустимы такие сокращения -- зависит от скрипта, его размера и сложности, от того, есть ли другие планеты и пользователи. В общем, лучше не жалеть букв и называть переменные так, чтобы по имени можно было легко понять, что в ней находится, и нельзя было перепутать переменные. + \ No newline at end of file diff --git a/1-js/2-first-steps/4-variable-names/1-declare-variables/task.md b/1-js/2-first-steps/4-variable-names/1-declare-variables/task.md new file mode 100644 index 00000000..1f0ef92f --- /dev/null +++ b/1-js/2-first-steps/4-variable-names/1-declare-variables/task.md @@ -0,0 +1,8 @@ +# Объявление переменных + +[importance 3] + +
    +
  1. Создайте переменную для названия нашей планеты и присвойте ей значение `"Земля"`. *Правильное* имя выберите сами.
  2. +
  3. Создайте переменную для имени посетителя со значением `"Петя"`. Имя также на ваш вкус.
  4. +
\ No newline at end of file diff --git a/1-js/2-first-steps/4-variable-names/article.md b/1-js/2-first-steps/4-variable-names/article.md new file mode 100644 index 00000000..db6a7899 --- /dev/null +++ b/1-js/2-first-steps/4-variable-names/article.md @@ -0,0 +1,118 @@ +# Правильный выбор имени переменной + +Правильный выбор имени переменной -- одна из самых важных и сложных вещей в программировании, которая отличает начинающего от гуру. + +[cut] + +Дело в том, что большинство времени мы тратим не на изначальное написание кода, а на его развитие. + +Возможно, эти слова не очевидны, если вы пока что ничего большого не писали или пишете код "только для чтения" (написал 5 строк, отдал заказчику и забыл). Но чем более серьёзные проекты вы будете делать, тем более актуальны они будут для вас. + +Что такое это "развитие"? Это когда я вчера написал код, а сегодня (или спустя неделю) прихожу и хочу его поменять. Например, вывести сообщение не так, а эдак... Обработать товары по-другому, добавить функционал.. А где у меня там сообщение хранится? А где товар?... + +**Гораздо проще найти нужные данные, если они правильно помечены, то есть когда переменная названа *правильно*.** + +
    +
  • **Правило 1.** + +**Никакого транслита. Только английский.** + +Неприемлемы: + +```js +var moiTovari; +var cena; +var ssilka; +``` + +Подойдут: + +```js +var myGoods; +var price; +var link; +``` + +Чем плох транслит? + +Во-первых, среди разработчиков всего мира принято использовать английский язык для имён переменных. И если ваш код потом попадёт к кому-то другому, например вы будете в команде больше чем из одного человека, то велик шанс, что транслит ему не понравится. + +Во-вторых, русский транслит хуже читается и длиннее, чем названия на английском. + +В-третьих, в проектах вы наверняка будете применять библиотеки, написанные другими людьми. Многое уже готово, в распоряжении современного разработчика есть масса инструментов, все они используют названия переменных и функций на английском языке, и вы, конечно, будете их использовать. А от кода, где транслит перемешан с английским -- могут волосы смогут встать дыбом, и не только на голове. + +Если вы вдруг не знаете английский -- самое время выучить. +
  • +
  • **Правило 2.** + +**Использовать короткие имена только для переменных "местного значения".** + +Называть переменные именами, не несущими смысловой нагрузки, например `a`, `e`, `p`, `mg` -- можно только в том случае, если они используются в небольшом фрагменте кода и их применение очевидно. + +Вообще же, название переменной должно быть понятным. Иногда для этого нужно использовать несколько слов. +
  • +
  • **Правило 3.** + +**Переменные из нескольких слов пишутся `вместеВотТак`.** + +Например: + +```js +var borderLeftWidth; +``` + +Этот способ записи называется "верблюжьей нотацией" или, по-английски, "camelCase". + +Существует альтернативный стандарт, когда несколько слов пишутся через знак подчеркивания `'_'`: + +```js +var border_left_width; +``` + +Преимущественно в JavaScript используется вариант `borderLeftWidth`, в частности во встроенных языковых и браузерных функциях. Поэтому целесообразно остановиться на нём. + +Ещё одна причина выбрать "верблюжью нотацию" -- запись в ней немного короче, чем c подчеркиванием, т.к. не нужно вставлять `'_'`. +
  • +
  • **Правило последнее, главное.** + +**Имя переменной должно максимально чётко соответствовать хранимым в ней данным.** + +Придумывание таких имен -- одновременно коротких и точных, при которых всегда понятно, что где лежит, приходит с опытом, но только если сознательно стремиться к этому. +
  • +
+ +Позвольте поделиться одним небольшим секретом, который позволит улучшить ваши названия переменных и сэкономит вам время. + +Бывает так, что вы написали код, через некоторое время к нему возвращаетесь, и вам надо что-то поправить. Например, изменить какую-то рамку `border`... + +...И вы помните, что переменная, в которой хранится нужное вам значение, называется примерно так: `borderLeftWidth`. Вы ищете ее в коде, не находите, разбираетесь, и обнаруживаете, что на самом деле переменная называлась вот так: `leftBorderWidth`. После чего вносите нужные правки. + +**Если вы искали переменную с одним именем, а нашли -- с другим, то самый лучший ход -- это *переименовать* переменную, чтобы имя было тем, которое вы искали.** + +То есть, у вас в коде `leftBorderWidth`, а вы ее переименовываете на `borderLeftWidth`. + +Зачем? Дело в том, что в следующий раз, когда вы захотите что-то поправить, то вы будете искать по тому же самому имени. Соответственно, это сэкономит вам время. + +Кроме того, поскольку именно это имя переменной пришло вам в голову -- скорее всего, оно больше соответствует хранимым там данным, чем то, которое вы придумали изначально. Исключения бывают, но в любом случае -- такое несовпадение -- это повод задуматься. + +[summary] +Смысл имени переменной -- это "имя на коробке", по которому мы сможем максимально быстро находить нужные нам данные. + +**Не нужно бояться переименовывать переменные, если вы придумали имя получше.** + +Современные редакторы позволяют делать это очень удобно и быстро. Это в конечном счете сэкономит вам время. +[/summary] + + +[warn header="Храните в переменной то, что следует"] +Бывают ленивые программисты, которые, вместо того чтобы объявить новую переменную, используют существующую. + +В результате получается, что такая переменная -- как коробка, в которую кидают то одно, то другое, то третье, при этом не меняя название. Что в ней лежит сейчас? А кто его знает.. Нужно подойти, проверить. + +Сэкономит такой программист время на объявлении переменной -- потеряет в два раза больше на отладке кода. + +**"Лишняя" переменная -- добро, а не зло.** +[/warn] + + + diff --git a/1-js/2-first-steps/5-strict-mode/article.md b/1-js/2-first-steps/5-strict-mode/article.md new file mode 100644 index 00000000..4acc70e8 --- /dev/null +++ b/1-js/2-first-steps/5-strict-mode/article.md @@ -0,0 +1,90 @@ +# Современный стандарт, "use strict" + +Очень долго язык JavaScript развивался без потери совместимости. Новые возможности добавлялись в язык, но старые -- никогда не менялись, чтобы не "сломать" уже существующие HTML/JS-страницы с их использованием. + +Однако, это привело к тому, что любая ошибка в дизайне языка становилась "вмороженной" в него навсегда. + +Так было до появления стандарта EcmaScript 5 (ES5), который одновременно добавил новые возможности и внёс в язык ряд исправлений, которые могут привести к тому, что старый код, который был написан до его появления, перестанет работать. + +Чтобы этого не случилось, решили, что по умолчанию эти опасные изменения будут выключены, и код будет работать по-старому. А для того, чтобы перевести код в режим полного соответствия современному стандарту, нужно указать специальную директиву `use strict`. + +Эта директива не поддерживается IE9-. + +[cut] + +## Директива use strict + +Директива выглядит как строка `"use strict";` или `'use strict';` и ставится в начале скрипта. + +Например: + +```js +"use strict"; + +// этот код будет работать по современному стандарту ES5 +... +``` + +**Важный пример отличия современного стандарта от старого -- присвоение переменной без объявления, оно в [старом стандарте](http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf) было допустимо, а в современном -- нет.** + +Поэтому следующий код выдаст ошибку: + +```js +//+ run +"use strict"; + +*!* +x = 5; // error: x is not defined +*/!* +``` + +Обратим внимание, директиву `use strict` нужно ставить до кода, иначе она не сработает: + +```js +//+ run +var a; + +"use strict"; // слишком поздно + +*!* +x = 5; // ошибки не будет, так как строгий режим не активирован +*/!* +``` + +[warn header="Отменить действие `use strict` никак нельзя"] +Не существует директивы `no use strict` или подобной, которая возвращает в старый режим. + +Если уж вошли в современный режим, то это дорога в один конец. +[/warn] + +[smart header="`use strict` для функций"] +Через некоторое время мы будем проходить [функции](/function-basics). На будущее заметим, что `use strict` также можно указывать в начале функций, тогда строгий режим будет действовать только внутри функции. +[/smart] + +## Нужен ли мне use strict? + +В строгом режиме исправлены некоторые ошибки в дизайне языка. Если старый код их эксплуатировал -- будет сюрприз и, возможно, ошибка. + +**Основная проблема при использовании `use strict` -- поддержка браузеров IE9-, которые игнорируют `use strict`.** + +Предположим, что мы, используя `use strict`, разработали код и протестировали его в браузере Chrome. Всё работает... Однако, вероятность ошибок при этом в IE9- выросла! Он-то всегда работает по старому стандарту, а значит, иногда по-другому. Возникающие ошибки придётся отлаживать уже в IE9-, и это намного менее приятно, нежели в Chrome. + +Поэтому строгий режим используют пока ещё не повсеместно. + +Впрочем, проблема не так страшна. Несовместимостей мало. И, если их знать (а в учебнике мы будем останавливаться на них) и писать правильный код, то всё будет в порядке и `use strict` станет нашим верным помощником. + +[smart header="ES5-shim"] +Директива `use strict` влияет только на "опасные" возможности языка. Большинство приятных новых возможностей, которые мы изучим далее, будут работать в любом случае, поскольку ни с чем не конфликтуют. + +Браузер IE8 был создан до появления ES5, поэтому не знает о них. Так что же, неужели они для нас закрыты, если мы решаем поддерживать IE8? К счастью, нет. + +Сообществом создана библиотека [ES5 shim](https://github.com/es-shims/es5-shim), которая при подключении файлов `es5-shim.js` и `es5-sham.js` добавляет в IE8- многие возможности из современного стандарта JavaScript. +[/smart] + +## Итого + +В этой главе мы познакомились с понятием "строгий режим" и посмотрели одно из важнейших его отличий -- обязательность объявления переменных. Ранее уже упоминалось, что объявлять переменные через `var` полезно с целью избежания конфликтов в IE8-. Теперь есть ещё довод -- соответствие современному стандарту JavaScript. + +Есть и другие отличия работы в строгом режиме, которые мы будем рассматривать далее, по мере погружения в JavaScript. + +Далее мы будем предполагать, что разработка ведётся либо в современном браузере, либо в IE8- с подключённым [ES5 shim](https://github.com/es-shims/es5-shim). Это позволит нам использовать большинство возможностей современного JavaScript во всех браузерах. diff --git a/1-js/2-first-steps/6-types-intro/article.md b/1-js/2-first-steps/6-types-intro/article.md new file mode 100644 index 00000000..71a7da0d --- /dev/null +++ b/1-js/2-first-steps/6-types-intro/article.md @@ -0,0 +1,105 @@ +# Шесть типов данных + +В JavaScript существует несколько основных типов данных. +[cut] + +## Число "number" + +```js +var n = 123; +n = 12.345; +``` + +Единый тип *число* используется как для целых, так и для дробных чисел. + +Существуют специальные числовые значения `Infinity` (бесконечность) и `NaN` (ошибка вычислений). + +Например, бесконечность `Infinity` получается при делении на ноль: + +```js +//+ run +alert( 1 / 0 ); // Infinity +``` + +Ошибка вычислений `NaN` будет результатом некорректной математической операции, например: + +```js +//+ run +alert( "нечисло" * 2 ); // NaN, ошибка +``` + +Эти значения формально принадлежат типу "число", хотя, конечно, числами в их обычном понимании не являются. + +## Строка "string" + +```js +var str = "Мама мыла раму"; +str = 'Одинарные кавычки тоже подойдут'; +``` + +**В JavaScript одинарные и двойные кавычки равноправны.** Можно использовать или те или другие. + +[smart header="Тип *символ* не существует, есть только *строка*."] +В некоторых языках программирования есть специальный тип данных для одного символа. Например, в языке С это `char`. В JavaScript есть только тип "строка" `string`. Что, надо сказать, вполне удобно. +[/smart] + +## Булевый (логический) тип "boolean" + +У него всего два значения: `true` (истина) и `false` (ложь). + +Как правило, такой тип используется для хранения значения типа да/нет, например: + +```js +var checked = true; // поле формы помечено галочкой +checked = false; // поле формы не содержит галочки +``` + +О нём мы поговорим более подробно, когда будем обсуждать логические вычисления и условные операторы. + +## Специальное значение "null" + +Значение `null` не относится ни к одному из типов выше, а образует свой отдельный тип, состоящий из единственного значения `null`: + +```js +var age = null; +``` + +В JavaScript `null` не является "ссылкой на несуществующий объект" или "нулевым указателем", как в некоторых других языках. Это просто специальное значение, которое имеет смысл "ничего" или "значение неизвестно". + +В частности, код выше говорит о том, что возраст `age` неизвестен. + +## Специальное значение "undefined" + +Значение `undefined`, как и `null`, образует свой собственный тип, состоящий из одного этого значения. Оно имеет смысл "значение не присвоено". + +Если переменная объявлена, но в неё ничего не записано, то ее значение как раз и есть `undefined`: + +```js +//+ run +var x; +alert(x); // выведет "undefined" +``` + +Можно присвоить `undefined` и в явном виде, хотя это делается редко: + +```js +//+ run +var x = 123; +x = undefined; + +alert(x); // "undefined" +``` + +В явном виде `undefined` обычно не присваивают, так как это противоречит его смыслу. Для записи в переменную "пустого" или "неизвестного" значения используется `null`. + +## Объекты "object" + +Первые 5 типов называют *"примитивными"*. + +Особняком стоит шестой тип: *"объекты"*. К нему относятся, например, даты, он используется для коллекций данных и для объявления более сложных сущностей. Позже мы вернёмся к этому типу и рассмотрим его принципиальные отличия от примитивов. + +## Итого + +Есть 5 "примитивных" типов: `number`, `string`, `boolean`, `null`, `undefined` и 6-й тип -- объекты `object`. + +Очень скоро мы изучим их во всех деталях. \ No newline at end of file diff --git a/1-js/2-first-steps/7-properties-and-methods/article.md b/1-js/2-first-steps/7-properties-and-methods/article.md new file mode 100644 index 00000000..d878c3f9 --- /dev/null +++ b/1-js/2-first-steps/7-properties-and-methods/article.md @@ -0,0 +1,112 @@ +# Методы и свойства + +Все значения в JavaScript, за исключением `null` и `undefined`, содержат набор вспомогательных функций и значений, доступных "через точку". + +Такие функции называют "методами", а значения -- "свойствами". Здесь мы рассмотрим основы использования свойств и методов. + +[cut] + +## Свойство str.length + +У строки есть *свойство* `length`, содержащее длину: + +```js +//+ run +alert( "Привет, мир!".length ); // 12 +``` + +Можно и записать строку в переменную, а потом запросить её свойство: + +```js +//+ run +var str = "Привет, мир!"; +alert( str.length ); // 12 +``` + +## Метод str.toUpperCase() + +Также у строк есть *метод* `toUpperCase()`, который возвращает строку в верхнем регистре: + +```js +//+ run +var hello = "Привет, мир!"; + +*!* +alert( hello.toUpperCase() ); // "ПРИВЕТ, МИР!" +*/!* +``` + +[warn header="Вызов метода -- через круглые скобки!"] + +Обратите внимание, для вызова метода обязательно нужны круглые скобки. + +Посмотрите, например, результат обращения к `toUpperCase` без скобок: + +```js +//+ run +var hello = "Привет"; + +*!* +alert( hello.toUpperCase ); // function... +*/!* +``` + +Метод -- это встроенная команда ("функция", мы поговорим о них позже), которую нужно вызвать для получения значения. Если обратиться к ней без скобок, то результатом будет сама эта функция. Как правило браузер выведет её как-то так: `"function toUpperCase() { ... }"`, а чтобы получить результат -- нужно добавить скобки: + +```js +//+ run +var hello = "Привет"; + +*!* +alert( hello.toUpperCase() ); // ПРИВЕТ +*/!* +``` + +[/warn] + +Более подробно с различными свойствами и методами строк мы познакомимся в главе [](/string). + +## Метод num.toFixed(n) + +Есть методы и у чисел, например `num.toFixed(n)`. Он округляет число `num` до `n` знаков после запятой, при необходимости добивает нулями до данной длины и возвращает в виде строки (удобно для форматированного вывода): + +```js +//+ run +var n = 12.345; + +alert( n.toFixed(2) ); // "12.35" +alert( n.toFixed(0) ); // "12" +alert( n.toFixed(5) ); // "12.34500" +``` + +Детали работы `toFixed` разобраны в главе [](/number). + +[warn header="Обращение к методам чисел"] +К методу числа можно обратиться и напрямую: + +```js +//+ run +alert( 12.34.toFixed(1) ); // 12.3 +``` + +...Но если число целое, то будет проблема: + +```js +//+ run +alert( 12.toFixed(1) ); // ошибка! +``` + +Ошибка произойдёт потому, что JavaScript ожидает десятичную дробь после точки. + +Это -- особенность синтаксиса JavaScript. Вот так -- будет работать: + +```js +//+ run +alert( 12..toFixed(1) ); // 12.0 +``` + +[/warn] + + + +Мы еще встретимся со [строками](/string) и [числами](/number) в последующих главах и глубже познакомимся с их свойствами и методами. \ No newline at end of file diff --git a/1-js/2-first-steps/8-operators/1-increment-order/solution.md b/1-js/2-first-steps/8-operators/1-increment-order/solution.md new file mode 100644 index 00000000..de103276 --- /dev/null +++ b/1-js/2-first-steps/8-operators/1-increment-order/solution.md @@ -0,0 +1,23 @@ +# Разъяснения + +```js +//+ run +var a = 1, b = 1, c, d; + +// префиксная форма сначала увеличивает a до 2, а потом возвращает +c = ++a; alert(c); // 2 + +// постфиксная форма увеличивает, но возвращает старое значение +d = b++; alert(d); // 1 + +// сначала увеличили a до 3, потом использовали в арифметике +c = (2+ ++a); alert(c); // 5 + +// увеличили b до 3, но в этом выражении оставили старое значение +d = (2+ b++); alert(d); // 4 + +// каждую переменную увеличили по 2 раза +alert(a); // 3 +alert(b); // 3 +``` + diff --git a/1-js/2-first-steps/8-operators/1-increment-order/task.md b/1-js/2-first-steps/8-operators/1-increment-order/task.md new file mode 100644 index 00000000..6489a2af --- /dev/null +++ b/1-js/2-first-steps/8-operators/1-increment-order/task.md @@ -0,0 +1,20 @@ +# Инкремент, порядок срабатывания + +[importance 5] + +Посмотрите, понятно ли вам, почему код ниже работает именно так? + +```js +//+ run +var a = 1, b = 1, c, d; + +c = ++a; alert(c); // 2 +d = b++; alert(d); // 1 + +c = (2+ ++a); alert(c); // 5 +d = (2+ b++); alert(d); // 4 + +alert(a); // 3 +alert(b); // 3 +``` + diff --git a/1-js/2-first-steps/8-operators/2-assignment-result/solution.md b/1-js/2-first-steps/8-operators/2-assignment-result/solution.md new file mode 100644 index 00000000..c0e69ce4 --- /dev/null +++ b/1-js/2-first-steps/8-operators/2-assignment-result/solution.md @@ -0,0 +1,11 @@ +Ответ: `x = 5`. + +Оператор присваивания возвращает значение, которое будет записано в переменную, например: + +```js +//+ run +var a = 2; +alert( a *= 2 ); // 4 +``` + +Отсюда `x = 1 + 4 = 5`. \ No newline at end of file diff --git a/1-js/2-first-steps/8-operators/2-assignment-result/task.md b/1-js/2-first-steps/8-operators/2-assignment-result/task.md new file mode 100644 index 00000000..a3c166af --- /dev/null +++ b/1-js/2-first-steps/8-operators/2-assignment-result/task.md @@ -0,0 +1,12 @@ +# Результат присваивания + +[importance 3] + +Чему будет равен `x` в примере ниже? + +```js +var a = 2; + +var x = 1 + (a *= 2); +``` + diff --git a/1-js/2-first-steps/8-operators/article.md b/1-js/2-first-steps/8-operators/article.md new file mode 100644 index 00000000..faebc399 --- /dev/null +++ b/1-js/2-first-steps/8-operators/article.md @@ -0,0 +1,415 @@ +# Основные операторы + +Для работы с переменными, со значениями, JavaScript поддерживает все стандартные операторы, большинство которых есть и в других языках программирования. +[cut] + +## Термины: "унарный", "бинарный", "операнд" + +У операторов есть своя терминология, которая используется во всех языках программирования. +
    +
  • *Операнд* -- то, к чему применяется оператор. Например: `5 * 2` -- оператор умножения с левым и правым операндами. Другое название: "аргумент оператора".
  • +
  • *Унарным* называется оператор, который применяется к одному выражению. Например, оператор унарный минус `"-"` меняет знак числа на противоположный: + +```js +//+ run +var x = 1; +alert( -x ); // -1, унарный минус +alert( -(x+2) ); // -3, унарный минус применён к результату сложения x+2 +alert( -(-3) ); // 3 +``` + +
  • +
  • *Бинарным* называется оператор, который применяется к двум операндам. Тот же минус существует и в бинарной форме: + +```js +//+ run +var x = 1, y = 3; +alert( y - x ); // 2, бинарный минус +``` + +
  • +
+ +Некоторые операторы, например, вычитание `"-"` и сложение `"+"`, бывают в двух вариантах: унарный -- при применении к одному операнду, и бинарный -- к двум. + +## Арифметические операторы + +Базовые арифметические операторы знакомы нам с детства: это плюс `+`, минус `-`, умножить `*`, поделить `/`. + +Например: + +```js +//+ run +alert(2 + 2); // 4 +``` + +Или чуть сложнее: + +```js +//+ run +var i = 2; + +i = (2 + i) * 3 / i; + +alert(i); // 6 +``` + +**Более редкий арифметический оператор `%` интересен тем, что никакого отношения к процентам не имеет. Его результат `a % b` -- это остаток от деления `a` на `b`.** + +Например: + +```js +//+ run +alert(5 % 2); // 1, остаток от деления 5 на 2 +alert(8 % 3); // 2, остаток от деления 8 на 3 +alert(6 % 3); // 0, остаток от деления 6 на 3 +``` + +### Сложение строк, бинарный + + +Если бинарный оператор `+` применить к строкам, то он их объединяет в одну: + +```js +var a = "моя" + "строка"; +alert(a); // моястрока +``` + +**Если хотя бы один аргумент является строкой, то второй будет также преобразован к строке!** + +Причем не важно, справа или слева находится операнд-строка, в любом случае нестроковый аргумент будет преобразован. Например: + +```js +//+ run +alert( '1' + 2 ); // "12" +alert( 2 + '1' ); // "21" +``` + +Это приведение к строке -- особенность бинарного оператора `"+"`. + +**Остальные арифметические операторы работают только с числами и всегда приводят аргументы к числу.** + +Например: + +```js +//+ run +alert( '1' - 2 ); // -1 +alert( 6 / '2'); // 3 +``` + +### Унарный плюс + + +Унарный, то есть применённый к одному значению, плюс как арифметический оператор ничего не делает: + +```js +//+ run +alert( +1 ); // 1 +alert( +(1-2) ); // -1 +``` + +Как видно, плюс ничего не изменил в выражениях. Результат -- такой же, как и без него. + +Тем не менее, он широко применяется, так как его "побочный эффект" -- преобразование значения в число. + +Например, у нас есть два числа, в форме строк, и нужно их сложить. Бинарный плюс сложит их как строки, поэтому используем унарный плюс, чтобы преобразовать к числу: + +```js +//+ run +var a = "2"; +var b = "3"; + +alert( a + b ); // "23", так как бинарный плюс складывает строки +alert( +a + b ); // "23", второй операнд - всё ещё строка + +alert( +a + +b); // 5, число, так как оба операнда предварительно преобразованы в числа +``` + +## Присваивание + +Оператор присваивания выглядит как знак равенства `=`: + +```js +var i = 1 + 2; + +alert(i); // 3 +``` + +Он вычисляет выражение, которое находится справа, и присваивает результат переменной. Это выражение может быть достаточно сложным и включать в себя любые другие переменные: + +```js +//+ run +var a = 1; +var b = 2; + +*!* +a = b + a + 3; // (*) +*/!* + +alert(a); // 6 +``` + +В строке `(*)` сначала произойдет вычисление, использующее текущее значение `a` (т.е. `1`), после чего результат перезапишет старое значение `a`. + +**Возможно присваивание по цепочке:** + +```js +//+ run +var a, b, c; + +*!* +a = b = c = 2 + 2; +*/!* + +alert(a); // 4 +alert(b); // 4 +alert(c); // 4 +``` + +Такое присваивание работает справа-налево, то есть сначала вычислятся самое правое выражение `2+2`, присвоится в `c`, затем выполнится `b = c` и, наконец, `a = b`. + +[smart header="Оператор `\"=\"` возвращает значение"] +Все операторы возвращают значение. Вызов `x = выражение` записывает выражение в `x`, а затем возвращает его. Благодаря этому присваивание можно использовать как часть более сложного выражения: + +```js +//+ run +var a = 1; +var b = 2; + +*!* +var c = 3 - (a = b + 1); +*/!* + +alert(a); // 3 +alert(c); // 0 +``` + +В примере выше результатом `(a = b + 1)` является значение, которое записывается в `a` (т.е. `3`). Оно используется для вычисления `c`. + +Забавное применение присваивания, не так ли? + +Знать, как это работает -- стоит обязательно, а вот писать самому -- только если вы уверены, что это сделает код более читаемым и понятным. +[/smart] + + +## Приоритет + +В том случае, если в выражении есть несколько операторов - порядок их выполнения определяется *приоритетом*. + +Из школы мы знаем, что умножение в выражении `2 * 2 + 1` выполнится раньше сложения, т.к. его *приоритет* выше, а скобки явно задают порядок выполнения. Но в JavaScript -- гораздо больше операторов, поэтому существует целая [таблица приоритетов](https://developer.mozilla.org/en/JavaScript/Reference/operators/operator_precedence). + +Она содержит как уже пройденные операторы, так и те, которые мы еще не проходили. В ней каждому оператору задан числовой приоритет. Тот, у кого число меньше -- выполнится раньше. Если приоритет одинаковый, то порядок выполнения -- слева направо. + +Отрывок из таблицы: + + + + + + + + + +
.........
5умножение`*`
5деление`/`
6сложение`+`
6вычитание`-`
17присвоение`=`
.........
+ +Посмотрим на таблицу в действии. + +В выражении `x = 2 * 2 + 1` есть три оператора: присвоение `=`, умножение `*` и сложение `+`. Приоритет умножения `*` равен `5`, оно выполнится первым, затем произойдёт сложение `+`, у которого приоритет `6`, и после них -- присвоение `=`, с приоритетом 17. + +## Инкремент/декремент: ++, -- + +Одной из наиболее частых операций в JavaScript, как и во многих других языках программирования, является увеличение или уменьшение переменной на единицу. + +Для этого существуют даже специальные операторы: +
    +
  • **Инкремент** `++` увеличивает на 1: + +```js +//+ run +var i = 2; +i++; // более короткая запись для i = i + 1. +alert(i); // 3 +``` + +
  • +
  • **Декремент** `--` уменьшает на 1: + +```js +//+ run +var i = 2; +i--; // более короткая запись для i = i - 1. +alert(i); // 1 +``` + +
  • +
+ +[warn] +Инкремент/декремент можно применить только к переменной. +Код `5++` даст ошибку. +[/warn] + +Вызывать эти операторы можно не только после, но и перед переменной: `i++` (называется "постфиксная форма") или `++i` ("префиксная форма"). + +Обе эти формы записи делают одно и то же: увеличивают на `1`. + +Тем не менее, между ними существует разница. Она видна только в том случае, когда мы хотим не только увеличить/уменьшить переменную, но и использовать результат в том же выражении. + +Например: + +```js +//+ run +var i = 1; +var a = ++i; // (*) + +alert(a); // *!*2*/!* +``` + +В строке `(*)` вызов `++i` увеличит переменную, а *затем* вернёт ее значение в `a`. **То есть, в `a` попадёт значение `i` *после* увеличения**. + +**Постфиксная форма `i++` отличается от префиксной `++i` тем, что возвращает старое значение, бывшее до увеличения.** + +В примере ниже в `a` попадёт старое значение `i`, равное `1`: + +```js +//+ run +var i = 1; +var a = i++; // (*) + +alert(a); // *!*1*/!* +``` + +
    +
  • Если результат оператора не используется, а нужно только увеличить/уменьшить переменную -- без разницы, какую форму использовать: + +```js +//+ run +var i = 0; +i++; +++i; +alert(i); // 2 +``` + +
  • +
  • Если хочется тут же использовать результат, то нужна префиксная форма: + +```js +//+ run +var i = 0; +alert( ++i ); // 1 +``` + +
  • +
  • Если нужно увеличить, но нужно значение переменной *до увеличения* -- постфиксная форма: + +```js +//+ run +var i = 0; +alert( i++ ); // 0 +``` + +
  • +
+ +**Инкремент/декремент можно использовать в любых выражениях.** + +При этом он имеет более высокий приоритет и выполняется раньше, чем арифметические операции: + +```js +//+ run +var i = 1; +alert( 2 * ++i ); // 4 +``` + + + +```js +//+ run +var i = 1; +alert( 2 * i++ ); // 2, выполнился раньше но значение вернул старое +``` + +При этом, нужно с осторожностью использовать такую запись, потому что при чтении кода зачастую неочевидно, что переменая увеличивается. Три строки -- длиннее, зато нагляднее: + +```js +//+ run +var i = 1; +alert( 2 * i ); +i++; +``` + +## Побитовые операторы + +Побитовые операторы рассматривают аргументы как 32-разрядные целые числа и работают на уровне их внутреннего двоичного представления. + +Эти операторы не являются чем-то специфичным для JavaScript, они поддерживаются в большинстве языков программирования. + +Поддерживаются следующие побитовые операторы: +
    +
  • AND(и) ( `&` )
  • +
  • OR(или) ( `|` )
  • +
  • XOR(побитовое исключающее или) ( `^` )
  • +
  • NOT(не) ( `~` )
  • +
  • LEFT SHIFT(левый сдвиг) ( `<<` )
  • +
  • RIGHT SHIFT(правый сдвиг) ( `>>` )
  • +
  • ZERO-FILL RIGHT SHIFT(правый сдвиг с заполнением нулями) ( `>>>` )
  • +
+ +Вы можете более подробно почитать о них в отдельной статье [](/bitwise-operators). + + + +## Вызов операторов с присваиванием + +Часто нужно применить оператор к переменной и сохранить результат в ней же, например: + +```js +var n = 2; +n = n + 5; +n = n * 2; +``` + +Эту запись можно укоротить при помощи совмещённых операторов: +=, -=, *=, /=, >>=, <<=, >>>=, &=, |=, ^=. + +Вот так: + +```js +//+ run +var n = 2; +n += 5; // теперь n=7 (работает как n = n + 5) +n *= 2; // теперь n=14 (работает как n = n * 2) + +alert(n); // 14 +``` + +Все эти операторы имеют в точности такой же приоритет, как обычное присваивание, то есть выполняются после большинства других операций. + + + +## Оператор запятая + +Запятая тоже является оператором. Ее можно вызвать явным образом, например: + +```js +//+ run +*!* +a = (5, 6); +*/!* + +alert(a); +``` + +Запятая позволяет перечислять выражения, разделяя их запятой `','`. Каждое из них -- вычисляется и отбрасывается, за исключением последнего, которое возвращается. + +Запятая -- единственный оператор, приоритет которого ниже присваивания. В выражении `a = (5,6)` для явного задания приоритета использованы скобки, иначе оператор `'='` выполнился бы до запятой `','`, получилось бы `(a=5), 6`. + +Зачем же нужен такой странный оператор, который отбрасывает значения всех перечисленных выражений, кроме последнего? + +Обычно он используется в составе более сложных конструкций, чтобы сделать несколько действий в одной строке. Например: + +```js +// три операции в одной строке +for (*!*a = 1, b = 3, c = a*b*/!*; a < 10; a++) { + ... +} +``` + +Такие трюки используются во многих JavaScript-фреймворках для укорачивания кода. \ No newline at end of file diff --git a/1-js/2-first-steps/9-comparison/article.md b/1-js/2-first-steps/9-comparison/article.md new file mode 100644 index 00000000..29832001 --- /dev/null +++ b/1-js/2-first-steps/9-comparison/article.md @@ -0,0 +1,232 @@ +# Операторы сравнения и логические значения + +В этом разделе мы познакомимся с операторами сравнения и с логическими значениями, которые такие операторы возвращают. +[cut] +Многие операторы сравнения знакомы нам со школы: + +
    +
  • Больше/меньше: a > b, a < b.
  • +
  • Больше/меньше или равно: a >= b, a <= b.
  • +
  • Равно `a == b`. +Для сравнения используется два символа равенства `'='`. Один символ `a = b` означал бы присваивание.
  • +
  • "Не равно". В школе он пишется как , в JavaScript -- знак равенства с восклицательным знаком перед ним !=.
  • +
+ +## Логические значения + +Как и другие операторы, сравнение возвращает значение. Это значение имеет специальный *логический* тип. + +Существует всего два логических значения: +
    +
  • `true` -- имеет смысл "да", "верно", "истина".
  • +
  • `false` -- означает "нет", "неверно", "ложь".
  • +
+ +Например: + +```js +//+ run +alert( 2 > 1 ); // true, верно +alert( 2 == 1 ); // false, неверно +alert( 2 != 1 ); // true +``` + +Логические значения можно использовать и напрямую, присваивать переменным, работать с ними как с любыми другими: + +```js +//+ run +var a = true; // присвоили явно +var b = 3 > 4; // false + +alert( b ); // false + +alert( a == b ); // (true == false) неверно, результат false +``` + +## Сравнение строк + +Строки сравниваются побуквенно: + +```js +//+ run +alert( 'Б' > 'А' ); // true +``` + +[warn header="Осторожно, Unicode!"] +Аналогом "алфавита" во внутреннем представлении строк служит кодировка, у каждого символа -- свой номер (код). JavaScript использует кодировку [Unicode](http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4). + +При этом сравниваются *численные коды символов*. В частности, код у символа `Б` больше, чем у `А`, поэтому и результат сравнения такой. + +**В кодировке Unicode обычно код у строчной буквы больше, чем у прописной.** + +Поэтому регистр имеет значение: + +```js +//+ run +alert('а' > 'Я'); // true, строчные буквы больше прописных +``` + +Для корректного сравнения символы должны быть в одинаковом регистре. +[/warn] + +Если строка состоит из нескольких букв, то сравнение осуществляется как в телефонной книжке или в словаре. Сначала сравниваются первые буквы, потом вторые, и так далее, пока одна не будет больше другой. + +Иными словами, больше -- та строка, которая в телефонной книге была бы на большей странице. + +Например: +
    +
  • Если первая буква первой строки больше -- значит первая строка больше, независимо от остальных символов: + +```js +//+ run +alert( 'Банан' > 'Аят' ); +``` + +
  • +
  • Если одинаковы -- сравнение идёт дальше. Здесь оно дойдёт до третьей буквы: + +```js +//+ run +alert( 'Вася' > 'Ваня' ); // true, т.к. 'с' > 'н' +``` + +
  • +
  • При этом любая буква больше отсутствия буквы: + +```js +//+ run +alert( 'Привет' > 'Прив' ); // true, так как 'е' больше чем "ничего". +``` + +
  • +
+Такое сравнение называется *лексикографическим*. + + +[warn] +Обычно мы получаем значения от посетителя в виде строк. Например, `prompt` возвращает *строку*, которую ввел посетитель. + +Числа, полученные таким образом, в виде строк сравнивать нельзя, результат будет неверен. Например: + +```js +//+ run +alert( "2" > "14" ); // true, неверно, ведь 2 не больше 14 +``` + +В примере выше `2` оказалось больше `14`, потому что строки сравниваются посимвольно, а первый символ `'2'` больше `'1'`. + +Правильно было бы преобразовать их к числу явным образом. Например, поставив перед ними `+`: + +```js +//+ run +alert( +"2" > +"14" ); // false, теперь правильно +``` + +[/warn] + +## Сравнение разных типов + +При сравнении значения преобразуются к числам. Исключение: когда оба значения -- строки, тогда не преобразуются. + +Например: + +```js +//+ run +alert( '2' > 1 ); // true +alert( '01' == 1 ); //true +alert( false == 0 ); // true, false становится 0, а true 1. +``` + +Тема преобразований типов будет продолжена далее, в главе [](/types-conversion). + +## Строгое равенство + +Обычное равенство не может отличить `0` от `false`: + +```js +//+ run +alert(0 == false); // true, так как false преобразуется к 0 +``` + +Что же делать, если всё же нужно отличить `0` от `false`? + +**Для проверки равенства без преобразования типов используются операторы строгого равенства `===` (тройное равно) и `!==`.** + +Они сравнивают без приведения типов. Если тип разный, то такие значения всегда неравны (строго): + +```js +//+ run +alert(0 === false); // false, т.к. типы различны +``` + +Строгое сравнение предпочтительно, если мы хотим быть уверены, что "сюрпризов" не будет. + +## Сравнение с null и undefined + +Проблемы со специальными значениями возможны, когда к переменной применяется операция сравнения `> < <= >=`, а у неё может быть как численное значение, так и `null/undefined`. + +**Интуитивно кажется, что `null/undefined` эквивалентны нулю, но это не так! Они ведут себя по-другому.** + +
    +
  1. **Значения `null` и `undefined` равны `==` друг другу и не равны чему бы то ни было ещё.** +Это жёсткое правило буквально прописано в спецификации языка.
  2. +
  3. **При преобразовании в число `null` становится `0`, а `undefined` становится `NaN`.**
  4. +
+ +Посмотрим забавные следствия. + +[smart header="Некорректный результат сравнения `null` с `0`"] +Сравним `null` с нулём: + +```js +//+ run +alert(null > 0); // false +alert(null == 0); // false +``` + +Итак, мы получили, что `null` не больше и не равен нулю. А теперь... + +```js +//+ run +alert(null >= 0); // *!*true*/!* +``` + +Как такое возможно? Если нечто *"больше или равно нулю"*, то резонно полагать, что оно либо *больше*, либо *равно*. Но здесь это не так. + +Дело в том, что алгоритмы проверки равенства `==` и сравнения `>= > < <=` работают по-разному. + +Сравнение честно приводит к числу, получается ноль. А при проверке равенства значения `null` и `undefined` обрабатываются особым образом: они равны друг другу, но не равны чему-то ещё. + +В результате получается странная с точки зрения здравого смысла ситуация, которую мы видели в примере выше. + +[/smart] + +[smart header="Несравнимый `undefined`"] +Значение `undefined` вообще нельзя сравнивать: + +```js +//+ run +alert(undefined > 0); // false (1) +alert(undefined < 0); // false (2) +alert(undefined == 0); // false (3) +``` + +
    +
  • Сравнения `(1)` и `(2)` дают `false` потому, что `undefined` при преобразовании к числу даёт `NaN`. А значение `NaN` по стандарту устроено так, что сравнения `==`, `<`, `>`, `<=`, `>=` и даже `===` с ним возвращают `false`.
  • +
  • Проверка равенства `(3)` даёт `false`, потому что в стандарте явно прописано, что `undefined` равно лишь `null` и ничему другому.
  • +
+[/smart] + +**Вывод: любые сравнения с `undefined/null`, кроме точного `===`, следует делать с осторожностью.** + +Желательно не использовать сравнения `>= > < <=` с ними, во избежание ошибок в коде. + + +## Итого + +
    +
  • В JavaScript есть логические значения `true` (истина) и `false` (ложь). Операторы сравнения возвращают их.
  • +
  • Строки сравниваются побуквенно.
  • +
  • Значения разных типов приводятся к числу при сравнении, за исключением строгого равенства `===` (`!==`).
  • +
  • Значения `null` и `undefined` равны `==` друг другу и не равны ничему другому. В других сравнениях (с участием `>`,`<`) их лучше не использовать, так как они ведут себя не как `0`.
  • +
\ No newline at end of file diff --git a/1-js/2-first-steps/index.md b/1-js/2-first-steps/index.md new file mode 100644 index 00000000..eee61f1c --- /dev/null +++ b/1-js/2-first-steps/index.md @@ -0,0 +1,3 @@ +# Основы JavaScript + +Основные кирпичики из которых состоят скрипты. \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/article.md b/1-js/3-writing-js/1-debugging-chrome/article.md new file mode 100644 index 00000000..0d497bc7 --- /dev/null +++ b/1-js/3-writing-js/1-debugging-chrome/article.md @@ -0,0 +1,254 @@ +# Отладка в браузере Chrome + +Перед тем, как двигаться дальше, поговорим об отладке скриптов. + +Все современные браузеры поддерживают для этого "инструменты разработчика". Исправление ошибок с их помощью намного проще и быстрее. + +На текущий момент самые многофункциональные инструменты -- в браузере Chrome. Также очень хорош Firebug (для Firefox). + +[cut] + +## Общий вид панели Sources + +В вашей версии Chrome панель может выглядеть несколько по-иному, но что где находится, должно быть понятно. + +Зайдите на страницу [debugging/pow/index.html](/debugging/pow/index.html) браузером Chrome. + +Откройте инструменты разработчика: [key F12] или в меню `Инструменты > Инструменты Разработчика`. + +Выберите сверху `Sources` (вместо иконок у вас могут быть просто надписи "Elements", "Resources", "Network", "Sources"...) + + + +Вы видите три зоны: + +
    +
  1. **Зона исходных файлов.** В ней находятся все подключённые к странице файлы, включая JS/CSS. Выберите `pow.js`, если он не выбран.
  2. +
  3. **Зона текста.** В ней находится текст файлов.
  4. +
  5. **Зона информации и контроля.** Мы поговорим о ней позже.
  6. +
+ +Обычно зона исходных файлов при отладке не нужна. Скройте её кнопкой . + +## Общие кнопки управления + + + +Три полезные кнопки управления: +
+
Формат
+
Нажатие форматирует текст текущего файла, расставляет отступы. Нужна, если вы хотите разобраться в чужом коде, плохо отформатированном или сжатом.
+
Консоль
+
Очень полезная кнопка, открывает тут же консоль для запуска команд. Можно смотреть код и тут же запускайть функции. Её нажатие можно заменить на клавишу Esc.
+
Окно
+
Если код очень большой, то можно вынести инструменты разработки вбок или в отдельное окно, зажав эту кнопку и выбрав соответствующий вариант из списка.
+
+ +## Точки остановки + +Открыли `pow.js` в зоне текста? Кликните на 6й строке файла `pow.js`, прямо на цифре 6. + +Поздравляю! Вы поставили "точку остановки" или, как чаще говорят, "брейкпойнт". + + + +Слово *Брейкпойнт* (breakpoint) -- часто используемый английский жаргонизм. Это то место в коде, где отладчик будет *автоматически* останавливать выполнение JavaScript, как только оно до него дойдёт. + + +**В остановленном коде можно посмотреть текущие значения переменных, выполнять команды и т.п., в общем -- отлаживать его.** + +Вы можете видеть, что информация о точке остановки появилась справа, в подвкладке Breakpoints. + +Вкладка Breakpoints очень удобна, когда код большой, она позволяет: + +
    +
  • Быстро перейти на место кода, где стоит брейкпойнт -- кликом на текст.
  • +
  • Временно выключить брейкпойнт -- кликом на чекбокс.
  • +
  • Быстро удалить брейкпойнт -- правым кликом на текст и выбором Remove...
  • +
+ +[smart header="Дополнительные возможности"] +
    +
  • Остановку можно инициировать и напрямую из кода скрипта, командой `debugger`: + +```js +function pow(x, n) { + ... + debugger; // <-- отладчик остановится тут + ... +} +``` + +
  • +
  • *Правый клик* на номер строки `pow.js` позволит создать условную точку остановки (conditional breakpoint), т.е. задать условие, при котором точка остановки сработает. + +Это удобно, если остановка нужна только при определённом значении переменной или параметра функции. +
  • +
+[/smart] + +## Остановиться и осмотреться + +Наша функция выполняется сразу при загрузке страницы, так что самый простой способ активировать JavaScript -- перезагрузить её. Итак, нажимаем [key F5] (Windows, Linux) или [key Cmd+R] (Mac). + +Если вы сделали всё, как описано выше, то выполнение прервётся как раз на 6й строке. + + + +Обратите внимание на информационные вкладки справа (отмечены стрелками). + +В них мы можем посмотреть текущее состояние: +
    +
  1. **`Watch Expressions` -- показывает текущие значения любых выражений.** + +Можно раскрыть эту вкладку, нажать мышью `+` на ней и ввести любое выражение. Отладчик будет отображать его значение на текущий момент, автоматически перевычисляя его при проходе по коду.
  2. +
  3. **`Call Stack` -- стек вызовов, все вложенные вызовы, которые привели к текущему месту кода.** + +На текущий момент видно, отладчик находится в функции `pow` (pow.js, строка 6), вызванной из анонимного кода (index.html, строка 13).
  4. +
  5. **`Scope Variables` -- переменные.** + +На текущий момент строка 6 ещё не выполнилась, поэтому `result` равен `undefined`. + +В `Local` показываются переменные функции: объявленные через `var` и параметры. Вы также можете там видеть ключевое слово `this`, если вы не знаете, что это такое -- ничего страшного, мы это обсудим позже, в следующих главах учебника. + +В `Global` -- глобальные переменные и функции. +
  6. +
+ +## Управление выполнением + +Пришло время "погонять" скрипт и "оттрейсить" (от англ. trace, отслеживать) его работу. + +Обратим внимание на панель управления справа-сверху, в ней есть 6 кнопок: + +
+
-- продолжить выполнение, горячая клавиша [key F8].
+
Если скрипт не встретит новых точек остановки, то на этом работа в отладчике закончится. + +Нажмите на эту кнопку. + +Вы увидите, что отладчик остался на той же строке, но в `Call Stack` появился новый вызов. Это произошло потому, что в 6й строке находится рекурсивный вызов функции `pow`, т.е. управление перешло в неё опять, но с другими аргументами. + +Походите по стеку вверх-вниз -- вы увидите, что действительно аргументы разные. +
+
-- сделать шаг, не заходя внутрь функции, горячая клавиша [key F10].
+
Выполняет одну команду скрипта. Если в ней есть вызов функции -- то отладчик обходит его стороной, т.е. не переходит на код внутри. + +Эта кнопка очень удобна, если в текущей строке вызывается функция JS-фреймворка или какая-то другая, которая нас ну совсем не интересует. Тогда выполнение продолжится дальше, без захода в эту функцию, что нам и нужно. + +Обратим внимание, в данном случае эта кнопка при нажатии всё-таки перейдёт внутрь вложенного вызова `pow`, так как внутри `pow` находится брейкпойнт, а на включённых брейкпойнтах отладчик останавливается всегда. +
+
-- сделать шаг, горячая клавиша [key F11].
+
Выполняет одну команду скрипта и переходит к следующей. Если есть вложенный вызов, то заходит внутрь функции. + +Эта кнопка позволяет подробнейшим образом пройтись по очереди по командам скрипта. +
+
-- выполнять до выхода из текущей функции, горячая клавиша [key Shift+F11].
+
Выполняет команды до завершения текущей функции. + +Эта кнопка очень удобна в случае, если мы нечаянно вошли во вложенный вызов, который нам не интересен -- чтобы быстро из него выйти. +
+
-- отключить/включить все точки остановки.
+
Эта кнопка никак не двигает нас по коду, она позволяет временно отключить все точки остановки в файле. +
+
-- включить/отключить автоматическую остановку при ошибке.
+
Эта кнопка -- одна из самых важных. + +Нажмите её несколько раз. В старых версиях Chrome у неё три режима -- нужен фиолетовый, в новых -- два, тогда достаточно синего. + +Когда она включена, то при ошибке в коде он автоматически остановится и мы сможем посмотреть в отладчике текущие значения переменных, при желании выполнить команды и выяснить, как так получилось. +
+
+ +**Процесс отладки заключается в том, что мы останавливаем скрипт, смотрим, что с переменными, переходим дальше и ищем, где поведение отклоняется от правильного.** + +[smart header="Дополнительные возможности"] +Правый клик на номер строки открывает контекстное меню, в котором можно запустить выполнение кода до неё (Continue to here). + +Это очень удобно, если промежуточные строки нас не интересуют. +[/smart] + + + +## Консоль + +При отладке, кроме просмотра переменных, бывает полезно запускать команды JavaScript. Для этого нужна консоль. + +В неё можно перейти, нажав кнопку "Console" вверху-справа, а можно и открыть в дополнение к отладчику, нажав на кнопку или клавишей [key ESC]. + +**Самая любимая команда разработчиков: `console.log(...)`.** + +Она пишет переданные ей аргументы в консоль, например: + +```js +//+ run +// результат будет виден в консоли +for(var i=0; i<5; i++) { + console.log("значение", i); +} +``` + +Полную информацию по специальным командам консоли вы можете получить на странице [](https://developers.google.com/chrome-developer-tools/docs/commandline-api?hl=ru). Эти команды также действуют в Firebug (отладчик для браузера Firefox). + +Консоль поддерживают все браузеры, и, хотя IE10- поддерживает далеко не все функции, `console.log` работает везде, пользуйтесь им вместо `alert`. + +## Ошибки + +Ошибки JavaScript выводятся в консоли. + +Например, прервите отладку -- для этого достаточно закрыть инструменты разрабтчика -- и откройте страницу [debugging/pow-error/index.html](/debugging/pow-error/index.html). + +Перейдите во вкладку Console инструментов разработчика ([key Ctrl+Shift+J] / [key Cmd+Shift+J]). + +В консоли вы увидите что-то подобное: + + +Красная строка -- это сообщение об ошибке. + +В чём дело? Если мы хотим понять, что случилось -- перейдём в отладчик. + +Для этого: +
    +
  1. Перейдите на вкладку Sources.
  2. +
  3. Включите останов при ошибке, используя кнопку .
  4. +
  5. Перезагрузите страницу.
  6. +
+ +После перезагрузки страницы JavaScript-код запустится снова и отладчик остановит выполнение на строке с ошибкой: + + + +Можно посмотреть значения переменных. Открыть консоль и попробовать запустить что-то в ней. Поставить брейкпойнты раньше по коду и посмотреть, что привело к такой печальной картине, и так далее. + +В данном случае-то всё просто: опечатка в имени переменной `y` вместо `x`. Этот тип ошибки называется `ReferenceError`. + +## Итого + +Отладчик позволяет: +
    +
  • Останавливаться на отмеченном месте (breakpoint) или по команде `debugger`.
  • +
  • Выполнять код -- по одной строке или до определённого места.
  • +
  • Смотреть переменные, выполнять команды в консоли и т.п.
  • +
+ +В этой главе кратко описаны возможности отладчика Google Chrome, относящиеся именно к работе с кодом. + +Пока что это всё, что нам надо, но, конечно, инструменты разработчика умеют много чего ещё. В частности, вкладка Elements -- позволяет работать со страницей (понадобится позже), Timeline -- смотреть, что именно делает браузер и сколько это у него занимает и т.п. + +Осваивать можно двумя путями: +
    +
  1. [Официальная документация](https://developers.google.com/chrome-developer-tools/docs/overview) (на англ.)
  2. +
  3. Кликать в разных местах и смотреть, что получается. Не забывать о клике правой кнопкой мыши.
  4. +
+ +Мы ещё вернёмся к отладчику позже, когда будем работать с HTML. +[head] + +[/head] \ No newline at end of file diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_break_error.png b/1-js/3-writing-js/1-debugging-chrome/chrome_break_error.png new file mode 100755 index 00000000..e55e7e02 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_break_error.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_break_error@2x.png b/1-js/3-writing-js/1-debugging-chrome/chrome_break_error@2x.png new file mode 100755 index 00000000..96341920 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_break_error@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources.png new file mode 100755 index 00000000..be7df489 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources@2x.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources@2x.png new file mode 100755 index 00000000..effa8673 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break.png new file mode 100755 index 00000000..a6b806ac Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break@2x.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break@2x.png new file mode 100755 index 00000000..35593906 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_break@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint.png new file mode 100755 index 00000000..17ddbab4 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint@2x.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint@2x.png new file mode 100755 index 00000000..e1cce1b2 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_breakpoint@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons.png new file mode 100755 index 00000000..ecfe6d8f Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons@2x.png b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons@2x.png new file mode 100755 index 00000000..8a927d30 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/chrome_sources_buttons@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/console_error.png b/1-js/3-writing-js/1-debugging-chrome/console_error.png new file mode 100755 index 00000000..18346ea6 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/console_error.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/console_error@2x.png b/1-js/3-writing-js/1-debugging-chrome/console_error@2x.png new file mode 100755 index 00000000..1e470740 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/console_error@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage1.png b/1-js/3-writing-js/1-debugging-chrome/manage1.png new file mode 100755 index 00000000..bbc2a6c7 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage1.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage1@2x.png b/1-js/3-writing-js/1-debugging-chrome/manage1@2x.png new file mode 100755 index 00000000..61ffb88f Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage1@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage2.png b/1-js/3-writing-js/1-debugging-chrome/manage2.png new file mode 100755 index 00000000..40abcd45 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage2.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage2@2x.png b/1-js/3-writing-js/1-debugging-chrome/manage2@2x.png new file mode 100755 index 00000000..90977218 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage2@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage3.png b/1-js/3-writing-js/1-debugging-chrome/manage3.png new file mode 100755 index 00000000..5f82f393 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage3.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage3@2x.png b/1-js/3-writing-js/1-debugging-chrome/manage3@2x.png new file mode 100755 index 00000000..1c4b1ac8 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage3@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage4.png b/1-js/3-writing-js/1-debugging-chrome/manage4.png new file mode 100755 index 00000000..6c851d68 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage4.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage4@2x.png b/1-js/3-writing-js/1-debugging-chrome/manage4@2x.png new file mode 100755 index 00000000..24e18a5e Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage4@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage5.png b/1-js/3-writing-js/1-debugging-chrome/manage5.png new file mode 100755 index 00000000..9f30eb2d Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage5.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage5@2x.png b/1-js/3-writing-js/1-debugging-chrome/manage5@2x.png new file mode 100755 index 00000000..ad386c3e Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage5@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage6.png b/1-js/3-writing-js/1-debugging-chrome/manage6.png new file mode 100755 index 00000000..30409411 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage6.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/manage6@2x.png b/1-js/3-writing-js/1-debugging-chrome/manage6@2x.png new file mode 100755 index 00000000..d84cd790 Binary files /dev/null and b/1-js/3-writing-js/1-debugging-chrome/manage6@2x.png differ diff --git a/1-js/3-writing-js/1-debugging-chrome/statusbarButtonGlyphs.svg b/1-js/3-writing-js/1-debugging-chrome/statusbarButtonGlyphs.svg new file mode 100755 index 00000000..6e10e153 --- /dev/null +++ b/1-js/3-writing-js/1-debugging-chrome/statusbarButtonGlyphs.svg @@ -0,0 +1,1735 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ! + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/2-coding-style/1-style-errors/solution.md b/1-js/3-writing-js/2-coding-style/1-style-errors/solution.md new file mode 100644 index 00000000..f23811eb --- /dev/null +++ b/1-js/3-writing-js/2-coding-style/1-style-errors/solution.md @@ -0,0 +1,48 @@ +# Ответ + +Вы могли заметить следующие недостатки, сверху-вниз: + +```js +function pow(x,n) // <- отсутствует пробел между аргументами +{ // <- фигурная скобка на отдельной строке + var result=1; // <- нет пробелов вокруг знака = + for(var i=0;i + +Разберём основные моменты. + +### Фигурные скобки + +Пишутся на той же строке, так называемый "египетский" стиль. Перед скобкой -- пробел. + + + +Если у вас уже есть опыт в разработке и вы привыкли делать скобку на отдельной строке -- это тоже вариант. В конце концов, решать вам. Но в основных JavaScript-фреймворках (jQuery, Dojo, Google Closure Library, Mootools, Ext.JS, YUI...) стиль именно такой. + +Если условие и код достаточно короткие, например `if (cond) return null;`, то запись в одну строку вполне читаема... Но, как правило, отдельная строка всё равно воспринимается лучше. + +### Длина строки + +Максимальную длину строки согласовывают в команде. Как правило, это либо `80`, либо `120` символов, в зависимости от того, какие мониторы у разработчиков. + +Более длинные строки необходимо разбивать. Если этого не сделать, то перевод очень длинной строки сделает редактор, и это может быть менее красиво и читаемо. + +### Отступы + +Отступы нужны двух типов: + +
    +
  • **Горизонтальный отступ, при вложенности -- два(или четыре) пробела.** + +Как правило, используются именно пробелы, т.к. они позволяют сделать более гибкие "конфигурации отступов", чем символ "Tab". + +Например: + +```js +function fib(n) { +*!* + var a = 1; + var b = 1; +*/!* + for (var i = 3; i <= n; i++) { + var c = a + b; + a = b; + b = c; + } + return b; +} +``` + +Кстати, обратите внимание, переменные в выделенном фрагменте объявлены по вертикали, а не в строку `var a=1, b=1`. Так более наглядно, человеческий глаз лучше воспринимает ("сканирует") вертикально выравненную информацию, нежели по горизонтали. Это известный факт среди дизайнеров и нам, программистам, он тоже будет полезен для лучшей организации кода. + +
  • +
  • **Вертикальный отступ, для лучшей разбивки кода -- перевод строки.** + +Используется, чтобы разделить логические блоки внутри одной функции. В примере ниже разделены функция `pow`, получение данных `x,n` и их обработка `if`. + +```js +function pow(x, n) { + return (n != 1) ? pow(x, n-1) : x; +} + // <-- +x = prompt(...); +n = prompt(...); + // <-- +if (n >= 1) { + var result = pow(x, n); + alert(result); +} +``` + +Вставляйте дополнительный перевод строки туда, где это сделает код более читаемым. Не должно быть более 9 строк кода подряд без вертикального отступа. +
  • +
+ +### Точка с запятой + +Точки с запятой нужно ставить, даже если их, казалось бы, можно пропустить. + +Есть языки, в которых точка с запятой не обязательна, и её там никто не ставит. В JavaScript она тоже не обязательна, но ставить нужно. В чём же разница? + +Она в том, что **в JavaScript без точки с запятой возможны трудноуловимые ошибки.** С некоторыми примерами вы встретитесь дальше в учебнике. Такая вот особенность синтаксиса. И поэтому рекомендуется её всегда ставить. + +## Именование + +Общее правило: +
    +
  • Имя переменной -- существительное.
  • +
  • Имя функции -- глагол или начинается с глагола. Бывает, что имена для краткости делают существительными, но глаголы понятнее.
  • +
+ +Для имён используется английский язык (не транслит) и верблюжья нотация. + +Более подробно -- читайте про [имена функций](#function-naming) и [имена переменных](#variable-naming). + +## Уровни вложенности + +Уровней вложенности должно быть немного. + +Например, [проверки в циклах лучше делать через "continue"](#continue), чтобы не было дополнительного уровня `if(..) { ... }`: + +Вместо: + +```js +for (var i=0; i<10; i++) { + if (i подходит) { + ... // <- уровень вложенности 2 + } +} +``` + +Используйте: + +```js +for (var i=0; i<10; i++) { + if (i *!*не*/!* подходит) *!*continue*/!*; + ... // <- уровень вложенности 1 +} +``` + +Аналогичная ситуация -- с `if/else` и `return`. Следующие две конструкции идентичны. + +Первая: + +```js +function isEven(n) { // проверка чётности + if (n % 2 == 0) { + return true; +*!* + } else { + return false; + } +*/!* +} +``` + +Вторая: + +```js +function isEven(n) { // проверка чётности + if (n % 2 == 0) { + return true; + } + +*!* + return false; +*/!* +} +``` + +Если в блоке `if` идёт `return`, то `else` за ним не нужен. + +**Лучше быстро обработать простые случаи, вернуть результат, а дальше разбираться со сложным, без дополнительного уровня вложенности.** + +В случае с функцией `isEven` можно было бы поступить и проще: + +```js +function isEven(n) { // проверка чётности + return n % 2 == 0; +} +``` + +..Казалось бы, можно пойти дальше, есть ещё более короткий вариант: + +```js +function isEven(n) { // проверка чётности + return !(n % 2); +} +``` + +...Однако, код `!(n % 2)` менее очевиден чем `n % 2 == 0`. Поэтому, на самом деле, последний вариант хуже. **Главное для нас -- не краткость кода, а его простота и читаемость.** + +## Функции = Комментарии + +Функции должны быть небольшими. Если функция большая -- желательно разбить её на несколько. + +Этому правилу бывает сложно следовать, но оно стоит того. При чем же здесь комментарии? + +Вызов отдельной небольшой функции не только легче отлаживать и тестировать -- сам факт его наличия является *отличным комментарием*. + +Сравните, например, две функции `showPrimes(n)` для вывода простых чисел до `n`. + +Первый вариант: + +```js +function showPrimes(n) { + nextPrime: + for (var i=2; i +
  • Функции над кодом, который их использует: + +```js +// *!*объявить функции*/!* +function createElement() { + ... +} + +function setHandler(elem) { + ... +} + +function walkAround() { + ... +} + +// *!*код, использующий функции*/!* +var elem = createElement(); +setHandler(elem); +walkAround(); +``` + +
  • +
  • Сначала код, а функции внизу: + +```js +// *!*код, использующий функции*/!* +var elem = createElement(); +setHandler(elem); +walkAround(); + +// --- *!*функции*/!* --- + +function createElement() { + ... +} + +function setHandler(elem) { + ... +} + +function walkAround() { + ... +} +``` + +
  • + + +...На самом деле существует еще третий "стиль", при котором функции хаотично разбросаны по коду ;), но это ведь не наш метод, да? + +**Как правило, лучше располагать функции под кодом, который их использует.** То есть, это 2й способ. + +Дело в том, что при чтении такого кода мы хотим знать в первую очередь, *что он делает*, а уже затем *какие функции ему помогают.* Если первым идёт код, то это как раз дает необходимую информацию. Что же касается функций, то вполне возможно нам и не понадобится их читать, особенно если они названы адекватно и то, что они делают, понятно. + +У первого способа, впрочем, есть то преимущество, что на момент чтения мы уже знаем, какие функции существуют. + +Таким образом, если над названиями функций никто не думает -- может быть, это будет лучшим выбором :). Попробуйте оба варианта, но по моей практике предпочтителен всё же второй. + + +## Комментарии + +В коде нужны комментарии. + +**Как правило, комментарии отвечают на вопрос "что происходит в коде?"** + +Например: +
      +
    • **Архитектурный комментарий -- "как оно, вообще, устроено".** + +Какие компоненты есть, какие технологии использованы, поток взаимодействия. О чём и зачем этот скрипт. Эти комментарии особенно нужны, если вы не один. + +Для описания архитектуры, кстати, создан специальный язык [UML](http://ru.wikipedia.org/wiki/Unified_Modeling_Language), красивые диаграммы, но можно и без этого. Главное -- чтобы понятно. +
    • +
    • **Справочный комментарий перед функцией -- о том, что именно она делает, какие параметры принимает и что возвращает.** + +Для таких комментариев существует синтаксис [JSDoc](http://en.wikipedia.org/wiki/JSDoc). + +```js +/** + * Возвращает x в степени n, только для натуральных n + * + * @param {number} x Число для возведения в степень. + * @param {number} n Показатель степени, натуральное число. + * @return {number} x в степени n. + */ +function pow(x, n) { + ... +} +``` + +Такие комментарии позволяют сразу понять, что принимает и что делает функция, не вникая в код. + +Кстати, они автоматически обрабатываются многими редакторами, например [Aptana](http://aptana.com) и редакторами от [JetBrains](http://www.jetbrains.com/), которые учитывают их при автодополнении. +
    • +
    • **Краткий комментарий, что именно происходит в данном блоке кода.** + +Что интересно, в коде начинающих разработчиков обычно комментариев либо нет, либо они как раз такого типа: "что делается в этих строках кода". + +На самом деле именно эти комментарии, как правило, являются самыми ненужными. Хороший код и так самоочевиден, если не используются особо сложные алгоритмы. + +Об этом замечательно выразился Р. Мартин в книге ["Чистый код"](http://www.ozon.ru/context/detail/id/21916535/): "Если вам кажется, что нужно добавить комментарий для улучшения понимания, это значит, что ваш код не достаточно прост, и, может, стоит переписать его". + +
    • +
    + +**...Но куда более важными могут быть комментарии, которые объясняют не *что*, а *почему* в коде происходит именно это!** + +Как правило, из кода можно понять, что он делает. Бывает, конечно, всякое, но, в конце концов, вы этот код *видите*. Однако гораздо важнее может быть то, чего вы *не видите*! + +*Почему* это сделано именно так? На это сам код ответа не даёт. + +Например: + +
      +
    • **Есть несколько способов решения задачи. Почему выбран именно этот?** + +Например, пробовали решить задачу по-другому, но не получилось -- напишите об этом. Почему вы выбрали именно этот способ решения? Особенно это важно в тех случаях, когда используется не первый приходящий в голову способ, а какой-то другой. + +Без этого возможна, например, такая ситуация: +
        +
      • Вы открываете код, который был написан какое-то время назад, и видите, что он "неоптимален".
      • +
      • Думаете: "Какой я был дурак", и переписываете под "более очевидный и правильный" вариант.
      • +
      • ...Порыв, конечно, хороший, да только этот вариант вы уже обдумали раньше. И отказались, а почему -- забыли. В процессе переписывания вспомнили, конечно (к счастью), но результат - потеря времени на повторное обдумывание.
      • +
      + +Комментарии, которые объясняют поведение кода, очень важны. Они помогают понять происходящее и принять правильное решение о развитии кода. + +
    • +
    • **Какие неочевидные возможности обеспечивает этот код?** Где в другом месте кода они используются? + +В хорошем коде должно быть минимум неочевидного. Но там, где это есть -- пожалуйста, комментируйте. +
    • + +
    + +Один из показателей хорошего разработчика -- качество комментариев, которые позволяют эффективно поддерживать код, возвращаться к нему после любой паузы и легко вносить изменения. + + +## Руководства по стилю + +Когда написанием проекта занимается целая команда, то должен существовать один стандарт кода, описывающий где и когда ставить пробелы, запятые, переносы строк и т.п. + +Сейчас, когда есть столько готовых проектов, нет смысла придумывать целиком своё руководство по стилю. Можно взять уже готовое, и которому, по желанию, всегда можно что-то добавить. + +Большинство есть на английском, сообщите мне, если найдёте хороший перевод: + +
      +
    • [Google JavaScript Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml)
    • +
    • [JQuery Core Style Guidelines](http://docs.jquery.com/JQuery_Core_Style_Guidelines)
    • +
    • [Idiomatic.JS](https://github.com/rwldrn/idiomatic.js) (есть [перевод](https://github.com/rwldrn/idiomatic.js/tree/master/translations/ru_RU))
    • +
    • [Dojo Style Guide](http://dojotoolkit.org/community/styleGuide)
    • +
    + +Для того, чтобы начать разработку, вполне хватит элементов стилей, обозначенных в этой главе. В дальнейшем, посмотрите на эти руководства, найдите "свой" стиль ;) + +### Автоматизированные средства проверки + +Существуют онлайн-сервисы, проверяющие стиль кода. + +Самые известные -- это: + +
      +
    • [JSLint](http://www.jslint.com/) -- проверяет код на соответствие [стилю JSLint](http://www.jslint.com/lint.html), в онлайн-интерфейсе вверху можно ввести код, а внизу различные настройки проверки, чтобы сделать её более мягкой.
    • +
    • [JSHint](http://www.jshint.com/) -- ещё один вариант JSLint, ослабляющий требования в ряде мест.
    • +
    • [Closure Linter](https://developers.google.com/closure/utilities/) -- проверка на соответствие [Google JavaScript Style Guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml).
    • +
    + +Все они также доступны в виде программ, которые можно скачать. + +## Итого + +Описанные принципы оформления кода уместны в большинстве проектов. Есть и другие полезные соглашения. + +Следуя (или не следуя) им, необходимо помнить, что любые советы по стилю хороши лишь тогда, когда делают код читаемее, понятнее, проще в поддержке. + diff --git a/1-js/3-writing-js/2-coding-style/cheatsheet.png b/1-js/3-writing-js/2-coding-style/cheatsheet.png new file mode 100755 index 00000000..ea150473 Binary files /dev/null and b/1-js/3-writing-js/2-coding-style/cheatsheet.png differ diff --git a/1-js/3-writing-js/2-coding-style/figure.png b/1-js/3-writing-js/2-coding-style/figure.png new file mode 100755 index 00000000..5d2f1167 Binary files /dev/null and b/1-js/3-writing-js/2-coding-style/figure.png differ diff --git a/1-js/3-writing-js/3-write-unmain-code/article.md b/1-js/3-writing-js/3-write-unmain-code/article.md new file mode 100644 index 00000000..9bee2b50 --- /dev/null +++ b/1-js/3-writing-js/3-write-unmain-code/article.md @@ -0,0 +1,332 @@ +# Как писать неподдерживаемый код? + +Предлагаю вашему вниманию советы мастеров древности, следование которым создаст дополнительные рабочие места для JavaScript-разработчиков. + +Если вы будете им следовать, то ваш код будет так сложен в поддержке, что у JavaScript'еров, которые придут после вас, даже простейшее изменение займет годы *оплачиваемого* труда! А сложные задачи оплачиваются хорошо, так что они, определённо, скажут вам "Спасибо". + +Более того, *внимательно* следуя этим правилам, вы сохраните и своё рабочее место, так как все будут бояться вашего кода и бежать от него... + +...Впрочем, всему своя мера. При написании такого кода он не должен *выглядеть* сложным в поддержке, код должен *быть* таковым. Явно кривой код может написать любой дурак. Это заметят, и вас уволят, а код будет переписан с нуля. Вы не можете такого допустить. Эти советы учитывают такую возможность. Да здравствует дзен. + +Статья представляет собой мой вольный перевод [How To Write Unmaintainable Code](http://mindprod.com/jgloss/unmain.html) с дополнениями, актуальными для JavaScript. + +[cut] + +## Соглашения + +[quote author="Сериал \"Симпсоны\", серия Helter Shelter"] +Рабочий-чистильщик осматривает дом:
    +"...Вот только жук у вас необычный...
    +И чтобы с ним справиться, я должен жить как жук, стать жуком, думать как жук."
    +(грызёт стол Симпсонов) +[/quote] + +Чтобы помешать другому программисту исправить ваш код, вы должны понять путь его мыслей. + +Представьте, перед ним -- ваш большой скрипт. И ему нужно поправить его. У него нет ни времени ни желания, чтобы читать его целиком, а тем более -- досконально разбирать. Он хотел бы по-быстрому найти нужное место, сделать изменение и убраться восвояси без появления побочных эффектов. + +Он рассматривает ваш код как бы через трубочку из туалетной бумаги. Это не даёт ему общей картины, он ищет тот небольшой фрагмент, который ему необходимо изменить. По крайней мере, он надеется, что этот фрагмент будет небольшим. + +**На что он попытается опереться в этом поиске -- так это на соглашения, принятые в программировании, об именах переменных, названиях функций и методов...** + +Как затруднить задачу? Можно везде нарушать соглашения -- это помешает ему, но такое могут заметить, и код будет переписан. Как поступил бы ниндзя на вашем месте? + +**...Правильно! Следуйте соглашениям "в общем", но иногда -- нарушайте их.** Тщательно разбросанные по коду нарушения соглашений с одной стороны не делают код явно плохим при первом взгляде, а с другой -- имеют в точности тот же, и даже лучший эффект, чем явное неследование им! + +Если пример, который я приведу ниже, пока сложноват -- пропустите его, но обязательно вернитесь к нему позже. Поверьте, это стоит того. + +Во фреймворке jQuery есть метод [wrap](http://api.jquery.com/wrap/), который обёртывает один элемент вокруг другого: + +```js +var img = $(''); // создали новые элементы (jQuery-синтаксис) +var div = $('
    '); // и поместили в переменную + +*!* +img.wrap(div); // обернуть img в div +*/!* +``` + +Результат кода выше -- два элемента, один вложен в другой: + +```html +
    + +
    +``` + +(`div` обернулся вокруг `img`) + +А теперь, когда все расслабились и насладились этим замечательным методом... + +...Самое время ниндзя нанести свой удар! + +**Как вы думаете, что будет, если добавить к коду выше строку:** + +```js +//+ lines first-line=5 +div.append(''); +``` + +[smart header="jQuery-справка"] +Вызов `elemA.append(elemB)` добавляет `elemB` в конец содержимого элемента `elemA`. +[/smart] + +**Возможно, вы полагаете, что `` добавится в конец `div`, сразу после `img`?** + +А вот и нет! А вот и нет!.. + +Оказывается, внутри вызова `img.wrap(div)` происходит *клонирование* `div`. И вокруг `img` оборачивается не сам `div`, а его злой клон. + +При этом исходная переменная `div` не меняется, в ней как был пустой `div`, так и остался. В итоге, после применения к нему `append` получается два `div'а`: один обёрнут вокруг `span`, а в другом -- только `img`. + + + + + + + + + + + + +
    Переменная `div`Клон `div`, созданный `wrap` + (не присвоен никакой переменной)
    + +```html +
    + +
    +``` + +
    + +```html +
    + +
    +``` + +
    + +Странно? Неочевидно? Да, и не только вам :) + +Соглашение в данном случае -- в том, что большинство методов jQuery не клонируют элементы. А вызов `wrap` -- клонирует. + +Код его истинный ниндзя писал! + +## Краткость -- сестра таланта! + +Пишите "как короче", а не как понятнее. "Меньше букв" -- уважительная причина для нарушения любых соглашений. + +Ваш верный помощник -- возможности языка, использованные неочевидным образом. + +Обратите внимание на оператор вопросительный знак `'?'`, например: + +```js +// код из jQuery +i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; +``` + +Разработчик, встретивший эту строку и попытавшийся понять, чему же всё-таки равно `i`, скорее всего придёт к вам за разъяснениями. Смело скажите ему, что короче -- это всегда лучше. Посвятите и его в пути ниндзя. Не забудьте вручить [Дао дэ цзин](http://lib.ru/POECHIN/lao1.txt). + +## Именование + +Существенную часть науки о создании неподдерживаемого кода занимает искусство выбора имён. + +### Однобуквенные переменные + +Называйте переменные коротко: `a`, `b` или `c`. + +В этом случае никто не сможет найти её, используя фунцию "Поиск" текстового редактора. + +Более того, даже найдя -- никто не сможет "расшифровать" её и догадаться, что она означает. + +### Не используйте i для цикла + +В тех местах, где однобуквенные переменные общеприняты, например, в счетчике цикла -- ни в коем случае не используйте стандартные названия `i`, `j`, `k`. Где угодно, только не здесь! + +Остановите свой взыскательный взгляд на чём-нибудь более экзотическом. Например, `x` или `y`. + +Эффективность этого подхода особенно заметна, если тело цикла занимает одну-две страницы. + +В этом случае заметить, что переменная -- счетчик цикла, без пролистывания вверх, невозможно. + +### Русские слова и сокращения + +Если вам *приходится* использовать длинные, понятные имена переменных -- что поделать.. Но и здесь есть простор для творчества! + +**Назовите переменные "калькой" с русского языка или как-то "улучшите" английское слово.** + +В одном месте напишите `var ssilka`, в другом `var ssylka`, в третьем `var link`, в четвёртом -- `var lnk`... Это действительно великолепно работает и очень креативно! + +Количество ошибок при поддержке такого кода увеличивается во много раз. + +### Будьте абстрактны при выборе имени + +[quote author="Лао-цзы"]Лучший кувшин лепят всю жизнь.
    +Высокая музыка неподвластна слуху.
    +Великий образ не имеет формы.[/quote] + +При выборе имени старайтесь применить максимально абстрактное слово, например `obj`, `data`, `value`, `item`, `elem` и т.п. + +
      +
    • **Идеальное имя для переменной: `data`.** Используйте это имя везде, где можно. В конце концов, каждая переменная содержит *данные*, не правда ли? + +Но что делать, если имя `data` уже занято? Попробуйте `value`, оно не менее универсально. Ведь каждая переменная содержит *значение*. + +Занято и это? Есть и другой вариант. +
    • +
    • **Называйте переменную по типу данных, которые она хранит: `obj`, `num`, `arr`...** + +Насколько это усложнит разработку? Как ни странно, намного! + +Казалось бы, название переменной содержит информацию, говорит о том, что в переменной -- число, объект или массив... С другой стороны, **когда непосвящённый будет разбирать этот код -- он с удивлением обнаружит, что информации нет!** + +Ведь как раз тип легко понять, запустив отладчик и посмотрев, что внутри. Но в чём смысл этой переменной? Что за массив/объект/число в ней хранится? Без долгой медитации над кодом тут не обойтись! +
    • +
    • **Что делать, если и эти имена кончились? Просто добавьте цифру:** `item1, item2, elem5, data1`...
    • +
    + +### Похожие имена + +Только истинно внимательный программист достоин понять ваш код. Но как проверить, достоин ли читающий? + +**Один из способов -- использовать похожие имена переменных, например `data` и `date`.** Бегло прочитать такой код почти невозможно. А уж заметить опечатку и поправить её... Ммммм... Мы здесь надолго, время попить чайку. + +### А.К.Р.О.Н.И.М + +Используйте сокращения, чтобы сделать код короче. + +Например `ie` (Inner Element), `mc` (Money Counter) и другие. Если вы обнаружите, что путаетесь в них сами -- героически страдайте, но не переписывайте код. Вы знали, на что шли. + +### Хитрые синонимы + +[quote author="Конфуций"]Очень трудно найти чёрную кошку в тёмной комнате, особенно когда её там нет.[/quote] + +**Чтобы было не скучно -- используйте *похожие названия* для обозначения *одинаковых действий*.** + +Например, если метод показывает что-то на экране -- начните его название с `display..` (скажем, `displayElement`), а в другом месте объявите аналогичный метод как `show..` (`showFrame`). + +**Как бы намекните этим, что существует тонкое различие между способами показа в этих методах, хотя на самом деле его нет.** + +По возможности, договоритесь с членами своей команды. Если Вася в своих классах использует `display..`, то Валера -- обязательно `render..`, а Петя -- `paint..`. + +**...И напротив, если есть две функции с важными отличиями -- используйте одно и то же слово для их описания!** Например, с `print...` можно начать метод печати на принтере `printPage`, а также -- метод добавления текста на страницу `printText`. + +А теперь, пусть читающий код думает: "Куда же выводит сообщение `printMessage`?". Особый шик -- добавить элемент неожиданности. Пусть `printMessage` выводит не туда, куда все, а в новое окно! + +### Словарь терминов -- это еда! + +Ни в коем случае не поддавайтесь требованиям написать словарь терминов для проекта. Если же он уже есть -- не следуйте ему, а лучше проглотите и скажите, что так и былО! + +Пусть читающий ваш код программист напрасно ищет различия в `helloUser` и `welcomeVisitor` и пытается понять, когда что использовать. Вы-то знаете, что на самом деле различий нет, но искать их можно о-очень долго. + +**Для обозначения посетителя в одном месте используйте `user`, а в другом `visitor`, в третьем -- просто `u`. Выбирайте одно имя или другое, в зависимости от функции и настроения.** + +Это воплотит сразу два ключевых принципа ниндзя-дизайна -- *сокрытие информации* и *подмена понятий*! + +### Повторно используйте имена + +По возможности, повторно используйте имена переменных, функций и свойств. Просто записывайте в них новые значения. + +Добавляйте новое имя только если это абсолютно необходимо. + +В функции старайтесь обойтись только теми переменными, которые были переданы как параметры. + +Это не только затруднит идентификацию того, что *сейчас* находится в переменной, но и сделает почти невозможным поиск места, в котором конкретное значение было присвоено. + +Цель -- максимально усложнить отладку и заставить читающего код программиста построчно анализировать код и конспектировать изменения переменных для каждой ветки исполнения. + +**Продвинутый вариант этого подхода -- незаметно (!) подменить переменную на нечто похожее, например:** + +```js +function ninjaFunction(elem) { + // 20 строк кода, работающего с elem + + elem = elem.cloneNode(true); + + // еще 20 строк кода, работающего с elem +} +``` + +Программист, пожелавший добавить действия с `elem` во вторую часть функции, будет удивлён. Лишь во время отладки, посмотрев весь код, он с удивлением обнаружит, что оказывается имел дело с клоном! + +Регулярные встречи с этим приемом на практике говорят: защититься невозможно. Эффективно даже против опытного ниндзи. + +### Добавляйте подчеркивания + +Добавляйте подчеркивания `_` и `__` к именам переменных. Желательно, чтобы их смысл был известен только вам, а лучше -- вообще без явной причины. + +Этим вы достигните двух целей. Во-первых, код станет длиннее и менее читаемым, а во-вторых, другой программист будет долго искать смысл в подчёркиваниях. Особенно хорошо сработает и внесет сумятицу в его мысли, если в некоторых частях проекта подчеркивания будут, а в некоторых -- нет. + +В процессе развития кода вы, скорее всего, будете путаться и смешивать стили: добавлять имена с подчеркиваниями там, где обычно подчеркиваний нет, и наоборот. Это нормально и полностью соответствует третьей цели -- увеличить количество ошибок при внесении исправлений. + +### Покажите вашу любовь к разработке + +Пусть все видят, какими замечательными сущностями вы оперируете! Имена `superElement`, `megaFrame` и `niceItem` при благоприятном положении звёзд могут привести к просветлению читающего. + +Действительно, с одной стороны, кое-что написано: `super..`, `mega..`, `nice..` С другой -- это не несёт никакой конкретики. Читающий может решить поискать в этом глубинный смысл и замедитировать на часок-другой оплаченного рабочего времени. + +### Перекрывайте внешние переменные + +[quote author="Гуань Инь-цзы"] +Находясь на свету, нельзя ничего увидеть в темноте.
    +Пребывая же в темноте, увидишь все, что находится на свету. +[/quote] + +Почему бы не использовать одинаковые переменные внутри и снаружи функции? Это просто и не требует придумывать новых имён. + +```js +var *!*user*/!* = authenticateUser(); + +function render() { + var *!*user*/!* = ... + ... + ...многобукв... + ... + ... // <-- программист захочет внести исправления сюда, и.. + ... +} +``` + +Зашедший в середину метода `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(..))`, будут весьма удивлены результатом. Ответьте им: "надо читать документацию!". И перешлите эту статью. + +## Заключение + +Все советы выше пришли из реального кода... И в том числе от разработчиков с большим опытом. + +Возможно, даже больше вашего, так что не судите опрометчиво ;) + +
      +
    • Следуйте нескольким из них -- и ваш код станет полон сюрпризов.
    • +
    • Следуйте многим -- и ваш код станет истинно вашим, никто не захочет изменять его.
    • +
    • Следуйте всем -- и ваш код станет ценным уроком для молодых разработчиков, ищущих просветления.
    • +
    \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/solution.js b/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/solution.js new file mode 100644 index 00000000..ba86c0d8 --- /dev/null +++ b/1-js/3-writing-js/4-testing/1-pow-nan-spec/_js.view/solution.js @@ -0,0 +1,10 @@ +function pow(x, n) { + if (n < 0) return NaN; + if (Math.round(n) != n) return NaN; + + var result = 1; + for(var i=0; i +``` + +Этот файл содержит код библиотек, стили, настройки для них и запуск `mocha.run` по окончании загрузки страницы. Если нет элемента с `id="mocha"`, то результаты выводятся в ``. + +Сборка сделана исключительно для более компактного представления задач, без рекомендаций использовать именно её в проектах. diff --git a/1-js/3-writing-js/4-testing/2-pow-test-0/_js.view/solution.js b/1-js/3-writing-js/4-testing/2-pow-test-0/_js.view/solution.js new file mode 100644 index 00000000..dbc8d388 --- /dev/null +++ b/1-js/3-writing-js/4-testing/2-pow-test-0/_js.view/solution.js @@ -0,0 +1,11 @@ +function pow(x, n) { + if (n < 0) return NaN; + if (Math.round(n) != n) return NaN; + if (n == 0 && x ==0) return NaN; + + var result = 1; + for(var i=0; i0 не определён). + +При необходимости, исправьте реализацию, чтобы тесты проходили без ошибок. \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/3-pow-test-wrong/solution.md b/1-js/3-writing-js/4-testing/3-pow-test-wrong/solution.md new file mode 100644 index 00000000..46b37ff6 --- /dev/null +++ b/1-js/3-writing-js/4-testing/3-pow-test-wrong/solution.md @@ -0,0 +1,27 @@ +Этот тест демонстрирует один из соблазнов, которые ожидают начинающего автора тестов. + +Вместо того, чтобы написать три различных теста, он изложил их в виде одного потока вычислений, с несколькими `assert`. + +Иногда так написать легче и проще, однако при ошибке в тесте гораздо менее очевидно, что же пошло не так. + +Если в сложном тесте произошла ошибка где-то посередине потока вычислений, то придётся выяснять, какие конкретно были входные и выходные данные на этот момент, то есть по сути -- отлаживать код самого теста. + +Гораздо лучше будет разбить тест на несколько блоков `it`, с чётко прописанными входными и выходными данными. + +```js +describe("Возводит x в степень n", function() { + it("5 в степени 1 равно 5", function() { + assert.equal( pow(5, 1), 5 ); + }); + + it("5 в степени 2 равно 25", function() { + assert.equal( pow(5, 2), 25 ); + }); + + it("5 в степени 3 равно 125", function() { + assert.equal( pow(5, 3), 25 ); + }); +}); +``` + +Можно использовать цикл для генерации блоков `it`, в этом случае важно, чтобы сам код такого цикла был достаточно простым. Иногда проще записать несколько блоков `it` вручную, как сделано выше, чем "городить огород" из синтаксических конструкций. diff --git a/1-js/3-writing-js/4-testing/3-pow-test-wrong/task.md b/1-js/3-writing-js/4-testing/3-pow-test-wrong/task.md new file mode 100644 index 00000000..febf59f2 --- /dev/null +++ b/1-js/3-writing-js/4-testing/3-pow-test-wrong/task.md @@ -0,0 +1,22 @@ +# Что не так в тесте? + +[importance 5] + +Что не так в этом тесте функции `pow`? + +```js +it("Возводит x в степень n", function() { + var x = 5; + + var result = x; + assert.equal( pow(x, 1), result ); + + var result *= x; + assert.equal( pow(x, 2), result ); + + var result *= x; + assert.equal( pow(x, 3), result ); +}); +``` + +P.S. Синтаксически он верен и работает, но спроектирован неправильно. \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/article.md b/1-js/3-writing-js/4-testing/article.md new file mode 100644 index 00000000..164607ab --- /dev/null +++ b/1-js/3-writing-js/4-testing/article.md @@ -0,0 +1,447 @@ +# Автоматические тесты при помощи chai и mocha + +В этой главе мы разберём основы автоматического тестирования. Оно будет применяться далее в задачах, и вообще, входит в "образовательный минимум" программиста. + +[cut] + +## Зачем нужны тесты? + +При написании функции мы обычно представляем, что она должна делать, какое значение -- на каких аргументах выдавать. + +В процессе разработки мы, время от времени, проверяем функцию. Самый простой способ проверки -- это запустить функцию и посмотреть результат. + +Потом написать ещё код, попробовать запустить -- опять посмотреть результат. + +И так -- "до победного конца". + +К сожалению, такие ручные запуски -- очень несовершенное средство проверки. + +**Когда проверяешь работу кода вручную -- легко его "недотестировать".** + +Например, пишем функцию `f`. Написали, тестируем с разными аргументами. Вызов функции `f(a)` -- работает, а вот `f(b)` -- не работает. Поправили код -- стало работать `f(b)`, вроде закончили. Но при этом забыли заново протестировать `f(a)` -- упс, вот и возможная ошибка в коде. + +**Автоматизированное тестирование -- это когда тесты написаны отдельно от кода, и можно в любой момент запустить их и проверить все важные случаи использования.** + +## BDD -- поведенческие тесты кода + +Мы рассмотрим методику тестирования, которая входит в [BDD](http://en.wikipedia.org/wiki/Behavior-driven_development) -- Behavior Driven Development. Подход BDD давно и с успехом используется во многих проектах. + +BDD -- это не просто тесты. Это гораздо больше. + +**Тесты BDD -- это три в одном: это И тесты И документация И примеры использования одновременно.** + +Впрочем, хватит слов. Рассмотрим примеры. + +## Разработка pow + +Допустим, мы хотим разработать функцию `pow(x, n)`, которая возводит `x` в целую степень `n`, для простоты `n≥0`. + +### Спецификация + +Ещё до разработки мы можем представить себе, что эта функция будет делать и описать это по методике BDD. + +Это описание называется *спецификация* (или, как говорят в обиходе, "спека") и выглядит так: + +```js +describe("pow", function() { + + it("возводит в n-ю степень", function() { + assert.equal( pow(2, 3), 8); + }); + +}); +``` + +У спецификации есть три основных строительных блока, которые вы видите в примере выше: +
    +
    `describe(название, function() { ... })`
    +
    Задаёт, что именно мы описываем, используется для группировки "рабочих лошадок" -- блоков `it`. В данном случае мы описываем функцию `pow`.
    +
    `it(название, function() { ... })`
    +
    В названии блока `it` *человеческим языком* описывается, что должна делать функция, далее следует *тест*, который проверяет это.
    +
    `assert.equal(value1, value2)`
    +
    Код внутри `it`, если реализация верна, должен выполняться без ошибок. + +Для того, чтобы проверить, делает ли `pow` то, что задумано, используются функции вида `assert.*`. Пока что нас интересует только одна из них -- `assert.equal`, она сравнивает свой первый аргумент со вторым и выдаёт ошибку в случае, когда они не равны. Есть и другие виды сравнений и проверок, которые мы увидим далее.
    +
    + +Как правило, поток разработки таков: +
      +
    1. Пишется спецификация, которая описывает самый базовый функционал.
    2. +
    3. Делается начальная реализация.
    4. +
    5. Для проверки соответствия спецификации мы задействуем одновременно фреймворк, в нашем случае [Mocha](http://visionmedia.github.io/mocha/) вместе со спецификацией и реализацией. Фреймворк запускает все тесты `it` и выводит ошибки, если они возникнут. При ошибках вносятся исправления.
    6. +
    7. Спецификация расширяется, в неё добавляются возможности, которые пока, возможно, не поддерживаются реализацией.
    8. +
    9. Идём на пункт 3, делаем реализацию, и так далее, до победного конца.
    10. +
    + +Разработка ведётся *итеративно*, один проход за другим, пока спецификация и реализация не будут завершены. + +В нашем случае первый шаг уже завершён, начальная спецификация готова, хорошо бы приступить к реализации. Но перед этим проведём "нулевой" запуск спецификации, просто чтобы увидеть, что уже в таком виде, даже без реализации -- тесты работают. + +### Проверка спецификации + +Для запуска тестов нужны соответствующие JavaScript-библиотеки. + +Мы будем использовать: +
      +
    • [Mocha](http://visionmedia.github.io/mocha/) -- эта библиотека содержит общие функции для тестирования, включая `describe` и `it`.
    • +
    • [Chai](http://chaijs.com) -- библиотека поддерживает разнообразные функции для проверок. Есть разные "стили" проверки результатов, с которыми мы познакомимся позже, на текущий момент мы будем использовать лишь `assert.equal`.
    • +
    • [Sinon](http://sinonjs.org/) -- для эмуляции и хитрой подмены функций "заглушками", понадобится позднее.
    • +
    + +Эти библиотеки позволяют тестировать JS не только в браузере, но и на сервере Node.JS. Здесь мы рассмотрим браузерный вариант, серверный использует те же функции. + +Пример HTML-страницы для тестов: + +```html + +``` + +Эту страницу можно условно разделить на три части: +
      +
    1. В `` подключаем библиотеки и стили.
    2. +
    3. Подключаем ` + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/beforeafter.view/test.js b/1-js/3-writing-js/4-testing/beforeafter.view/test.js new file mode 100755 index 00000000..f55c0781 --- /dev/null +++ b/1-js/3-writing-js/4-testing/beforeafter.view/test.js @@ -0,0 +1,12 @@ +describe("Тест", function() { + + before(function() { alert("Начало тестов"); }); + after(function() { alert("Конец тестов"); }); + + beforeEach(function() { alert("Вход в тест"); }); + afterEach(function() { alert("Выход из теста"); }); + + it('тест 1', function() { alert('1'); }); + it('тест 2', function() { alert('2'); }); + +}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/index.html b/1-js/3-writing-js/4-testing/index.html new file mode 100755 index 00000000..b53b160a --- /dev/null +++ b/1-js/3-writing-js/4-testing/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-1.view/index.html b/1-js/3-writing-js/4-testing/pow-1.view/index.html new file mode 100755 index 00000000..b53b160a --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-1.view/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-1.view/test.js b/1-js/3-writing-js/4-testing/pow-1.view/test.js new file mode 100755 index 00000000..1513b594 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-1.view/test.js @@ -0,0 +1,7 @@ +describe("pow", function() { + + it("возводит в n-ю степень", function() { + assert.equal( pow(2, 3), 8); + }); + +}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-2.view/index.html b/1-js/3-writing-js/4-testing/pow-2.view/index.html new file mode 100755 index 00000000..1bd90a55 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-2.view/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-2.view/test.js b/1-js/3-writing-js/4-testing/pow-2.view/test.js new file mode 100755 index 00000000..3323af1f --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-2.view/test.js @@ -0,0 +1,11 @@ +describe("pow", function() { + + it("при возведении 2 в 3ю степень результат 8", function() { + assert.equal( pow(2, 3), 8); + }); + + it("при возведении 3 в 4ю степень равен 81", function() { + assert.equal( pow(3, 4), 81); + }); + +}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-3.view/index.html b/1-js/3-writing-js/4-testing/pow-3.view/index.html new file mode 100755 index 00000000..6bca4917 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-3.view/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-3.view/test.js b/1-js/3-writing-js/4-testing/pow-3.view/test.js new file mode 100755 index 00000000..b1aca005 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-3.view/test.js @@ -0,0 +1,14 @@ +describe("pow", function() { + + function makeTest(x) { + var expected = x*x*x; + it("при возведении "+x+" в степень 3 результат: " + expected, function() { + assert.equal( pow(x, 3), expected); + }); + } + + for(var x = 1; x <= 5; x++) { + makeTest(x); + } + +}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-4.view/index.html b/1-js/3-writing-js/4-testing/pow-4.view/index.html new file mode 100755 index 00000000..6bca4917 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-4.view/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-4.view/test.js b/1-js/3-writing-js/4-testing/pow-4.view/test.js new file mode 100755 index 00000000..06e6997b --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-4.view/test.js @@ -0,0 +1,20 @@ +describe("pow", function() { + + describe("возводит x в степень n", function() { + + function makeTest(x) { + var expected = x*x*x; + it("при возведении "+x+" в степень 3 результат: " + expected, function() { + assert.equal( pow(x, 3), expected); + }); + } + + for(var x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + // ... + +}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-min.view/index.html b/1-js/3-writing-js/4-testing/pow-min.view/index.html new file mode 100755 index 00000000..1bd90a55 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-min.view/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-min.view/test.js b/1-js/3-writing-js/4-testing/pow-min.view/test.js new file mode 100755 index 00000000..1513b594 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-min.view/test.js @@ -0,0 +1,7 @@ +describe("pow", function() { + + it("возводит в n-ю степень", function() { + assert.equal( pow(2, 3), 8); + }); + +}); \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan-assert.view/index.html b/1-js/3-writing-js/4-testing/pow-nan-assert.view/index.html new file mode 100755 index 00000000..e35012d5 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-nan-assert.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan-assert.view/test.js b/1-js/3-writing-js/4-testing/pow-nan-assert.view/test.js new file mode 100755 index 00000000..d65b4e6e --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-nan-assert.view/test.js @@ -0,0 +1,26 @@ +describe("pow", function() { + + describe("возводит x в степень n", function() { + + function makeTest(x) { + var expected = x*x*x; + it("при возведении "+x+" в степень 3 результат: " + expected, function() { + assert.equal( pow(x, 3), expected); + }); + } + + for(var x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + it("при возведении в отрицательную степень результат NaN", function() { + assert( isNaN( pow(2, -1) ), "pow(2, -1) не NaN" ); + }); + + it("при возведении в дробную степень результат NaN", function() { + assert( isNaN( pow(2, 1.5) ), "pow(2, -1.5) не NaN" ); + }); + +}); diff --git a/1-js/3-writing-js/4-testing/pow-nan.view/index.html b/1-js/3-writing-js/4-testing/pow-nan.view/index.html new file mode 100755 index 00000000..e35012d5 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-nan.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testing/pow-nan.view/test.js b/1-js/3-writing-js/4-testing/pow-nan.view/test.js new file mode 100755 index 00000000..ce885be6 --- /dev/null +++ b/1-js/3-writing-js/4-testing/pow-nan.view/test.js @@ -0,0 +1,26 @@ +describe("pow", function() { + + describe("возводит x в степень n", function() { + + function makeTest(x) { + var expected = x*x*x; + it("при возведении "+x+" в степень 3 результат: " + expected, function() { + assert.equal( pow(x, 3), expected); + }); + } + + for(var x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + it("при возведении в отрицательную степень результат NaN", function() { + assert( isNaN( pow(2, -1) ) ); + }); + + it("при возведении в дробную степень результат NaN", function() { + assert( isNaN( pow(2, 1.5) ) ); + }); + +}); diff --git a/1-js/3-writing-js/4-testingpow-full/pow-full.view/index.html b/1-js/3-writing-js/4-testingpow-full/pow-full.view/index.html new file mode 100755 index 00000000..f42a4ec7 --- /dev/null +++ b/1-js/3-writing-js/4-testingpow-full/pow-full.view/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + \ No newline at end of file diff --git a/1-js/3-writing-js/4-testingpow-full/pow-full.view/test.js b/1-js/3-writing-js/4-testingpow-full/pow-full.view/test.js new file mode 100755 index 00000000..158ddfe3 --- /dev/null +++ b/1-js/3-writing-js/4-testingpow-full/pow-full.view/test.js @@ -0,0 +1,44 @@ +describe("pow", function() { + + describe("возводит x в степень n", function() { + + function makeTest(x) { + var expected = x*x*x; + it("при возведении "+x+" в степень 3 результат: " + expected, function() { + assert.equal( pow(x, 3), expected); + }); + } + + for(var x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + it("при возведении в отрицательную степень результат NaN", function() { + assert( isNaN( pow(2, -1) ), "pow(2, -1) не NaN" ); + }); + + it("при возведении в дробную степень результат NaN", function() { + assert( isNaN( pow(2, 1.5) ), "pow(2, -1.5) не NaN" ); + }); + + describe("любое число, кроме нуля, в степени 0 равно 1", function() { + + function makeTest(x) { + it("при возведении " + x + " в степень 0 результат: 1", function() { + assert.equal( pow(x, 0), 1); + }); + } + + for(var x = -5; x <= 5; x+=2) { + makeTest(x); + } + + }); + + it("ноль в нулевой степени даёт NaN", function() { + assert( isNaN( pow(0,0) ), "0 в степени 0 не NaN"); + }); + +}); diff --git a/1-js/3-writing-js/4-testingpow-full/test.js b/1-js/3-writing-js/4-testingpow-full/test.js new file mode 100755 index 00000000..158ddfe3 --- /dev/null +++ b/1-js/3-writing-js/4-testingpow-full/test.js @@ -0,0 +1,44 @@ +describe("pow", function() { + + describe("возводит x в степень n", function() { + + function makeTest(x) { + var expected = x*x*x; + it("при возведении "+x+" в степень 3 результат: " + expected, function() { + assert.equal( pow(x, 3), expected); + }); + } + + for(var x = 1; x <= 5; x++) { + makeTest(x); + } + + }); + + it("при возведении в отрицательную степень результат NaN", function() { + assert( isNaN( pow(2, -1) ), "pow(2, -1) не NaN" ); + }); + + it("при возведении в дробную степень результат NaN", function() { + assert( isNaN( pow(2, 1.5) ), "pow(2, -1.5) не NaN" ); + }); + + describe("любое число, кроме нуля, в степени 0 равно 1", function() { + + function makeTest(x) { + it("при возведении " + x + " в степень 0 результат: 1", function() { + assert.equal( pow(x, 0), 1); + }); + } + + for(var x = -5; x <= 5; x+=2) { + makeTest(x); + } + + }); + + it("ноль в нулевой степени даёт NaN", function() { + assert( isNaN( pow(0,0) ), "0 в степени 0 не NaN"); + }); + +}); diff --git a/1-js/3-writing-js/index.md b/1-js/3-writing-js/index.md new file mode 100644 index 00000000..96320c44 --- /dev/null +++ b/1-js/3-writing-js/index.md @@ -0,0 +1,8 @@ +# Качество кода + +Для того, чтобы код был качественным, необходимы как минимум: +
        +
      1. Умение отладить код и поправить ошибки.
      2. +
      3. Хороший стиль кода.
      4. +
      5. Тестировать код, желательно -- в автоматическом режиме.
      6. +
      \ No newline at end of file diff --git a/1-js/4-data-structures/1-string/1-ucfirst/_js.view/solution.js b/1-js/4-data-structures/1-string/1-ucfirst/_js.view/solution.js new file mode 100644 index 00000000..5f8c8226 --- /dev/null +++ b/1-js/4-data-structures/1-string/1-ucfirst/_js.view/solution.js @@ -0,0 +1,9 @@ +function ucFirst(str) { + var newStr = str.charAt(0).toUpperCase(); + + for(var i=1; i maxlength) ? + str.slice(0, maxlength - 3) + '...' : str; +} \ No newline at end of file diff --git a/1-js/4-data-structures/1-string/3-truncate/_js.view/test.js b/1-js/4-data-structures/1-string/3-truncate/_js.view/test.js new file mode 100644 index 00000000..8c173a49 --- /dev/null +++ b/1-js/4-data-structures/1-string/3-truncate/_js.view/test.js @@ -0,0 +1,16 @@ +describe("truncate", function() { + it("обрезает строку до указанной длины (включая троеточие)", function() { + assert.equal( + truncate("Вот, что мне хотелось бы сказать на эту тему:", 20), + "Вот, что мне хоте..." + ); + }); + + it("не меняет короткие строки", function() { + assert.equal( + truncate("Всем привет!", 20), + "Всем привет!" + ); + }); + +}); \ No newline at end of file diff --git a/1-js/4-data-structures/1-string/3-truncate/solution.md b/1-js/4-data-structures/1-string/3-truncate/solution.md new file mode 100644 index 00000000..06e56ada --- /dev/null +++ b/1-js/4-data-structures/1-string/3-truncate/solution.md @@ -0,0 +1,28 @@ +Так как окончательная длина строки должна быть `maxlength`, то нужно её обрезать немного короче, чтобы дать место для троеточия. + +```js +//+ run +function truncate(str, maxlength) { + if (str.length > maxlength) { + return str.slice(0, maxlength - 3) + '...'; + // итоговая длина равна maxlength + } + + return str; +} + +alert(truncate("Вот, что мне хотелось бы сказать на эту тему:", 20)); +alert(truncate("Всем привет!", 20)); +``` + +Можно было бы написать этот код ещё короче: + +```js +//+ run +function truncate(str, maxlength) { + return (str.length > maxlength) ? + str.slice(0, maxlength - 3) + '...' : str; +} +``` + +P.S. Кстати, в кодироке Unicode существует специальный символ "троеточие": `…` (HTML: `…`), который можно использовать вместо трёх точек. Если его использовать, то можно отрезать только один символ. diff --git a/1-js/4-data-structures/1-string/3-truncate/task.md b/1-js/4-data-structures/1-string/3-truncate/task.md new file mode 100644 index 00000000..099b9e3f --- /dev/null +++ b/1-js/4-data-structures/1-string/3-truncate/task.md @@ -0,0 +1,17 @@ +# Усечение строки + +[importance 5] + +Создайте функцию `truncate(str, maxlength)`, которая проверяет длину строки `str`, и если она превосходит `maxlength` -- заменяет конец `str` на `"..."`, так чтобы ее длина стала равна `maxlength`. + +Результатом функции должна быть (при необходимости) усечённая строка. + +Например: + +```js +truncate("Вот, что мне хотелось бы сказать на эту тему:", 20) = "Вот, что мне хотело..." + +truncate("Всем привет!", 20) = "Всем привет!" +``` + +Эта функция имеет применение в жизни. Она используется, чтобы усекать слишком длинные темы сообщений. diff --git a/1-js/4-data-structures/1-string/article.md b/1-js/4-data-structures/1-string/article.md new file mode 100644 index 00000000..2309ba44 --- /dev/null +++ b/1-js/4-data-structures/1-string/article.md @@ -0,0 +1,538 @@ +# Строки + +В JavaScript любые текстовые данные являются строками. Не существует отдельного типа "символ", который есть в ряде других языков. + +Внутренним форматом строк, вне зависимости от кодировки страницы, является [Юникод (Unicode)](http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4). +[cut] +## Создание строк + +Строки создаются при помощи двойных или одинарных кавычек: + +```js +var text = "моя строка"; + +var anotherText = 'еще строка'; + +var str = "012345"; +``` + +В JavaScript **нет разницы между двойными и одинарными кавычками**. + +### Специальные символы + +Строки могут содержать специальные символы. Самый часто используемый из таких символов -- это *перевод строки*. + +Он обозначается как `\n`, например: + +```js +//+ run +alert('Привет\nМир'); // выведет "Мир" на новой строке +``` + +Есть и более редкие символы, вот их список: + + + + + + + + + + +
      Специальные символы
      СимволОписание
      \bBackspace
      \fForm feed
      \nNew line
      \rCarriage return
      \tTab
      \uNNNNСимвол в кодировке Юникод с шестнадцатеричным кодом `NNNN`. Например, `\u00A9` -- юникодное представление символа копирайт © +
      + +### Экранирование специальных символов + +Если строка в одинарных кавычках, то внутренние одинарные кавычки внутри должны быть *экранированы*, то есть снабжены обратным слешем `\'`, вот так: + +```js +var str = '*!*I\'m*/!* a JavaScript programmer'; +``` + +В двойных кавычках -- экранируются внутренние двойные: + +```js +//+ run +var str = "I'm a JavaScript \"programmer\" "; +alert(str); +``` + +Экранирование служит исключительно для правильного восприятия строки JavaScript. **В памяти строка будет содержать сам символ без `'\'`**. Вы можете увидеть это, запустив пример выше. + +Сам символ обратного слэша `'\'` является служебным, поэтому всегда экранируется, т.е пишется как `\\`: + +```js +//+ run +var str = ' символ \\ '; + +alert(str); // символ \ +``` + +Заэкранировать можно любой символ. Если он не специальный, то ничего не произойдёт: + +```js +//+ run +alert( "\a" ); // a +// идентично alert( "a" ); +``` + +## Методы и свойства + +Здесь мы рассмотрим методы и свойства строк, с некоторыми из которых мы знакомились ранее, в главе [](/properties-and-methods). + + +### Длина length + +Одно из самых частых действий со строкой -- это получение ее длины: + +```js +//+ run +var str = "My\n"; // 3 символа. Третий - перевод строки + +alert(str.length); // 3 +``` + +### Доступ к символам + +Чтобы получить символ, используйте вызов `charAt(позиция)`. Первый символ имеет позицию `0`: + +```js +//+ run +var str = "jQuery"; +alert( str.charAt(0) ); // "j" +``` + +В JavaScript **нет отдельного типа "символ"**, так что `charAt` возвращает строку, состоящую из выбранного символа. + +[smart] +В современных браузерах (не IE7-) для доступа к символу можно также использовать квадратные скобки: + +```js +//+ run +var str = "Я - современный браузер!"; +alert(str[0]); // "Я", IE8+ +``` + +Разница между этим способом и `charAt` заключается в том, что если символа нет -- `charAt` выдает пустую строку, а скобки -- `undefined`: + +```js +//+ run +alert( "".charAt(0) ); // пустая строка +alert( ""[0] ); // undefined, IE8+ +``` + +[/smart] + +[warn header="Вызов метода -- всегда со скобками"] + +Обратите внимание, `str.length` -- это *свойство* строки, а `str.charAt(pos)` -- *метод*, т.е. функция. + +Обращение к методу всегда идет со скобками, а к свойству -- без скобок. + +[/warn] + + +### Изменения строк + +Строки в JavaScript нельзя изменять. Можно прочитать символ, но нельзя заменить его. Как только строка создана -- она такая навсегда. + +Чтобы это обойти, создаётся новая строка и присваивается в переменную вместо старой: + +```js +//+ run +var str = "строка"; + +str = str.charAt(3) + str.charAt(4) + str.charAt(5); + +alert(str); // ока +``` + +### Смена регистра + +Методы `toLowerCase()` и `toUpperCase()` меняют регистр строки на нижний/верхний: + +```js +//+ run +alert( "Интерфейс".toUpperCase() ); // ИНТЕРФЕЙС +``` + +Пример ниже получает первый символ и приводит его к нижнему регистру: + +```js +alert( "Интерфейс".charAt(0).toLowerCase() ); // 'и' +``` + +### Поиск подстроки + +Для поиска подстроки есть метод indexOf(подстрока[, начальная_позиция]). + +Он возвращает позицию, на которой находится `подстрока` или `-1`, если ничего не найдено. Например: + +```js +//+ run +var str = "Widget with id"; + +alert( str.indexOf("Widget") ); // 0, т.к. "Widget" найден прямо в начале str +alert( str.indexOf("id") ); // 1, т.к. "id" найден, начиная с позиции 1 +alert( str.indexOf("Lalala") ); // -1, подстрока не найдена +``` + +Необязательный второй аргумент позволяет искать, начиная с указанной позиции. Например, первый раз `"id"` появляется на позиции `1`. Чтобы найти его следующее появление -- запустим поиск с позиции `2`: + +```js +//+ run +var str = "Widget with id"; + +alert( str.indexOf("id", 2) ) // 12, поиск начат с позиции 2 +``` + +Также существует аналогичный метод lastIndexOf, который ищет не с начала, а с конца строки. + +[smart] +Для красивого вызова `indexOf` применяется побитовый оператор НЕ `'~'`. + +Дело в том, что вызов `~n` эквивалентен выражению `-(n+1)`, например: + +```js +//+ run +alert( ~2 ); // -(2+1) = -3 +alert( ~1 ); // -(1+1) = -2 +alert( ~0 ); // -(0+1) = -1 +*!* +alert( ~-1 ); // -(-1+1) = 0 +*/!* +``` + +Как видно, `~n` -- ноль только в случае, когда `n == -1`. + +То есть, проверка `if ( ~str.indexOf(...) )` означает, что результат `indexOf` отличен от `-1, т.е. совпадение есть. + +Вот так: + +```js +//+ run +var str = "Widget"; + +if( ~str.indexOf("get") ) { + alert('совпадение есть!'); +} +``` + +Вообще, использовать возможности языка неочевидным образом не рекомендуется, поскольку ухудшает читаемость кода. + +Однако, в данном случае, все в порядке. Просто запомните: `'~'` читается как "не минус один", а `"if ~str.indexOf"` читается как `"если найдено"`. + +[/smart] + + +### Поиск всех вхождений + +Чтобы найти все вхождения подстроки, нужно запустить `indexOf` в цикле. Как только получаем очередную позицию -- начинаем следующий поиск со следующей. + +Пример такого цикла: + +```js +//+ run +var str = "Ослик Иа-Иа посмотрел на виадук"; // ищем в этой строке +var target = "Иа"; // цель поиска + +var pos = 0; +while(true) { + var foundPos = str.indexOf(target, pos); + if (foundPos == -1) break; + + alert(foundPos); // нашли на этой позиции + pos = foundPos + 1; // продолжить поиск со следующей +} +``` + +Такой цикл начинает поиск с позиции `0`, затем найдя подстроку на позиции `foundPos`, следующий поиск продолжит с позиции `pos = foundPos+1`, и так далее, пока что-то находит. + +Впрочем, тот же алгоритм можно записать и короче: + +```js +//+ run +var str = "Ослик Иа-Иа посмотрел на виадук"; // ищем в этой строке +var target = "Иа"; // цель поиска + +*!* +var pos = -1; +while ( (pos = str.indexOf(target, pos+1)) != -1) { + alert(pos); +} +*/!* +``` + +### Взятие подстроки: substr, substring, slice. + +В JavaScript существуют целых 3 (!) метода для взятия подстроки, с небольшими отличиями между ними. + +
      +
      `substring(start [, end])` +
      +Метод `substring(start, end)` возвращает подстроку с позиции `start` до, но не включая `end`. + +```js +//+ run +var str = "*!*s*/!*tringify"; +alert(str.substring(0,1)); // "s", символы с позиции 0 по 1 не включая 1. +``` + +Если аргумент `end` отсутствует, то идет до конца строки: + +```js +//+ run +var str = "st*!*ringify*/!*"; +alert(str.substring(2)); // ringify, символы с позиции 2 до конца +``` + +
      +
      `substr(start [, length])`
      +
      Первый аргумент имеет такой же смысл, как и в `substring`, а второй содержит не конечную позицию, а количество символов. + +```js +//+ run +var str = "st*!*ring*/!*ify"; +str = str.substr(2,4); // ring, со 2й позиции 4 символа +alert(str) +``` + +Если второго аргумента нет -- подразумевается "до конца строки".
      +
      `slice(start [, end])`
      +
      Возвращает часть строки от позиции `start` до, но не включая, позиции `end`. Смысл параметров -- такой же как в `substring`.
      +
      + +### Отрицательные аргументы + +Различие между `substring` и `slice` -- в том, как они работают с отрицательными и выходящими за границу строки аргументами: + +
      +
      `substring(start, end)`
      +
      Отрицательные аргументы интерпретируются как равные нулю. Слишком большие значения усекаются до длины строки: + +```js +//+ run +alert( "testme".substring(-2) ); // "testme", -2 становится 0 +``` + +Кроме того, если start > end, то аргументы меняются местами, т.е. возвращается участок строки *между* `start` и `end`: + +```js +//+ run +alert( "testme".substring(4, -1) ); // "test" +// -1 становится 0 -> получили substring(4, 0) +// 4 > 0, так что аргументы меняются местами -> substring(0, 4) = "test" +``` + +
      +
      `slice`
      +
      Отрицательные значения отсчитываются от конца строки: + +```js +//+ run +alert( "testme".slice(-2) ); // "me", от 2 позиции с конца +``` + + + +```js +//+ run +alert( "testme".slice(1, -1) ); // "estm", от 1 позиции до первой с конца. +``` + +Это гораздо более удобно, чем странная логика `substring`. +
      +
      + +Отрицательное значение первого параметра поддерживается в `substr` во всех браузерах, кроме IE8-. + +[summary] +Самый удобный метод из списка -- это `slice(start, end)`. Очень забавно, что не `substr`, не `substring`, а именно метод `slice`, название которого наименее очевидно. + +Впрочем, `substr(start, length)` тоже можно использовать, но с оглядкой на IE8-, который не поддерживает отрицательный `start`. +[/summary] + + + + +## Кодировка Юникод + +Как мы знаем, символы сравниваются в алфавитном порядке `'А' < 'Б' < 'В' < ... < 'Я'`. + +Но есть несколько странностей.. + +
        +
      1. Почему буква `'а'` маленькая больше буквы `'Я'` большой? + +```js +//+ run +alert( 'а' > 'Я' ); // true +``` + +
      2. +Буква `'ё'` находится в алфавите между `е` и `ж`: абвгде**ё**жз... Но почему тогда `'ё'` больше `'я'`? + +```js +//+ run +alert( 'ё' > 'я' ); // true +``` + +
      3. +
      + +Чтобы разобраться с этим, обратимся к внутреннему представлению строк в JavaScript. + +**Все строки имеют внутреннюю кодировку [Юникод](http://ru.wikipedia.org/wiki/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4).** + +Неважно, на каком языке написана страница, находится ли она в windows-1251 или utf-8. Внутри JavaScript-интерпретатора все строки приводятся к единому "юникодному" виду. Каждому символу соответствует свой код. + +Есть метод для получения символа по его коду: +
      +
      String.fromCharCode(code)
      +
      Возвращает символ по коду `code`: + +```js +//+ run +alert( String.fromCharCode(1072) ); // 'а' +``` + +
      +
      + +...И метод для получения цифрового кода из символа: + +
      +
      str.charCodeAt(pos)
      +
      Возвращает код символа на позиции `pos`. Отсчет позиции начинается с нуля. + +```js +//+ run +alert( "абрикос".charCodeAt(0) ); // 1072, код 'а' +``` + +
      +
      + +Теперь вернемся к примерам выше. Почему сравнения `'ё' > 'я'` и `'а' > 'Я'` дают такой странный результат? + +Дело в том, что **символы сравниваются не по алфавиту, а по коду**. У кого код больше -- тот и больше. В юникоде есть много разных символов. Кириллическим буквам соответствует только небольшая часть из них, подробнее -- [Кириллица в Юникоде](http://ru.wikipedia.org/wiki/%D0%9A%D0%B8%D1%80%D0%B8%D0%BB%D0%BB%D0%B8%D1%86%D0%B0_%D0%B2_%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4%D0%B5). + +Выведем отрезок символов юникода с кодами от `1034` до `1113`: + +```js +//+ run +var str = ''; +for (var i=1034; i<=1113; i++) { + str += String.fromCharCode(i); +} +alert(str); +``` + +Результат: +
      +ЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљ +
      + +Мы можем увидеть из этого отрезка две важных вещи: + +
        +
      1. **Строчные буквы идут после заглавных, поэтому они всегда больше.** + +В частности, `'а'(код 1072) > 'Я'(код 1071)`. + +То же самое происходит и в английском алфавите, там `'a' > 'Z'`. +
      2. +
      3. **Ряд букв, например `ё`, находятся вне основного алфавита.** + +В частности, маленькая буква `ё` имеет код, больший чем `я`, поэтому **`'ё'(код 1105) > 'я'(код 1103)`**. + +Кстати, большая буква `Ё` располагается в Unicode до `А`, поэтому **`'Ё'`(код 1025) < `'А'`(код 1040)**. Удивительно: есть буква меньше чем `А` :) +
      4. +
      + +**Буква `ё` не уникальна, точки над буквой используются и в других языках, приводя к тому же результату.** + +Например, при работе с немецкими названиями: + +```js +//+ run +alert( "ö" > "z" ); // true +``` + +[smart header="Юникод в HTML"] +Кстати, если мы знаем код символа в кодировке юникод, то можем добавить его в HTML, используя "числовую ссылку" (numeric character reference). + +Для этого нужно написать сначала `&#`, затем код, и завершить точкой с запятой `';'`. Например, символ `'а'` в виде числовой ссылки: `а`. + +Если код хотят дать в 16-ричной системе счисления, то начинают с `&#x`. + +В юникоде есть много забавных и полезных символов, например, символ ножниц: ✂ (`✂`), дроби: ½ (`½`) ¾ (`¾`) и другие. Их можно использовать вместо картинок в дизайне. +[/smart] + + +## Посимвольное сравнение + +Сравнение строк работает *лексикографически*, иначе говоря, посимвольно. + +Сравнение строк `s1` и `s2` обрабатывается по следующему алгоритму: + +
      1. Сравниваются первые символы: `a = s1.charAt(0)` и `b = s2.charAt(0)`. Если они разные, то сравниваем их и, в зависимости от результата их сравнения, возвратить `true` или `false`. Если же они одинаковые, то...
      2. +
      3. Сравниваются вторые символы `a = s1.charAt(1)` и `b = s2.charAt(1)`
      4. +
      5. Затем третьи `a = s1.charAt(2)` и `b = s2.charAt(2)` и так далее, пока символы не будут наконец разными, и тогда какой символ больше -- та строка и больше. Если же в какой-либо строке закончились символы, то считаем, что она меньше, а если закончились в обеих -- они равны.
      6. +
      + +Спецификация языка определяет этот алгоритм более детально. Если же говорить простыми словами, смысл алгоритма в точности соответствует порядку, по которому имена заносятся в орфографический словарь. + +```js +"Вася" > "Ваня" // true, т.к. начальные символы совпадают, а потом 'с' > 'н' +"Дома" > "До" // true, т.к. начало совпадает, но в 1й строке больше символов +``` + +[warn header="Числа в виде строк сравниваются как строки"] + +Бывает, что числа приходят в скрипт в виде строк, например как результат `prompt`. В этом случае результат их сравнения будет неверным: + +```js +//+ run +alert("2" > "14"); // true, так как это строки, и для первых символов верно "2" > "1" +``` + +Если хотя бы один аргумент -- не строка, то другой будет преобразован к числу: + +```js +//+ run +alert(2 > "14"); // false +``` + +[/warn] + +## Правильное сравнение + +Все современные браузеры, кроме IE10- (для которых нужно подключить библиотеку [Intl.JS](https://github.com/andyearnshaw/Intl.js/)) поддерживают стандарт [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), поддерживающий сравнение строк на разных языках, с учётом их правил. + +Способ использования: + +```js +//+ run +var str = "Ёлки"; + +alert( str.localeCompare("Яблони") ); // -1 +``` + +Метод `str1.localeCompare(str2)` возвращает `-1`, если `str1 < str2`, `1`, если `str1 > str2` и `0`, если они равны. + +Более подробно про устройство этого метода можно будет узнать в статье [](/intl), когда это вам понадобится. + +## Итого + +
        +
      • Строки в JavaScript имеют внутреннюю кодировку Юникод. При написании строки можно использовать специальные символы, например `\n` и вставлять юникодные символы по коду.
      • +
      • Мы познакомились со свойством `length` и методами `charAt`, `toLowerCase/toUpperCase`, `substring/substr/slice` (предпочтителен `slice`)
      • +
      • Строки сравниваются побуквенно. Поэтому если число получено в виде строки, то такие числа могут сравниваться некорректно, нужно преобразовать его к типу *number*.
      • +
      • При сравнении строк следует иметь в виду, что буквы сравниваются по их кодам. Поэтому большая буква меньше маленькой, а буква `ё` вообще вне основного алфавита.
      • +
      • Для правильного сравнения существует целый стандарт ECMA 402. Это не такое простое дело, много языков и много правил. Он поддерживается во всех современных браузерах, кроме IE10-, в которых нужна библиотека [](https://github.com/andyearnshaw/Intl.js/). Такое сравнение работает через вызов `str1.localeCompare(str2)`.
      • +
      + +Больше информации о методах для строк можно получить в справочнике: [http://javascript.ru/String](). \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/1-new-date/solution.md b/1-js/4-data-structures/10-datetime/1-new-date/solution.md new file mode 100644 index 00000000..d20950e8 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/1-new-date/solution.md @@ -0,0 +1,10 @@ +Дата в местной временной зоне создается при помощи `new Date`. + +Месяцы начинаются с нуля, так что февраль имеет номер 1. Параметры можно указывать с точностью до минут: + +```js +//+ run +var d = new Date(2012, 1, 20, 3, 12); +alert(d); +``` + diff --git a/1-js/4-data-structures/10-datetime/1-new-date/task.md b/1-js/4-data-structures/10-datetime/1-new-date/task.md new file mode 100644 index 00000000..c96882ac --- /dev/null +++ b/1-js/4-data-structures/10-datetime/1-new-date/task.md @@ -0,0 +1,7 @@ +# Создайте дату + +[importance 5] + +Создайте объект `Date` для даты: 20 февраля 2012 года, 3 часа 12 минут. + +Временная зона -- местная. Выведите его на экран. \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/2-get-week-day/_js.view/solution.js b/1-js/4-data-structures/10-datetime/2-get-week-day/_js.view/solution.js new file mode 100644 index 00000000..9b543d37 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/2-get-week-day/_js.view/solution.js @@ -0,0 +1,5 @@ +function getWeekDay(date) { + var days = ['вс','пн','вт','ср','чт','пт','сб'] ; + + return days[ date.getDay() ]; +} \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/2-get-week-day/_js.view/test.js b/1-js/4-data-structures/10-datetime/2-get-week-day/_js.view/test.js new file mode 100644 index 00000000..33f5a79a --- /dev/null +++ b/1-js/4-data-structures/10-datetime/2-get-week-day/_js.view/test.js @@ -0,0 +1,29 @@ +describe("getWeekDay", function() { + it("3 января 2014 - пятница", function() { + assert.equal( getWeekDay(new Date(2014, 0, 3)), 'пт'); + }); + + it("4 января 2014 - суббота", function() { + assert.equal( getWeekDay(new Date(2014, 0, 4)), 'сб'); + }); + + it("5 января 2014 - воскресенье", function() { + assert.equal( getWeekDay(new Date(2014, 0, 5)), 'вс'); + }); + + it("6 января 2014 - понедельник", function() { + assert.equal( getWeekDay(new Date(2014, 0, 6)), 'пн'); + }); + + it("7 января 2014 - вторник", function() { + assert.equal( getWeekDay(new Date(2014, 0, 7)), 'вт'); + }); + + it("8 января 2014 - среда", function() { + assert.equal( getWeekDay(new Date(2014, 0, 8)), 'ср'); + }); + + it("9 января 2014 - четверг", function() { + assert.equal( getWeekDay(new Date(2014, 0, 9)), 'чт'); + }); +}); \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/2-get-week-day/solution.md b/1-js/4-data-structures/10-datetime/2-get-week-day/solution.md new file mode 100644 index 00000000..6f65f4e9 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/2-get-week-day/solution.md @@ -0,0 +1,16 @@ +Метод `getDay()` позволяет получить номер дня недели, начиная с воскресенья. + +Запишем имена дней недели в массив, чтобы можно было их достать по номеру: + +```js +//+ run +function getWeekDay(date) { + var days = ['вс','пн','вт','ср','чт','пт','сб'] ; + + return days[ date.getDay() ]; +} + +var date = new Date(2014,0,3); // 3 января 2014 +alert( getWeekDay(date) ); // 'пт' +``` + diff --git a/1-js/4-data-structures/10-datetime/2-get-week-day/task.md b/1-js/4-data-structures/10-datetime/2-get-week-day/task.md new file mode 100644 index 00000000..b66aa2c1 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/2-get-week-day/task.md @@ -0,0 +1,13 @@ +# Имя дня недели + +[importance 5] + +Создайте функцию `getWeekDay(date)`, которая выводит текущий день недели в коротком формате 'пн', 'вт', ... 'вс'. + +Например: + +```js +var date = new Date(2012,0,3); // 3 января 2012 +alert( getWeekDay(date) ); // Должно вывести 'вт' +``` + diff --git a/1-js/4-data-structures/10-datetime/3-weekday/_js.view/solution.js b/1-js/4-data-structures/10-datetime/3-weekday/_js.view/solution.js new file mode 100644 index 00000000..3c3266ce --- /dev/null +++ b/1-js/4-data-structures/10-datetime/3-weekday/_js.view/solution.js @@ -0,0 +1,10 @@ +function getLocalDay(date) { + + var day = date.getDay(); + + if ( day == 0 ) { // день 0 становится 7 + day = 7; + } + + return day; +} diff --git a/1-js/4-data-structures/10-datetime/3-weekday/_js.view/test.js b/1-js/4-data-structures/10-datetime/3-weekday/_js.view/test.js new file mode 100644 index 00000000..7b7e2e81 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/3-weekday/_js.view/test.js @@ -0,0 +1,29 @@ +describe("getLocalDay возвращает день недели", function() { + it("3 января 2014 - пятница", function() { + assert.equal( getLocalDay(new Date(2014, 0, 3)), 5); + }); + + it("4 января 2014 - суббота", function() { + assert.equal( getLocalDay(new Date(2014, 0, 4)), 6); + }); + + it("5 января 2014 - воскресенье", function() { + assert.equal( getLocalDay(new Date(2014, 0, 5)), 7); + }); + + it("6 января 2014 - понедельник", function() { + assert.equal( getLocalDay(new Date(2014, 0, 6)), 1); + }); + + it("7 января 2014 - вторник", function() { + assert.equal( getLocalDay(new Date(2014, 0, 7)), 2); + }); + + it("8 января 2014 - среда", function() { + assert.equal( getLocalDay(new Date(2014, 0, 8)), 3); + }); + + it("9 января 2014 - четверг", function() { + assert.equal( getLocalDay(new Date(2014, 0, 9)), 4); + }); +}); diff --git a/1-js/4-data-structures/10-datetime/3-weekday/solution.md b/1-js/4-data-structures/10-datetime/3-weekday/solution.md new file mode 100644 index 00000000..62f951c5 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/3-weekday/solution.md @@ -0,0 +1,19 @@ +Решение - в использовании встроенной функции `getDay`. Она полностью подходит нашим целям, но для воскресенья возвращает 0 вместо 7: + +```js +//+ run +function getLocalDay(date) { + + var day = date.getDay(); + + if ( day == 0 ) { // день 0 становится 7 + day = 7; + } + + return day; +} + +alert( getLocalDay(new Date(2012,0,3)) ); // 2 +``` + +Если удобнее, чтобы день недели начинался с нуля, то можно возвращать в функции `day - 1`, тогда дни будут от 0 (пн) до 6(вс). \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/3-weekday/task.md b/1-js/4-data-structures/10-datetime/3-weekday/task.md new file mode 100644 index 00000000..b0398e39 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/3-weekday/task.md @@ -0,0 +1,13 @@ +# День недели в европейской нумерации + +[importance 5] + +Напишите функцию, `getLocalDay(date)` которая возвращает день недели для даты `date`. + +День нужно возвратить в европейской нумерации, т.е. понедельник имеет номер 1, вторник номер 2, ..., воскресенье - номер 7. + +```js +var date = new Date(2012, 0, 3); // 3 янв 2012 +alert( getLocalDay(date) ); // вторник, выведет 2 +``` + diff --git a/1-js/4-data-structures/10-datetime/4-get-date-ago/_js.view/solution.js b/1-js/4-data-structures/10-datetime/4-get-date-ago/_js.view/solution.js new file mode 100644 index 00000000..6389ebfc --- /dev/null +++ b/1-js/4-data-structures/10-datetime/4-get-date-ago/_js.view/solution.js @@ -0,0 +1,6 @@ +function getDateAgo(date, days) { + var dateCopy = new Date(date); + + dateCopy.setDate( date.getDate() - days ); + return dateCopy.getDate(); +} \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/4-get-date-ago/_js.view/test.js b/1-js/4-data-structures/10-datetime/4-get-date-ago/_js.view/test.js new file mode 100644 index 00000000..138ddbd7 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/4-get-date-ago/_js.view/test.js @@ -0,0 +1,27 @@ +describe("getDateAgo", function() { + + it("1 день до 02.01.2015 -> число 1", function() { + assert.equal( getDateAgo(new Date(2015, 0, 2), 1), 1 ); + }); + + + it("2 день до 02.01.2015 -> число 31", function() { + assert.equal( getDateAgo(new Date(2015, 0, 2), 2), 31 ); + }); + + it("100 дней от 02.01.2015 -> число 24", function() { + assert.equal( getDateAgo(new Date(2015, 0, 2), 100), 24 ); + }); + + it("365 дней от 02.01.2015 -> число 2", function() { + assert.equal( getDateAgo(new Date(2015, 0, 2), 365), 2 ); + }); + + it("не меняет переданный объект Date", function() { + var date = new Date(2015, 0, 2); + var dateCopy = new Date(date); + getDateAgo(dateCopy, 100); + assert.equal(date.getTime(), dateCopy.getTime()); + }); + +}); diff --git a/1-js/4-data-structures/10-datetime/4-get-date-ago/solution.md b/1-js/4-data-structures/10-datetime/4-get-date-ago/solution.md new file mode 100644 index 00000000..d9f8f553 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/4-get-date-ago/solution.md @@ -0,0 +1,29 @@ +Из даты `date` нужно вычесть указанное количество дней. Это просто: + +```js +function getDateAgo(date, days) { + date.setDate( date.getDate() - days ); + return date.getDate(); +} +``` + +Ситуацию осложняет то, что исходный объект даты не должен меняться. Это разумное требование, оно позволит избежать сюрпризов. + +Для того чтобы ему соответствовать, создадим копию объекта даты: + +```js +//+ run +function getDateAgo(date, days) { + var dateCopy = new Date(date); + + dateCopy.setDate( date.getDate() - days ); + return dateCopy.getDate(); +} + +var date = new Date(2015, 0, 2); + +alert( getDateAgo(date, 1) ); // 1, (1 января 2015) +alert( getDateAgo(date, 2) ); // 31, (31 декабря 2014) +alert( getDateAgo(date, 365) ); // 2, (2 января 2014) +``` + diff --git a/1-js/4-data-structures/10-datetime/4-get-date-ago/task.md b/1-js/4-data-structures/10-datetime/4-get-date-ago/task.md new file mode 100644 index 00000000..0db7847b --- /dev/null +++ b/1-js/4-data-structures/10-datetime/4-get-date-ago/task.md @@ -0,0 +1,17 @@ +# День указанное количество дней назад + +[importance 4] + +Создайте функцию `getDateAgo(date, days)`, которая возвращает число, которое было `days` дней назад от даты `date`. + +Например, для 2 января 2015: + +```js +var date = new Date(2015, 0, 2); + +alert( getDateAgo(date, 1) ); // 1, (1 января 2015) +alert( getDateAgo(date, 2) ); // 31, (31 декабря 2014) +alert( getDateAgo(date, 365) ); // 2, (2 января 2014) +``` + +P.S. Важная деталь: в процессе вычислений функция не должна менять переданный ей объект `date`. diff --git a/1-js/4-data-structures/10-datetime/5-last-day-of-month/_js.view/solution.js b/1-js/4-data-structures/10-datetime/5-last-day-of-month/_js.view/solution.js new file mode 100644 index 00000000..ed998043 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/5-last-day-of-month/_js.view/solution.js @@ -0,0 +1,4 @@ +function getLastDayOfMonth(year, month) { + var date = new Date(year, month+1, 0); + return date.getDate(); +} diff --git a/1-js/4-data-structures/10-datetime/5-last-day-of-month/_js.view/test.js b/1-js/4-data-structures/10-datetime/5-last-day-of-month/_js.view/test.js new file mode 100644 index 00000000..c1d174e0 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/5-last-day-of-month/_js.view/test.js @@ -0,0 +1,13 @@ +describe("getLastDayOfMonth", function() { + it("последний день 01.01.2012 - 31", function() { + assert.equal( getLastDayOfMonth(2012, 0), 31); + }); + + it("последний день 01.02.2012 - 29 (високосный год)", function() { + assert.equal( getLastDayOfMonth(2012, 1), 29); + }); + + it("последний день 01.02.2013 - 28", function() { + assert.equal( getLastDayOfMonth(2013, 1), 28); + }); +}); \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/5-last-day-of-month/solution.md b/1-js/4-data-structures/10-datetime/5-last-day-of-month/solution.md new file mode 100644 index 00000000..5dd517e9 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/5-last-day-of-month/solution.md @@ -0,0 +1,14 @@ +Создадим дату из следующего месяца, но день не первый, а "нулевой" (т.е. предыдущий): + +```js +//+ run +function getLastDayOfMonth(year, month) { + var date = new Date(year, month+1, 0); + return date.getDate(); +} + +alert( getLastDayOfMonth(2012, 0) ); // 31 +alert( getLastDayOfMonth(2012, 1) ); // 29 +alert( getLastDayOfMonth(2013, 1) ); // 28 +``` + diff --git a/1-js/4-data-structures/10-datetime/5-last-day-of-month/task.md b/1-js/4-data-structures/10-datetime/5-last-day-of-month/task.md new file mode 100644 index 00000000..8f88e971 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/5-last-day-of-month/task.md @@ -0,0 +1,13 @@ +# Последний день месяца? + +[importance 5] + +Напишите функцию `getLastDayOfMonth(year, month)`, которая возвращает последний день месяца. + +Параметры: +
        +
      • `year` -- 4-значный год, например 2012.
      • +
      • `month` -- месяц от 0 до 11.
      • +
      + +Например, `getLastDayOfMonth(2012, 1) = 29` (високосный год, февраль). diff --git a/1-js/4-data-structures/10-datetime/6-get-seconds-today/solution.md b/1-js/4-data-structures/10-datetime/6-get-seconds-today/solution.md new file mode 100644 index 00000000..00cbc9ee --- /dev/null +++ b/1-js/4-data-structures/10-datetime/6-get-seconds-today/solution.md @@ -0,0 +1,19 @@ +Для вывода достаточно сгенерировать объект `Date`, соответствующий началу дня, т.е. "сегодня" 00 часов 00 минут 00 секунд, и вычесть его из текущей даты. + +Полученная разница -- это как раз количество миллисекунд от начала дня, которое достаточно поделить на `1000`, чтобы получить секунды: + +```js +//+ run +function getSecondsToday() { + var now = new Date(); + + // создать объект из текущей даты, без часов-минут-секунд + var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + var diff = now - today; // разница в миллисекундах + return Math.round(diff / 1000); // перевести в секунды +} + +alert( getSecondsToday() ); +``` + diff --git a/1-js/4-data-structures/10-datetime/6-get-seconds-today/task.md b/1-js/4-data-structures/10-datetime/6-get-seconds-today/task.md new file mode 100644 index 00000000..0b8cde4a --- /dev/null +++ b/1-js/4-data-structures/10-datetime/6-get-seconds-today/task.md @@ -0,0 +1,13 @@ +# Сколько секунд уже прошло сегодня? + +[importance 5] + +Напишите функцию `getSecondsToday()` которая возвращает, сколько секунд прошло с начала сегодняшнего дня. + +Например, если сейчас `10:00` и не было перехода на зимнее/летнее время, то: + +```js +getSecondsToday() == 36000 // (3600 * 10) +``` + +Функция должна работать в любой день, т.е. в ней не должно быть конкретного значения сегодняшней даты. \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/7-get-seconds-to-tomorrow/solution.md b/1-js/4-data-structures/10-datetime/7-get-seconds-to-tomorrow/solution.md new file mode 100644 index 00000000..a00492fc --- /dev/null +++ b/1-js/4-data-structures/10-datetime/7-get-seconds-to-tomorrow/solution.md @@ -0,0 +1,17 @@ +Для получения оставшихся до конца дня миллисекунд нужно из "завтра 00ч 00мин 00сек" вычесть текущее время. + +Чтобы сгенерировать "завтра" -- увеличим текущую дату на 1 день: + +```js +//+ run +function getSecondsToTomorrow() { + var now = new Date(); + + // создать объект из завтрашней даты, без часов-минут-секунд + var tomorrow = new Date(now.getFullYear(), now.getMonth(), *!*now.getDate()+1*/!*); + + var diff = tomorrow - now; // разница в миллисекундах + return Math.round(diff / 1000); // перевести в секунды +} +``` + diff --git a/1-js/4-data-structures/10-datetime/7-get-seconds-to-tomorrow/task.md b/1-js/4-data-structures/10-datetime/7-get-seconds-to-tomorrow/task.md new file mode 100644 index 00000000..93980a4f --- /dev/null +++ b/1-js/4-data-structures/10-datetime/7-get-seconds-to-tomorrow/task.md @@ -0,0 +1,13 @@ +# Сколько секунд - до завтра? + +[importance 5] + +Напишите функцию `getSecondsToTomorrow()` которая возвращает, сколько секунд осталось до завтра. + +Например, если сейчас `23:00`, то: + +```js +getSecondsToTomorrow() == 3600 +``` + +P.S. Функция должна работать в любой день, т.е. в ней не должно быть конкретного значения сегодняшней даты. \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/_js.view/solution.js b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/_js.view/solution.js new file mode 100644 index 00000000..df675b10 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/_js.view/solution.js @@ -0,0 +1,13 @@ +function formatDate(date) { + + var dd = date.getDate(); + if ( dd < 10 ) dd = '0' + dd; + + var mm = date.getMonth()+1; + if ( mm < 10 ) mm = '0' + mm; + + var yy = date.getFullYear() % 100; + if ( yy < 10 ) yy = '0' + yy; + + return dd+'.'+mm+'.'+yy; +} diff --git a/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/_js.view/test.js b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/_js.view/test.js new file mode 100644 index 00000000..2980944a --- /dev/null +++ b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/_js.view/test.js @@ -0,0 +1,13 @@ +describe("formatDate", function() { + it("правильно форматирует дату 30.01.14", function() { + assert.equal( formatDate(new Date(2014, 0, 30)), '30.01.14'); + }); + + it("правильно форматирует дату 01.01.01", function() { + assert.equal( formatDate(new Date(2001, 0, 1)), '01.01.01'); + }); + + it("правильно форматирует дату 01.01.00", function() { + assert.equal( formatDate(new Date(2000, 0, 1)), '01.01.00'); + }); +}); diff --git a/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/solution.md b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/solution.md new file mode 100644 index 00000000..71125af8 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/solution.md @@ -0,0 +1,51 @@ +Получим компоненты один за другим. +
        +
      1. День можно получить как `date.getDate()`. При необходимости добавим ведущий ноль: + +```js +var dd = date.getDate(); +if (dd<10) dd= '0'+dd; +``` + +
      2. +
      3. `date.getMonth()` возвратит месяц, начиная с нуля. Увеличим его на 1: + +```js +var mm = date.getMonth() + 1; // месяц 1-12 +if (mm<10) mm= '0'+mm; +``` + +
      4. +
      5. `date.getFullYear()` вернет год в 4-значном формате. Чтобы сделать его двузначным - воспользуемся оператором взятия остатка `'%'`: + +```js +var yy = date.getFullYear() % 100; +if (yy<10) yy= '0'+yy; +``` + +Заметим, что год, как и другие компоненты, может понадобиться дополнить нулем слева, причем возможно что `yy == 0` (например, 2000 год). При сложении со строкой `0+'0' == '00'`, так что будет все в порядке. +
      6. +
      + +Полный код: + +```js +//+ run +function formatDate(date) { + + var dd = date.getDate(); + if ( dd < 10 ) dd = '0' + dd; + + var mm = date.getMonth()+1; + if ( mm < 10 ) mm = '0' + mm; + + var yy = date.getFullYear() % 100; + if ( yy < 10 ) yy = '0' + yy; + + return dd+'.'+mm+'.'+yy; +} + +var d = new Date(2014, 0, 30); // 30 Янв 2014 +alert( formatDate(d) ); // '30.01.14' +``` + diff --git a/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/task.md b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/task.md new file mode 100644 index 00000000..820b0a4b --- /dev/null +++ b/1-js/4-data-structures/10-datetime/8-format-date-ddmmyy/task.md @@ -0,0 +1,14 @@ +# Вывести дату в формате дд.мм.гг + +[importance 3] + +Напишите функцию `formatDate(date)`, которая выводит дату `date` в формате `дд.мм.гг`: + +Например: + +```js +var d = new Date(2014, 0, 30); // 30 января 2014 +alert( formatDate(d) ); // '30.01.14' +``` + +P.S. Обратите внимание, ведущие нули должны присутствовать, то есть 1 января 2001 должно быть 01.01.01, а не 1.1.1. \ No newline at end of file diff --git a/1-js/4-data-structures/10-datetime/9-format-date-relative/_js.view/solution.js b/1-js/4-data-structures/10-datetime/9-format-date-relative/_js.view/solution.js new file mode 100644 index 00000000..e332defe --- /dev/null +++ b/1-js/4-data-structures/10-datetime/9-format-date-relative/_js.view/solution.js @@ -0,0 +1,34 @@ +function formatDate(date) { + var diff = new Date() - date; // разница в миллисекундах + + if (diff < 1000) { // прошло менее 1 секунды + return 'только что'; + } + + var sec = Math.floor( diff / 1000 ); // округлить diff до секунд + + if (sec < 60) { + return sec + ' сек. назад'; + } + + var min = Math.floor( diff / 60000 ); // округлить diff до минут + if (min < 60) { + return min + ' мин. назад'; + } + + // форматировать дату, с учетом того, что месяцы начинаются с 0 + var d = date; + d = [ + '0'+d.getDate(), + '0'+(d.getMonth()+1), + ''+d.getFullYear(), + '0'+d.getHours(), + '0'+d.getMinutes() + ]; + + for(var i=0; i +
    4. Если со времени `date` прошло менее секунды, то возвращает `"только что"`.
    5. +
    6. Иначе если со времени `date` прошло менее минуты, то `"n сек. назад"`.
    7. +
    8. Иначе если прошло меньше часа, то `"m мин. назад"`.
    9. +
    10. Иначе полная дата в формате `"дд.мм.гг чч:мм"`.
    11. + + +Например: + +```js +function formatDate(date) { /* ваш код */ } + +alert( formatDate( new Date(new Date - 1) ) ); // "только что" + +alert( formatDate( new Date(new Date - 30*1000) ) ); // "30 сек. назад" + +alert( formatDate( new Date(new Date- 5*60*1000) ) ); // "5 мин. назад" + +alert( formatDate( new Date(new Date - 86400*1000) ) ); // вчерашняя дата в формате "дд.мм.гг чч:мм" +``` + diff --git a/1-js/4-data-structures/10-datetime/article.md b/1-js/4-data-structures/10-datetime/article.md new file mode 100644 index 00000000..13f9c5b8 --- /dev/null +++ b/1-js/4-data-structures/10-datetime/article.md @@ -0,0 +1,492 @@ +# Дата и Время + +Для работы с датой и временем в JavaScript используются объекты [Date](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/). + +[cut] +## Создание + +Для создания нового объекта типа `Date` используется один из синтаксисов: +
      +
      `new Date()`
      +
      Создает объект `Date` с текущей датой и временем: + +```js +//+ run +var now = new Date(); +alert(now); +``` + +
      +
      `new Date(milliseconds)`
      +
      Создает объект `Date`, значение которого равно количеству миллисекунд (1/1000 секунды), прошедших с 1 января 1970 года GMT+0. + +```js +//+ run +// 24 часа после 01.01.1970 GMT+0 +var Jan02_1970 = new Date(3600*24*1000); +alert( Jan02_1970 ); +``` + +
      +
      `new Date(datestring)`
      +
      Если единственный аргумент - строка, используется вызов `Date.parse` для ее разбора.
      +
      `new Date(year, month, date, hours, minutes, seconds, ms)`
      +
      Дату можно создать, используя компоненты в местной временной зоне. Для этого формата обязательны только первые два аргумента. Отсутствующие параметры, начиная с `hours` считаются равными нулю, а `date` -- единице. + +**Заметим, что год `year` должен быть из 4 цифр, а отсчет месяцев `month` начинается с нуля 0.** Например: + +```js +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); // то же самое +``` + +Дата задана с точностью до миллисекунд: + +```js +//+ run +var d = new Date(2011, 0, 1, 2, 3, 4, 567); +alert(d); // 1.01.2011, 02:03:04.567 +``` + +
      +
      + + +## Получение компонентов даты + +Для доступа к компонентам даты-времени объекта `Date` используются следующие методы: +
      +
      `getFullYear()`
      +
      Получить год(из 4 цифр)
      +
      `getMonth()`
      +
      Получить месяц, **от 0 до 11**.
      +
      `getDate()`
      +
      Получить число месяца, от 1 до 31.
      +
      `getHours(), getMinutes(), getSeconds(), getMilliseconds()`
      +
      Получить соответствующие компоненты.
      +
      + +[warn header="Устаревший `getYear()`"] +Некоторые браузеры реализуют нестандартный метод `getYear()`. Где-то он возвращает только две цифры из года, где-то четыре. Так или иначе, этот метод отсутствует в стандарте JavaScript. Не используйте его. Для получения года есть `getFullYear()`. +[/warn] + +Дополнительно можно получить день недели: +
      +
      `getDay()`
      +
      Получить номер дня в неделе. Неделя в JavaScript начинается с воскресенья, так что результат будет числом **от 0(воскресенье) до 6(суббота)**.
      +
      + +**Все методы, указанные выше, возвращают результат для местной временной зоны.** + +Существуют также UTC-варианты этих методов, возвращающие день, месяц, год и т.п. для зоны GMT+0 (UTC): `getUTCFullYear()`, `getUTCMonth()`, `getUTCDay()`. То есть, сразу после `"get"` вставляется `"UTC"`. + +Если ваше локальное время сдвинуто относительно UTC, то следующий код покажет разные часы: + +```js +//+ run +var date = new Date(); + +alert( date.getHours() ); // час в вашей зоне для даты date +alert( date.getUTCHours() ); // час в зоне GMT+0 для даты date +``` + +Кроме описанных выше, существуют два специальных метода без UTC-варианта: + +
      +
      `getTime()`
      +
      Возвращает число миллисекунд, прошедших с 01.01.1970 00:00:00 UTC. Это то же число, которое используется в конструкторе `new Date(milliseconds)`.
      +
      `getTimezoneOffset()`
      +
      Возвращает разницу между местным и UTC-временем, в минутах. + +```js +//+ run +alert( new Date().getTimezoneOffset() ); // Для GMT-1 выведет 60 +``` + +
      +
      + + + +## Установка компонентов даты + +Следующие методы позволяют устанавливать компоненты даты и времени: +
        +
      • `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`. При этом если какая-то компонента не указана, она не меняется. Например: + +```js +//+ run +var today = new Date; + +today.setHours(0); +alert( today ); // сегодня, но час изменён на 0 + +today.setHours(0, 0, 0, 0); +alert (today ); // сегодня, ровно 00:00:00. +``` + +### Автоисправление даты + +*Автоисправление* -- очень удобное свойство объектов `Date`. Оно заключается в том, что можно устанавливать заведомо некорректные компоненты (например 32 января), а объект сам себя поправит. + +```js +//+ run +var d = new Date(2013, 0, *!*32*/!*); // 32 января 2013 ?!? +alert(d); // ... это 1 февраля 2013! +``` + +**Неправильные компоненты даты автоматически распределяются по остальным.** + +Например, нужно увеличить на 2 дня дату "28 февраля 2011". Может быть так, что это будет 2 марта, а может быть и 1 марта, если год високосный. Но нам обо всем этом думать не нужно. Просто прибавляем два дня. Остальное сделает `Date`: + +```js +//+ run +var d = new Date(2011, 1, 28); +*!* +d.setDate( d.getDate() + 2 ); +*/!* + +alert(d); // 2 марта, 2011 +``` + +Также это используют для получения даты, отдаленной от имеющейся на нужный промежуток времени. Например, получим дату на 70 секунд большую текущей: + +```js +//+ run +var d = new Date(); +d.setSeconds( d.getSeconds()+70); + +alert(d); // выведет корректную дату +``` + +Можно установить и нулевые, и даже отрицательные компоненты. Например: + +```js +//+ run +var d = new Date; + +d.setDate(1); // поставить первое число месяца +alert(d); + +d.setDate(0); // нулевого числа нет, будет последнее число предыдущего месяца +alert(d); +``` + + + +```js +//+ run +var d = new Date; + +d.setDate(-1); // предпоследнее число предыдущего месяца +alert(d); +``` + +### Преобразование к числу, разность дат + +Когда объект `Date` используется в числовом контексте, он преобразуется в количество миллисекунд: + +```js +//+ run +alert( +new Date ) // +date то же самое, что: +date.valueOf() +``` + +**Важный побочный эффект: даты можно вычитать, результат вычитания объектов `Date` -- их временная разница, в миллисекундах**. + +Это используют для измерения времени: + +```js +//+ run +var start = new Date; // засекли время + +// что-то сделать +for (var i=0; i<100000; i++) { + var doSomething = i*i*i; +} + +var end = new Date; // конец измерения + +alert("Цикл занял " + (end-start) + " ms"); +``` + +### Бенчмаркинг + +Допустим, у нас есть несколько вариантов решения задачи, каждый описан функцией. + +Как узнать, какой быстрее? + +Для примера возьмем две функции, которые бегают по массиву: + +```js +function walkIn(arr) { + for(var key in arr) arr[i]++ +} + +function walkLength(arr) { + for(var i=0; i`, чтобы узнать, сколько времени потребовалось браузеру, чтобы до него добраться, включая загрузку HTML. + +Возвращаемое значение измеряется в миллисекундах, но дополнительно имеет точность 3 знака после запятой (до миллионных долей секунды!), поэтому можно использовать его и для более точного бенчмаркинга в том числе. +[/smart] + +[smart header="`console.time(метка)` и `console.timeEnd(метка)`"] +Для измерения с одновременным выводом результатов в консоли есть методы: +
        +
      • `console.time(метка)` -- включить внутренний хронометр браузера с меткой.
      • +
      • `console.timeEnd(метка)` -- выключить внутренний хронометр браузера с меткой и вывести результат.
      • +
      +Параметр `"метка"` используется для идентификации таймера, чтобы можно было делать много замеров одновременно и даже вкладывать измерения друг в друга. + +В коде ниже таймеры `walkIn`, `walkLength` -- конкретные тесты, а таймер "All Benchmarks" -- время "на всё про всё": + +```js +//+ run +var arr = []; +for(var i=0; i<1000; i++) arr[i] = 0; + +function walkIn(arr) { for(var key in arr) arr[i]++; } +function walkLength(arr) { for(var i=0; i +
    12. Автоматически выносят инвариант, то есть постоянное в цикле значение типа `arr.length`, за пределы цикла.
    13. +
    14. Стараются понять, значения какого типа хранит данная переменная или массив, какую структуру имеет объект и, исходя из этого, оптимизировать внутренние алгоритмы.
    15. +
    16. Выполняют простейшие операции, например сложение явно заданных чисел и строк, на этапе компиляции.
    17. +
    18. В теории, могут выкинуть код, который ни на что не влияет, например присваивание к неиспользуемой локальной переменной, хотя делают это редко.
    19. +
    +Они могут влиять на результаты тестов. +[/warn] + + +## Форматирование + +Во всех браузерах, кроме IE10-, поддерживается новый стандарт [Ecma 402](http://www.ecma-international.org/publications/standards/Ecma-402.htm), который добавляет специальные методы для форматирования дат. + +Это делается взыовом `date.toLocaleString(локаль, опции)`, у которого много настроек. Он позволяет указать, какие параметры даты нужно вывести, и ряд настроек вывода, после чего интерпретатор сам сформирует строку. + +Пример с почти всеми параметрами даты и русским, затем английским (США) форматированием: + +```js +//+ run +var date = new Date(2014, 11, 31, 12, 30, 0); + +var options = { + era: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'long', + timezone: 'UTC', + hour: 'numeric', + minute: 'numeric', + second: 'numeric' +}; + +alert( date.toLocaleString("ru", options) ); // среда, 31 декабря 2014 г. н.э. 12:30:00 +alert( date.toLocaleString("en-US", options) ); // Wednesday, December 31, 2014 Anno Domini 12:30:00 PM +``` + +Вы сможете подробно узнать о них в статье [](/intl), которая посвящена этому стандарту. + + +**Методы вывода без локализации:** + +
    +
    `toString()`, `toDateString()`, `toTimeString()`
    +
    Возвращают стандартное строчное представление, не указанное в стандарте, а зависящее от браузера. Единственное требование - читаемость человеком. Метод `toString` возвращает дату целиком, `toDateString()` и `toTimeString()` - только дату и время соответственно. + +```js +//+ run +var d = new Date(); + +alert( d.toString() ); // вывод, похожий на 'Wed Jan 26 2011 16:40:50 GMT+0300' +``` + +
    `toUTCString()`
    +
    То же самое, что `toString()`, но дата в зоне UTC.
    +
    +
    `toISOString()`
    +
    Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE<9. + +```js +//+ run +var d = new Date(); + +alert( d.toISOString() ); // вывод, похожий на '2011-01-26T13:51:50.417Z' +``` + +
    + +**Если хочется иметь большую гибкость и кросс-браузерность, то также можно воспользоваться специальной библиотекой, например [Moment.JS](http://momentjs.com/) или написать свою функцию.** + + + +## Разбор строки, Date.parse + +Все современные браузеры, включая IE9+, понимают даты в упрощённом формате ISO 8601 Extended. + +Этот формат выглядит так: `YYYY-MM-DDTHH:mm:ss.sssZ`. Для разделения даты и времени в нем используется символ `'T'`. Часть `'Z'` обозначает (необязательную) временную зону -- она может отсутствовать, тогда зона UTC, либо может быть символ `z` -- тоже UTC, или зона в формате `+-hh:mm`. + +Также возможны упрощенные варианты, к примеру: + +```js +YYYY +YYYY-MM +YYYY-MM-DD +``` + +Метод `Date.parse(str)` разбирает строку `str` в таком формате и возвращает соответствующее ей количество миллисекунд. Если это невозможно, `Date.parse` возвращает `NaN`. + +На момент написания некоторые браузеры (Safari) воспринимали формат без `'Z'` как дату в локальной таймзоне (по стандарту UTC), поэтому пример ниже в них работает некорректно: + +```js +//+ run +var msNoZone = Date.parse('2012-01-26T13:51:50.417'); // без зоны, значит UTC + +alert(msNoZone); // 1327571510417 (число миллисекунд) + +var msZ = Date.parse('2012-01-26T13:51:50.417z'); // зона z означает UTC +alert(msZ == msNoZone); // true, если браузер правильный +``` + +С таймзоной `-07:00 GMT` в конце все современные браузеры работают правильно: + +```js +//+ run +var ms = Date.parse('2012-01-26T13:51:50.417-07:00'); + +alert(ms); // 1327611110417 (число миллисекунд) +``` + +[smart header="Формат дат для IE8-"] +До появления спецификации EcmaScript 5 формат не был стандартизован, и браузеры, включая IE8-, имели свои собственные форматы дат. Частично, эти форматы пересекаются. + +Например, код ниже работает везде, включая старые IE: + +```js +//+ run +var ms = Date.parse("January 26, 2011 13:51:50"); + +alert(ms); +``` + +Вы также можете почитать о старых форматах IE в документации к методу MSDN Date.parse. + +Конечно же, сейчас лучше использовать современный формат. Если же нужна поддержка IE8-, то метод `Date.parse`, как и ряд других современных методов, добавляется библиотекой [es5-shim](https://github.com/kriskowal/es5-shim). +[/smart] + +## Метод Date.now() + +Метод `Date.now()` возвращает дату сразу в виде миллисекунд. + +Технически, он аналогичен вызову `+new Date()`, но в отличие от него не создаёт промежуточный объект даты, а поэтому -- во много раз быстрее. + +Его использование особенно рекомендуется там, где производительность при работе с датами критична. Обычно это не на веб-страницах, а, к примеру, в разработке игр на JavaScript. + +## Итого + +
      +
    • Дата и время представлены в JavaScript одним объектом: [Date](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/). Создать "только время" при этом нельзя, оно должно быть с датой. Список методов `Date` вы можете найти в справочнике [Date](http://javascript.ru/Date) или выше.
    • +
    • Отсчёт месяцев начинается с нуля.
    • +
    • Отсчёт дней недели (для `getDay()`) тоже начинается с нуля (и это воскресенье).
    • +
    • Объект `Date` удобен тем, что автокорректируется. Благодаря этому легко сдвигать даты.
    • +
    • При преобразовании к числу объект `Date` даёт количество миллисекунд, прошедших с 1 января 1970 UTC. Побочное следствие -- даты можно вычитать, результатом будет разница в миллисекундах.
    • +
    • Для получения текущей даты в миллисекундах лучше использовать `Date.now()`, чтобы не создавать лишний объект `Date` (кроме IE8-)
    • +
    • Для бенчмаркинга лучше использовать `performance.now()` (кроме IE9-), он в 1000 раз точнее.
    • +
    \ No newline at end of file diff --git a/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/_js.view/solution.js b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/_js.view/solution.js new file mode 100644 index 00000000..12a379c5 --- /dev/null +++ b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/_js.view/solution.js @@ -0,0 +1,28 @@ +function formatDate(date) { + if (typeof date == 'number') { + // перевести секунды в миллисекунды и преобразовать к Date + date = new Date(date*1000); + } else if(typeof date == 'string') { + // разобрать строку и преобразовать к Date + date = date.split('-'); + date = new Date(date[0], date[1]-1, date[2]); + } else if ( date.length ) { // есть длина, но не строка - значит массив + date = new Date(date[0], date[1], date[2]); + } + // преобразования для поддержки полиморфизма завершены, + // теперь мы работаем с датой (форматируем её) + + var day = date.getDate(); + if (day < 10) day = '0' + day; + + var month = date.getMonth()+1; + if (month < 10) month = '0' + month; + + // взять 2 последние цифры года + var year = date.getFullYear() % 100; + if (year < 10) year = '0' + year; + + var formattedDate = day + '.' + month + '.' + year; + + return formattedDate; +} \ No newline at end of file diff --git a/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/_js.view/test.js b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/_js.view/test.js new file mode 100644 index 00000000..adb572fc --- /dev/null +++ b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/_js.view/test.js @@ -0,0 +1,18 @@ +describe("formatDate", function() { + it("читает дату вида гггг-мм-дд из строки", function() { + assert.equal( formatDate( '2011-10-02' ), "02.10.11" ); + }); + + it("читает дату из числа 1234567890 (миллисекунды)", function() { + assert.equal( formatDate( 1234567890 ), "14.02.09" ); + }); + + it("читает дату из массива вида [гггг, м, д]", function() { + assert.equal( formatDate( [2014,0,1] ), "01.01.14" ); + }); + + it("читает дату из объекта Date", function() { + assert.equal( formatDate( new Date(2014,0,1) ), "01.01.14" ); + }); + +}); \ No newline at end of file diff --git a/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/solution.md b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/solution.md new file mode 100644 index 00000000..c1d24900 --- /dev/null +++ b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/solution.md @@ -0,0 +1,55 @@ +Для определения примитивного типа строка/число подойдет оператор [typeof](#type-typeof). + +Примеры его работы: + +```js +//+ run +alert(typeof 123); // "number" +alert(typeof "строка"); // "string" +alert(typeof new Date()); // "object" +alert(typeof []); // "object" +``` + +Оператор `typeof` не умеет различать разные типы объектов, они для него все на одно лицо: `"object"`. Поэтому он не сможет отличить `Date` от `Array`. + +Используем для них утиную типизацию: + +Функция: + +```js +//+ run +function formatDate(date) { + if (typeof date == 'number') { + // перевести секунды в миллисекунды и преобразовать к Date + date = new Date(date*1000); + } else if(typeof date == 'string') { + // разобрать строку и преобразовать к Date + date = date.split('-'); + date = new Date(date[0], date[1]-1, date[2]); + } else if ( date.length ) { // есть длина, но не строка - значит массив + date = new Date(date[0], date[1], date[2]); + } + // преобразования для поддержки полиморфизма завершены, + // теперь мы работаем с датой (форматируем её) + + var day = date.getDate(); + if (day < 10) day = '0' + day; + + var month = date.getMonth()+1; + if (month < 10) month = '0' + month; + + // взять 2 последние цифры года + var year = date.getFullYear() % 100; + if (year < 10) year = '0' + year; + + var formattedDate = day + '.' + month + '.' + year; + + return formattedDate; +} + +alert( formatDate( '2011-10-02' ) ); // 02.10.11 +alert( formatDate( 1234567890 ) ); // 14.02.09 +alert( formatDate( [2014,0,1] ) ); // 01.01.14 +alert( formatDate( new Date(2014,0,1) ) ); // 01.01.14 +``` + diff --git a/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/task.md b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/task.md new file mode 100644 index 00000000..d12a3af9 --- /dev/null +++ b/1-js/4-data-structures/11-typeof-duck-typing/1-format-date-polymorphic/task.md @@ -0,0 +1,26 @@ +# Полиморфная функция formatDate + +[importance 5] + +Напишите функцию `formatDate(date)`, которая возвращает дату в формате `dd.mm.yy`. + +Ее первый аргумент должен содержать дату в одном из видов: +
      +
    1. Как объект `Date`.
    2. +
    3. Как строку в формате `yyyy-mm-dd`.
    4. +
    5. Как число *секунд* с `01.01.1970`.
    6. +
    7. Как массив `[гггг, мм, дд]`, месяц начинается с нуля
    8. +
    +Для этого вам понадобится определить тип данных аргумента и, при необходимости, преобразовать входные данные в нужный формат. + +Пример работы: + +```js +function formatDate(date) { /* ваш код */ } + +alert( formatDate( '2011-10-02' ) ); // 02.10.11 +alert( formatDate( 1234567890 ) ); // 14.02.09 +alert( formatDate( [2014,0,1] ) ); // 01.01.14 +alert( formatDate( new Date(2014,0,1) ) ); // 01.01.14 +``` + diff --git a/1-js/4-data-structures/11-typeof-duck-typing/article.md b/1-js/4-data-structures/11-typeof-duck-typing/article.md new file mode 100644 index 00000000..cfcfe2d4 --- /dev/null +++ b/1-js/4-data-structures/11-typeof-duck-typing/article.md @@ -0,0 +1,162 @@ +# Оператор typeof и утиная типизация + +В этой главе мы рассмотрим, как создавать *полиморфные* функции, то есть такие, которые по-разному обрабатывают аргументы, в зависимости от их типа. Например, функция вывода может по-разному форматировать числа и даты. + +Для реализации такой возможности нужен способ определить тип переменной. + +[cut] +Как мы знаем, существует несколько *примитивных типов*: +
    +
    `null`
    +
    Специальный тип, содержит только значение `null`.
    +
    `undefined`
    +
    Специальный тип, содержит только значение `undefined`.
    +
    `number`
    +
    Числа: `0`, `3.14`, а также значения `NaN` и `Infinity`
    +
    `boolean`
    +
    `true`, `false`.
    +
    `string`
    +
    Строки, такие как `"Мяу"` или пустая строка `""`.
    +
    + +Все остальные значения являются **объектами**, включая функции и массивы. + +## Оператор typeof [#type-typeof] + +Оператор `typeof` возвращает тип аргумента. У него есть два синтаксиса: +
      +
    1. Синтаксис оператора: `typeof x`.
    2. +
    3. Синтаксис функции: `typeof(x)`.
    4. +
    + +Работают они одинаково, но первый синтаксис короче. + +**Результатом `typeof` является строка, содержащая тип:** + +```js +typeof undefined // "undefined" + +typeof 0 // "number" + +typeof true // "boolean" + +typeof "foo" // "string" + +typeof {} // "object" + +*!* +typeof null // "object" +*/!* + +function f() { /* ... */ } +typeof f // "function" +*/!* +``` + +Последние две строки помечены, потому что `typeof` ведет себя в них по-особому. + +
      +
    1. Результат `typeof null == "object"` -- это официально признанная ошибка в языке, которая сохраняется для совместимости. + +На самом деле `null` -- это не объект, а примитив. Это сразу видно, если попытаться присвоить ему свойство: + +```js +//+ run +var x = null; +x.prop = 1; // ошибка, т.к. нельзя присвоить свойство примитиву +``` + +
    2. +
    3. Для функции `f` значением `typeof f` является `"function"`. На самом деле функция не является отдельным базовым типом в JavaScript, все функции являются объектами, но такое выделение функций на практике удобно, так как позволяет легко определить функцию.
    4. +
    + +**Оператор `typeof` надежно работает с примитивными типами, кроме `null`, а также с функциями. Но обычные объекты, массивы и даты для `typeof` все на одно лицо, они имеют тип `'object'`:** + +```js +//+ run +alert( typeof {} ); // 'object' +alert( typeof [] ); // 'object' +alert( typeof new Date ); // 'object' +``` + +Поэтому различить их при помощи `typeof` нельзя. + +## Утиная типизация + +Основная проблема `typeof` -- неумение различать объекты, кроме функций. Но есть и другой способ проверки типа. + +Так называемая "утиная типизация" основана на одной известной пословице: *"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`: + +```js +//+ run +var something = [1,2,3]; + +if (something.splice) { + alert('Массив!'); +} +``` + +Обратите внимание -- в `if` мы не вызываем метод `something.splice()`, а пробуем получить само свойство `something.splice`. Для массивов оно всегда есть и является функцией, т.е. даст в логическом контексте `true`. + +Проверить на дату можно, определив наличие метода `getTime`: + +```js +//+ run +var x = new Date(); + +if (x.getTime) { + alert('Дата!'); +} +``` + +С виду такая проверка хрупка, ее можно "сломать", передав похожий объект с тем же методом. + +Но как раз в этом и есть смысл утиной типизации: если объект похож на массив, у него есть методы массива, то будем работать с ним как с массивом (какая разница, что это на самом деле). + +## Полиморфизм + +Используем проверку типов для того, чтобы создать полиморфную функцию `sayHi(who)`, которая говорит "Привет" своему аргументу. + +При этом, если передали массив, она должна вызвать себя для каждого подэлемента. + +```js +//+ run +function sayHi(who) { + + if (who.splice) { // проверка на массив (или что-то похожее) + for(var i=0; i +
  • Он считает `null` объектом, это внутренняя ошибка в языке.
  • +
  • Для функций он возвращает `function`, по стандарту функция не считается базовым типом, но на практике это удобно и полезно.
  • + + +Там, где нужно различать объекты, обычно используется утиная типизация, то есть мы смотрим, есть ли в объекте нужный метод, желательно -- тот, который мы собираемся исползовать, но это не обязательно. + diff --git a/1-js/4-data-structures/2-number/1-sum-interface/solution.md b/1-js/4-data-structures/2-number/1-sum-interface/solution.md new file mode 100644 index 00000000..d42523ce --- /dev/null +++ b/1-js/4-data-structures/2-number/1-sum-interface/solution.md @@ -0,0 +1,11 @@ + + +```js +//+ run demo +var a = +prompt("Введите первое число", ""); +var b = +prompt("Введите второе число", ""); + +alert( a + b ); +``` + +Обратите внимание на оператор `+` перед `prompt`, он сразу приводит вводимое значение к числу. Если бы его не было, то `a` и `b` были бы строками и складывались бы как строки, то есть `"1" + "2" = "12"`. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/1-sum-interface/task.md b/1-js/4-data-structures/2-number/1-sum-interface/task.md new file mode 100644 index 00000000..2aac44a6 --- /dev/null +++ b/1-js/4-data-structures/2-number/1-sum-interface/task.md @@ -0,0 +1,9 @@ +# Интерфейс суммы + +[importance 5] + +Создайте страницу, которая предлагает ввести два числа и выводит их сумму. + +[demo /] + +P.S. Есть "подводный камень" при работе с типами. diff --git a/1-js/4-data-structures/2-number/2-why-rounded-down/solution.md b/1-js/4-data-structures/2-number/2-why-rounded-down/solution.md new file mode 100644 index 00000000..574bf957 --- /dev/null +++ b/1-js/4-data-structures/2-number/2-why-rounded-down/solution.md @@ -0,0 +1,8 @@ +Во внутреннем двоичном представлении `6.35` является бесконечной двоичной дробью. Хранится она с потерей точности.. А впрочем, посмотрим сами: + +```js +//+ run +alert( 6.35.toFixed(20) ); // 6.34999999999999964473 +``` + +Интерпретатор видит число как `6.34...`, поэтому и округляет вниз. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/2-why-rounded-down/task.md b/1-js/4-data-structures/2-number/2-why-rounded-down/task.md new file mode 100644 index 00000000..203280c6 --- /dev/null +++ b/1-js/4-data-structures/2-number/2-why-rounded-down/task.md @@ -0,0 +1,19 @@ +# Почему 6.35.toFixed(1) == 6.3? + +[importance 4] + +В математике принято, что `5` округляется вверх, например: + +```js +//+ run +alert( 1.5.toFixed(0) ); // 2 +alert( 1.35.toFixed(1) ); // 1.4 +``` + +Но почему в примере ниже `6.35` округляется до `6.3`? + +```js +//+ run +alert( 6.35.toFixed(1) ); // 6.3 +``` + diff --git a/1-js/4-data-structures/2-number/3-sum-prices/solution.md b/1-js/4-data-structures/2-number/3-sum-prices/solution.md new file mode 100644 index 00000000..e01779c0 --- /dev/null +++ b/1-js/4-data-structures/2-number/3-sum-prices/solution.md @@ -0,0 +1,13 @@ +Есть два основных подхода. +
      +
    1. Можно хранить сами цены в "копейках" (центах и т.п.). Тогда они всегда будут целые и проблема исчезнет. Но при показе и при обмене данными нужно будет это учитывать и не забывать делить на 100.
    2. +
    3. При операциях, когда необходимо получить окончательный результат -- округлять до 2го знака после запятой. Все, что дальше -- ошибка округления: + +```js +//+ run +var price1 = 0.1, price2 = 0.2; +alert( +(price1 + price2).toFixed(2) ); +``` + +
    4. +
    \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/3-sum-prices/task.md b/1-js/4-data-structures/2-number/3-sum-prices/task.md new file mode 100644 index 00000000..02118152 --- /dev/null +++ b/1-js/4-data-structures/2-number/3-sum-prices/task.md @@ -0,0 +1,16 @@ +# Сложение цен + +[importance 5] + +Представьте себе электронный магазин. Цены даны с точностью до копейки(цента, евроцента и т.п.). + +Вы пишете интерфейс для него. Основная работа происходит на сервере, но и на клиенте все должно быть хорошо. Сложение цен на купленные товары и умножение их на количество является обычной операцией. + +Получится глупо, если при заказе двух товаров с ценами `0.10$` и `0.20$` человек получит общую стоимость `0.30000000000000004$`: + +```js +//+ run +alert( 0.1 + 0.2 + '$' ); +``` + +Что можно сделать, чтобы избежать проблем с ошибками округления? \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/4-endless-loop-error/solution.md b/1-js/4-data-structures/2-number/4-endless-loop-error/solution.md new file mode 100644 index 00000000..ef44f68b --- /dev/null +++ b/1-js/4-data-structures/2-number/4-endless-loop-error/solution.md @@ -0,0 +1,14 @@ +Потому что `i` никогда не станет равным `10`. + +Запустите, чтобы увидеть *реальные* значения `i`: + +```js +//+ run +var i = 0; +while(i < 11) { + i += 0.2; + if (i>9.8 && i<10.2) alert(i); +} +``` + +Ни одно из них в точности не равно `10`. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/4-endless-loop-error/task.md b/1-js/4-data-structures/2-number/4-endless-loop-error/task.md new file mode 100644 index 00000000..e0b500a5 --- /dev/null +++ b/1-js/4-data-structures/2-number/4-endless-loop-error/task.md @@ -0,0 +1,13 @@ +# Бесконечный цикл по ошибке + +[importance 4] + +Этот цикл - бесконечный. Почему? + +```js +var i = 0; +while(i != 10) { + i += 0.2; +} +``` + diff --git a/1-js/4-data-structures/2-number/5-get-decimal/_js.view/solution.js b/1-js/4-data-structures/2-number/5-get-decimal/_js.view/solution.js new file mode 100644 index 00000000..3b1a71d2 --- /dev/null +++ b/1-js/4-data-structures/2-number/5-get-decimal/_js.view/solution.js @@ -0,0 +1,7 @@ +function getDecimal(num) { + var str = "" + num; + var zeroPos = str.indexOf("."); + if (zeroPos == -1) return 0; + str = str.slice( zeroPos ); + return +str; +} \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/5-get-decimal/_js.view/test.js b/1-js/4-data-structures/2-number/5-get-decimal/_js.view/test.js new file mode 100644 index 00000000..2a759a5c --- /dev/null +++ b/1-js/4-data-structures/2-number/5-get-decimal/_js.view/test.js @@ -0,0 +1,21 @@ +describe("getDecimal", function() { + it("возвращает дробную часть 1.2 как 0.2", function() { + assert.equal( getDecimal(1.2), 0.2 ); + }); + + it("возвращает дробную часть 1.3 как 0.3", function() { + assert.equal( getDecimal(1.3), 0.3 ); + }); + + it("возвращает дробную часть 12.345 как 0.345", function() { + assert.equal( getDecimal(12.345), 0.345 ); + }); + + it("возвращает дробную часть -1.2 как 0.2", function() { + assert.equal( getDecimal(-1.2), 0.2 ); + }); + + it("возвращает дробную часть 5 как 0", function() { + assert.equal( getDecimal(5), 0 ); + }); +}); diff --git a/1-js/4-data-structures/2-number/5-get-decimal/solution.md b/1-js/4-data-structures/2-number/5-get-decimal/solution.md new file mode 100644 index 00000000..65cd4bc8 --- /dev/null +++ b/1-js/4-data-structures/2-number/5-get-decimal/solution.md @@ -0,0 +1,82 @@ +# Функция + +Первая идея может быть такой: + +```js +//+ run +function getDecimal(num) { + return num - Math.floor(num); +} + +alert( getDecimal(12.5) ); // 0.5 +*!* +alert( getDecimal(-1.2) ); // 0.8, неверно! +*/!* +``` + +Как видно из примера выше, для отрицательных чисел она не работает. + +Это потому, что округление `Math.floor` происходит всегда к ближайшему меньшему целому, то есть `Math.floor(-1.2) = -2`, а нам бы хотелось убрать целую часть, т.е. получить `-1`. + +Можно попытаться решить проблему так: + +```js +//+ run +function getDecimal(num) { + return num > 0 ? num - Math.floor(num) : Math.ceil(num) - num; +} + +alert( getDecimal(12.5) ); // 0.5 +*!* +alert( getDecimal(-1.2) ); // 0.19999999999999996, неверно! +alert( getDecimal(1.2) ); // 0.19999999999999996 +*/!* +``` + +Проблема с отрицательными числами решена, но результат, увы, не совсем тот. + +Внутреннее неточное представление чисел приводит к ошибке в вычислениях, которая проявляется при работе и с положительными и с отрицательными числами. + +Давайте попробуем ещё вариант -- получим остаток при делении на `1`. При таком делении от любого числа в остатке окажется именно дробная часть: + +```js +//+ run +function getDecimal(num) { + return num > 0 ? (num % 1) : (-num % 1); +} + +alert( getDecimal(12.5) ); // 0.5 +*!* +alert( getDecimal(1.2) ); // 0.19999999999999996, неверно! +*/!* +``` + +В общем-то, работает, функция стала короче, но, увы, ошибка сохранилась. + +Что делать? + +Увы, операции с десятичными дробями подразумевают некоторую потерю точности. + +Зависит от ситуации. +
      +
    • Если внешний вид числа неважен и ошибка в вычислениях допустима -- она ведь очень мала, то можно оставить как есть.
    • +
    • Перейти на промежуточные целочисленные вычисления там, где это возможно.
    • +
    • Если мы знаем, что десятичная часть жёстко ограничена, к примеру, может содержать не более 2 знаков то можно округлить число, то есть вернуть `+num.toFixed(2)`.
    • +
    + +Если эти варианты не подходят, то можно работать с числом как со строкой: + +```js +//+ run +function getDecimal(num) { + var str = "" + num; + var zeroPos = str.indexOf("."); + if (zeroPos == -1) return 0; + str = str.slice( zeroPos ); + return +str; +} + +alert( getDecimal(12.5) ); // 0.5 +alert( getDecimal(1.2) ); // 0.2 +``` + diff --git a/1-js/4-data-structures/2-number/5-get-decimal/task.md b/1-js/4-data-structures/2-number/5-get-decimal/task.md new file mode 100644 index 00000000..ec77059b --- /dev/null +++ b/1-js/4-data-structures/2-number/5-get-decimal/task.md @@ -0,0 +1,12 @@ +# Как получить дробную часть числа? + +[importance 4] + +Напишите функцию `getDecimal(num)`, которая возвращает десятичную часть числа: + +```js +alert( getDecimal(12.345) ); // 0.345 +alert( getDecimal(1.2) ); // 0.2 +alert( getDecimal(-1.2) ); // 0.2 +``` + diff --git a/1-js/4-data-structures/2-number/6-formula-binet/solution.md b/1-js/4-data-structures/2-number/6-formula-binet/solution.md new file mode 100644 index 00000000..d6628450 --- /dev/null +++ b/1-js/4-data-structures/2-number/6-formula-binet/solution.md @@ -0,0 +1,33 @@ + + +```js +//+ run +function fibBinet(n) { + var phi = (1 + Math.sqrt(5)) / 2; + // используем Math.round для округления до ближайшего целого + return Math.round( Math.pow(phi, n) / Math.sqrt(5) ); +} + +function fib(n){ + var a=1, b=0, x; + for(i=0; iF77 получился неверным!** + +Причина -- в ошибках округления, ведь √5 -- бесконечная дробь. + +Ошибки округления при вычислениях множатся и, в итоге, дают расхождение. \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/6-formula-binet/task.md b/1-js/4-data-structures/2-number/6-formula-binet/task.md new file mode 100644 index 00000000..80d4b9ad --- /dev/null +++ b/1-js/4-data-structures/2-number/6-formula-binet/task.md @@ -0,0 +1,27 @@ +# Формула Бине + +[importance 4] + +Последовательность [чисел Фибоначчи](http://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%A4%D0%B8%D0%B1%D0%BE%D0%BD%D0%B0%D1%87%D1%87%D0%B8) имеет формулу Fn = Fn-1 + Fn-2. То есть, следующее число получается как сумма двух предыдущих. + +Первые два числа равны `1`, затем `2(1+1)`, затем `3(1+2)`, `5(2+3)` и так далее: `1, 1, 2, 3, 5, 8, 13, 21...`. + +Код для их вычисления (из задачи [](/task/fibonacci-numbers)): + +```js +function fib(n){ + var a=1, b=0, x; + for(i=0; iFn равно ближайшему целому для ϕn/√5, где ϕ=(1+√5)/2 -- золотое сечение. + +Напишите функцию `fibBinet(n)`, которая будет вычислять Fn, используя эту формулу. Проверьте её для значения F77 (должно получиться `fibBinet(77) = 5527939700884757`). + +**Одинаковы ли результаты, полученные при помощи кода `fib(n)` выше и по формуле Бине? Если нет, то почему и какой из них верный?** \ No newline at end of file diff --git a/1-js/4-data-structures/2-number/7-random-0-max/solution.md b/1-js/4-data-structures/2-number/7-random-0-max/solution.md new file mode 100644 index 00000000..fef2bf9c --- /dev/null +++ b/1-js/4-data-structures/2-number/7-random-0-max/solution.md @@ -0,0 +1,9 @@ +Сгенерируем значение в диапазоне `0..1` и умножим на `max`: + +```js +//+ run +var max = 10; + +alert( Math.random()*max ); +``` + diff --git a/1-js/4-data-structures/2-number/7-random-0-max/task.md b/1-js/4-data-structures/2-number/7-random-0-max/task.md new file mode 100644 index 00000000..a7175b55 --- /dev/null +++ b/1-js/4-data-structures/2-number/7-random-0-max/task.md @@ -0,0 +1,5 @@ +# Случайное из интервала (0, max) + +[importance 2] + +Напишите код для генерации случайного значения в диапазоне от `0` до `max`, не включая `max`. diff --git a/1-js/4-data-structures/2-number/8-random-min-max/solution.md b/1-js/4-data-structures/2-number/8-random-min-max/solution.md new file mode 100644 index 00000000..896d6ff6 --- /dev/null +++ b/1-js/4-data-structures/2-number/8-random-min-max/solution.md @@ -0,0 +1,9 @@ +Сгенерируем значение из интервала `0..max-min`, а затем сдвинем на `min`: + +```js +//+ run +var min=5, max = 10; + +alert( min + Math.random()*(max-min) ); +``` + diff --git a/1-js/4-data-structures/2-number/8-random-min-max/task.md b/1-js/4-data-structures/2-number/8-random-min-max/task.md new file mode 100644 index 00000000..e5e8bf18 --- /dev/null +++ b/1-js/4-data-structures/2-number/8-random-min-max/task.md @@ -0,0 +1,5 @@ +# Случайное из интервала (min, max) + +[importance 2] + +Напишите код для генерации случайного числа от `min` до `max`, не включая `max`. diff --git a/1-js/4-data-structures/2-number/9-random-int-min-max/solution.md b/1-js/4-data-structures/2-number/9-random-int-min-max/solution.md new file mode 100644 index 00000000..09584440 --- /dev/null +++ b/1-js/4-data-structures/2-number/9-random-int-min-max/solution.md @@ -0,0 +1,78 @@ +# Очевидное неверное решение (round) + +Самый простой, но неверный способ -- это сгенерировать значение в интервале `min..max` и округлить его `Math.round`, вот так: + +```js +//+ run +function randomInteger(min, max) { + var rand = min + Math.random()*(max-min) + rand = Math.round(rand); + return rand; +} + +alert( randomInteger(1, 3) ); +``` + +Эта функция работает. Но при этом она некорректна: вероятность получить крайние значения `min` и `max` будет в два раза меньше, чем любые другие. + +При многократном запуске этого кода вы легко заметите, что `2` выпадает чаще всех. + +Это происходит из-за того, что `Math.round()` получает разнообразные случайные числа из интервала от `1` до `3`, но при округлении до ближайшего целого получится, что: + +```js +значения из диапазона 1 ... 1.49999.. станут 1 +значения из диапазона 1.5 ... 2.49999.. станут 2 +значения из диапазона 2.5 ... 2.99999.. станут 3 +``` + +Отсюда явно видно, что в `1` (как и `3`) попадает диапазон значений в два раза меньший, чем в `2`. Из-за этого такой перекос. + +# Верное решение с round + +Правильный способ: `Math.round(случайное от min-0.5 до max+0.5)` + +```js +//+ run +*!* +function randomInteger(min, max) { + var rand = min - 0.5 + Math.random()*(max-min+1) + rand = Math.round(rand); + return rand; +} +*/!* + +alert( randomInteger(5, 10) ); +``` + +В этом случае диапазон будет тот же (`max-min+1`), но учтена механика округления `round`. + +# Решение с floor + +Альтернативный путь - применить округление `Math.floor()` к случайному числу от `min` до `max+1`. + +Например, для генерации целого числа от `1` до `3`, создадим вспомогательное случайное значение от `1` до `4` (не включая `4`). + +Тогда `Math.floor()` округлит их так: + +```js +1 ... 1.999+ станет 1 +2 ... 2.999+ станет 2 +3 ... 3.999+ станет 3 +``` + +Все диапазоны одинаковы. +Итак, код: + +```js +//+ run +*!* +function randomInteger(min, max) { + var rand = min + Math.random() * (max+1-min); + rand = Math.floor(rand); + return rand; +} +*/!* + +alert( randomInteger(5, 10) ); +``` + diff --git a/1-js/4-data-structures/2-number/9-random-int-min-max/task.md b/1-js/4-data-structures/2-number/9-random-int-min-max/task.md new file mode 100644 index 00000000..7f747a25 --- /dev/null +++ b/1-js/4-data-structures/2-number/9-random-int-min-max/task.md @@ -0,0 +1,7 @@ +# Случайное целое от min до max + +[importance 2] + +Напишите функцию `randomInteger(min, max)` для генерации случайного **целого** числа между `min` и `max`, включая `min,max` как возможные значения. + +Любое число из интервала `min..max` должно иметь одинаковую вероятность. diff --git a/1-js/4-data-structures/2-number/article.md b/1-js/4-data-structures/2-number/article.md new file mode 100644 index 00000000..2787ce9d --- /dev/null +++ b/1-js/4-data-structures/2-number/article.md @@ -0,0 +1,615 @@ +# Числа + +Все числа в JavaScript, как целые так и дробные, имеют тип `Number` и хранятся в 64-битном формате [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), также известном как "double precision". + +Здесь мы рассмотрим различные тонкости, связанные с работой с числами в JavaScript. + +## Способы записи + +В JavaScript можно записывать числа не только в десятичной, но и в шестнадцатеричной (начинается с `0x`), а также восьмеричной (начинается с `0`) системах счисления: + +```js +//+ run +alert( 0xFF ); // 255 в шестнадцатиричной системе +alert( 010 ); // 8 в восьмеричной системе +``` + +Также доступна запись в *"научном формате"* (ещё говорят "запись с плавающей точкой"), который выглядит как `<число>e<кол-во нулей>`. + +Например, `1e3` -- это `1` с `3` нулями, то есть `1000`. + +```js +//+ run +// еще пример научной формы: 3 с 5 нулями +alert( 3e5 ); // 300000 +``` + +Если количество нулей отрицательно, то число сдвигается вправо за десятичную точку, так что получается десятичная дробь: + +```js +//+ run +// здесь 3 сдвинуто 5 раз вправо, за десятичную точку. +alert( 3e-5 ); // 0.00003 <-- 5 нулей, включая начальный ноль +``` + +## Деление на ноль, Infinity + +Представьте, что вы собираетесь создать новый язык... Люди будут называть его "JavaScript" (или LiveScript... неважно). + +Что должно происходить при попытке деления на ноль? + +Как правило, ошибка в программе... Во всяком случае, в большинстве языков программирования это именно так. + +Но создатель JavaScript решил пойти математически правильным путем. Ведь чем меньше делитель, тем больше результат. При делении на очень-очень маленькое число должно получиться очень большое. В математическом анализе это описывается через [пределы](http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%B5%D0%BB_(%D0%BC%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)), и если подразумевать предел, то в качестве результата деления на `0` мы получаем "бесконечность", которая обозначается символом `∞` или, в JavaScript: `"Infinity"`. + +```js +//+ run +alert(1/0); // Infinity +alert(12345/0); // Infinity +``` + +**`Infinity` -- особенное численное значение, которое ведет себя в точности как математическая бесконечность `∞`.** +
      +
    • `Infinity` больше любого числа.
    • +
    • Добавление к бесконечности не меняет её.
    • +
    + +```js +//+ run +alert(Infinity > 1234567890); // true +alert(Infinity + 5 == Infinity); // true +``` + +**Бесконечность можно присвоить и в явном виде: `var x = Infinity`.** + +Бывает и минус бесконечность `-Infinity`: + +```js +//+ run +alert( -1 / 0 ); // -Infinity +``` + +Бесконечность можно получить также, если сделать ну очень большое число, для которого количество разрядов в двоичном представлении не помещается в соответствующую часть стандартного 64-битного формата, например: + +```js +//+ run +alert( 1e500 ); // Infinity +``` + +## NaN + +Если математическая операция не может быть совершена, то возвращается специальное значение `NaN` (Not-A-Number). + +Например, деление `0/0` в математическом смысле неопределено, поэтому возвращает `NaN`: + +```js +//+ run +alert( 0 / 0 ); // NaN +``` + +Значение `NaN` используется для обозначения математической ошибки и обладает следующими свойствами: + +
      +
    • Значение `NaN` -- единственное, в своем роде, которое *не равно ничему, включая себя*. + +Следующий код ничего не выведет: + +```js +//+ run +if (NaN == NaN) alert("=="); // Ни один вызов +if (NaN === NaN) alert("==="); // не сработает +``` + +
    • +
    • Значение `NaN` можно проверить специальной функцией `isNaN(n)`, которая возвращает `true` если аргумент -- `NaN` и `false` для любого другого значения. + +```js +//+ run +var n = 0/0; + +alert( isNaN(n) ); // true +``` + +[smart] +Отсюда вытекает забавный способ проверки значения на `NaN`: можно проверить его на равенство самому себе, вот так: + +```js +//+ run +var n = 0/0; + +if (n !== n) alert('n = NaN!'); +``` + +Это работает, но для наглядности лучше использовать `isNaN(n)`. +[/smart] +
    • +
    • Значение `NaN` "прилипчиво". Любая операция с `NaN` возвращает `NaN`. + +```js +//+ run +alert( NaN + 1 ); // NaN +``` + +
    • +
    + +Если аргумент `isNaN` -- не число, то он автоматически преобразуется к числу. + + +[summary]Никакие математические операции в JavaScript не могут привести к ошибке или "обрушить" программу. + +В худшем случае, результат будет `NaN`. +[/summary] + +## isFinite(n) + +Итак, в JavaScript есть обычные числа и три специальных числовых значения: `NaN`, `Infinity` и `-Infinity`. + +**Функция `isFinite(n)` возвращает `true` только тогда, когда `n` -- обычное число, а не одно из этих значений:** + +```js +//+ run +alert( isFinite(1) ); // true +alert( isFinite(Infinity) ); // false +alert( isFinite(NaN) ); // false +``` + +Если аргумент `isFinite` -- не число, то он автоматически преобразуется к числу. + + +## Преобразование к числу + +Строгое преобразование можно осуществить унарным плюсом `'+'`: + +```js +//+ run +var s = "12.34"; +alert( +s ); // 12.34 +``` + +*Строгое* -- означает, что если строка не является в точности числом, то результат будет `NaN`: + +```js +//+ run +alert( +"12test" ); // NaN +``` + +Единственное исключение -- пробельные символы в начале и в конце строки, которые игнорируются: + +```js +//+ run +alert( +" -12"); // -12 +alert( +" \n34 \n"); // 34, перевод строки \n является пробельным символом +alert( +"" ); // 0, пустая строка становится нулем +alert( +"1 2" ); // NaN, пробел посередине числа - ошибка +``` + +Аналогичным образом происходит преобразование и в других математических операторах и функциях: + +```js +//+ run +alert( '12.34' / "-2" ); // -6.17 +``` + +### isNaN -- проверка на число для строк + +Функция `isNaN` является математической, она преобразует аргумент в число, а затем проверяет, `NaN` это или нет. + +Поэтому можно использовать ее для проверки: + +```js +//+ run +var x = "-11.5"; +if (isNaN(x)) { + alert("Строка преобразовалась в NaN. Не число"); +} else { + alert("Число"); +} +``` + +Единственный тонкий момент -- в том, что пустая строка и строка из пробельных символов преобразуются к `0`: + +```js +//+ run +alert(isNaN(" \n\n ")) // false, т.к. строка из пробелов преобразуется к 0 +``` + +В случае, если применить такую проверку не к строке, то могут быть сюрпризы, в частности `isNaN` посчитает числами значения `false, true, null`, так как они хотя и не числа, но преобразуются к ним: + +```js ++false = 0 // isNaN(false) преобразует false в число, получится 0 - ок ++true = 1 // тоже ок ++null = 0 // тоже ок ++undefined = NaN; // а вот это точно не число +``` + +## Мягкое преобразование: parseInt и parseFloat + +В мире HTML/CSS многие значения не являются в точности числами. Например, метрики CSS: `10pt` или `-12px`. + +Оператор `'+'` для таких значений возвратит `NaN`: + +```js +//+ run +alert( +"12px" ) // NaN +``` + +Для удобного чтения таких значений существует функция `parseInt`: + +```js +//+ run +alert( parseInt('12px') ); // 12 +``` + +**`parseInt` и ее аналог `parseFloat` преобразуют строку символ за символом, пока это возможно.** + +При возникновении ошибки возвращается число, которое получилось. `parseInt` читает из строки целое число, а `parseFloat` -- дробное. + +```js +//+ run +alert( parseInt('12px') ) // 12, ошибка на символе 'p' +alert( parseFloat('12.3.4') ) // 12.3, ошибка на второй точке +``` + +Конечно, существуют ситуации, когда `parseInt/parseFloat` возвращают `NaN`. Это происходит при ошибке на первом же символе: + +```js +//+ run +alert( parseInt('a123') ); // NaN +``` + +[warn header="Ошибка `parseInt('0..')`"] + +`parseInt` (но не `parseFloat`) понимает 16-ричную систему счисления: + +```js +//+ run +alert( parseInt('0xFF') ) // 255 +``` + +В старом стандарте JavaScript он умел понимать и восьмеричную: + +```js +//+ run +alert( parseInt('010') ) // в некоторых браузерах 8 +``` + +Если вы хотите быть уверенным, что число, начинающееся с нуля, будет интерпретировано верно -- используйте второй необязательный аргумент `parseInt` -- основание системы счисления: + +```js +//+ run +alert( parseInt('010', 10) ); // во всех браузерах 10 +``` + +[/warn] + +## Проверка на число для всех типов + +Если вам нужна действительно точная проверка на число, которая не считает числом строку из пробелов, логические и специальные значения -- используйте следующую функцию `isNumeric`: + +```js +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(основание системы)`, например: + +```js +//+ run +var n = 255; + +alert( n.toString(16) ); // ff +``` + +Основание может быть любым от `2` до `36`. + +
      +
    • Основание `2` бывает полезно для отладки битовых операций, которые мы пройдём чуть позже: + +```js +//+ run +var n = 4; +alert( n.toString(2) ); // 100 +``` + +
    • +
    • Основание `36` (по количеству букв в английском алфавите -- 26, вместе с цифрами, которых 10) используется для того, чтобы "кодировать" число в виде буквенно-цифровой строки. В этой системе счисления сначала используются цифры, а затем буквы от `a` до `z`: + +```js +//+ run +var n = 1234567890; +alert( n.toString(36) ); // kf12oi +``` + +При помощи такого кодирования можно сделать длинный цифровой идентификатор короче, чтобы затем использовать его в URL. +
    • +
    + + + + +## Округление + +Одна из самых частых операций с числом -- округление. В JavaScript существуют целых 3 функции для этого. + +
    +
    `Math.floor`
    +
    Округляет вниз
    +
    `Math.ceil`
    +
    Округляет вверх
    +
    `Math.round`
    +
    Округляет до ближайшего целого
    +
    + +```js +//+ run +alert( Math.floor(3.1) ); // 3 +alert( Math.ceil(3.1) ); // 4 +alert( Math.round(3.1) ); // 3 +``` + +[smart header="Округление битовыми операторами"] +[Битовые операторы](/bitwise-operators) делают любое число 32-битным целым, обрезая десятичную часть. + +В результате побитовая операция, которая не изменяет число, например, двойное битовое НЕ -- округляет его: + +```js +//+ run +alert( ~~12.3 ); // 12 +``` + +Любая побитовая операция такого рода подойдет, например XOR (исключающее ИЛИ, `"^"`) с нулем: + +```js +//+ run +alert( 12.3 ^ 0 ); // 12 +alert( 1.2 + 1.3 ^ 0); // 2, приоритет ^ меньше, чем + +``` + +Это удобно в первую очередь тем, что легко читается и не заставляет ставить дополнительные скобки как `Math.floor(...)`: + +```js +var x = a * b / c ^ 0; // читается так: "a*b/c *!*и округлить*/!*" +``` + +[/smart] + +### Округление до заданной точности + +Обычный трюк -- это умножить и поделить на 10 с нужным количеством нулей. Например, округлим `3.456` до 2го знака после запятой: + +```js +//+ run +var n = 3.456; +alert( Math.round( n * 100 ) / 100 ); // 3.456 -> 345.6 -> 346 -> 3.46 +``` + +Таким образом можно округлять число и вверх и вниз. + +### num.toFixed(precision) + +Существует специальный метод `num.toFixed(precision)`, который округляет число `num` до точности `precision` и возвращает результат *в виде строки*: + +```js +//+ run +var n = 12.34; +alert( n.toFixed(1) ); // "12.3" +``` + +Округление идёт до ближайшего значения, аналогично `Math.round`: + +```js +//+ run +var n = 12.36; +alert( n.toFixed(1) ); // "12.4" +``` + +Итоговая строка, при необходимости, дополняется нулями до нужной точности: + +```js +//+ run +var n = 12.34; +alert( n.toFixed(5) ); // "12.34000", добавлены нули до 5 знаков после запятой +``` + +Если нам нужно именно число, то мы можем получить его, применив `'+'` к результату `n.toFixed(..)`: + +```js +//+ run +var n = 12.34; +alert( +n.toFixed(5) ); // 12.34 +``` + +[warn header="Метод `toFixed` не эквивалентен `Math.round`!"] +Например, произведём округление до одного знака после запятой с использованием двух способов: + +```js +//+ run +var price = 6.35; + +alert( price.toFixed(1) ); // 6.3 +alert( Math.round(price*10)/10 ); // 6.4 +``` + +Как видно, результат разный! Вариант округления через `Math.round` получился более корректным, так как по общепринятым правилам `5` округляется вверх. А `toFixed` может округлить его как вверх, так и вниз. Почему? Скоро узнаем! +[/warn] + + +## Неточные вычисления + +Запустите этот пример: + +```js +//+ run +alert(0.1 + 0.2 == 0.3); +``` + +Запустили? Если нет -- все же сделайте это. + +Ок, вы запустили его. Результат несколько странный, не так ли? Возможно, ошибка в браузере? Поменяйте браузер, запустите еще раз. + +Хорошо, теперь мы можем быть уверены: `0.1 + 0.2` это не `0.3`. Но тогда что же это? + +```js +//+ run +alert(0.1 + 0.2); // 0.30000000000000004 +``` + +Как видите, произошла небольшая вычислительная ошибка. + +Дело в том, что в стандарте IEEE 754 на число выделяется ровно 8 байт(=64 бита), не больше и не меньше. + +Число `0.1 (=1/10)` короткое в десятичном формате, а в двоичной системе счисления это бесконечная дробь ([перевод десятичной дроби в двоичную систему](http://www.klgtu.ru/students/literature/inf_asu/1760.html)). Также бесконечной дробью является `0.2 (=2/10)`. + +Двоичное значение бесконечных дробей хранится только до определенного знака, поэтому возникает неточность. Это даже можно увидеть: + +```js +//+ run +alert( 0.1.toFixed(20) ); // 0.10000000000000000555 +``` + +Когда мы складываем `0.1` и `0.2`, то две неточности складываются, получаем третью. + +Конечно, это не означает, что точные вычисления для таких чисел невозможны. Они возможны. И даже необходимы. + +Например, есть два способа сложить `0.1` и `0.2`: +
      +
    1. Сделать их целыми, сложить, а потом поделить: + +```js +//+ run +alert( (0.1*10 + 0.2*10) / 10 ); // 0.3 +``` + +Это работает, т.к. числа `0.1*10 = 1` и `0.2*10 = 2` могут быть точно представлены в двоичной системе. +
    2. +
    3. Сложить, а затем округлить до разумного знака после запятой. Округления до 10-го знака обычно бывает достаточно, чтобы отсечь ошибку вычислений: + +```js +//+ run +var result = 0.1 + 0.2; +alert( +result.toFixed(10) ); // 0.3 +``` + +
    4. +
    + + + +[smart header="Забавный пример"] +Привет! Я -- число, растущее само по себе! + +```js +//+ run +alert(9999999999999999); +``` + +Причина та же -- потеря точности. + +Из `64` бит, отведённых на число, сами цифры числа занимают до `52` бит, остальные `11` бит хранят позицию десятичной точки и один бит -- знак. Так что если `52` бит не хватает на цифры, то при записи пропадут младшие разряды. + +Интерпретатор не выдаст ошибку, но в результате получится "не совсем то число", что мы и видим в примере выше. Как говорится: "как смог, так записал". + +[/smart] + +Ради справедливости заметим, что в точности то же самое происходит в любом другом языке, где используется формат 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](http://en.wikipedia.org/wiki/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(не включая). Генератор случайных чисел инициализуется текущим временем.
    +
    + +### Форматирование + +Для красивого вывода чисел в стандарте [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf) есть метод `toLocaleString()`: + +```js +//+ run +var number = 123456789; + +alert( number.toLocaleString() ); // 123 456 789 +``` + +Его поддерживают все современные браузеры, кроме IE10- (для которых нужно подключить библиотеку [Intl.JS](https://github.com/andyearnshaw/Intl.js/)). Он также умеет форматировать валюту и проценты. Более подробно про устройство этого метода можно будет узнать в статье [](/intl), когда это вам понадобится. + +## Итого + +
      +
    • Числа могут быть записаны в шестнадцатиричной, восьмеричной системе, а также "научным" способом.
    • +
    • В JavaScript существует числовое значение бесконечность `Infinity`.
    • +
    • Ошибка вычислений дает `NaN`.
    • +
    • Арифметические и математические функции преобразуют строку в точности в число, игнорируя начальные и конечные пробелы.
    • +
    • Функции `parseInt/parseFloat` делают числа из строк, которые начинаются с числа.
    • +
    • Есть четыре способа округления: `Math.floor`, `Math.round`, `Math.ceil` и битовый оператор. Для округления до нужного знака используйте `+n.toFixed(p)` или трюк с умножением и делением на 10p.
    • +
    • Дробные числа дают ошибку вычислений. При необходимости ее можно отсечь округлением до нужного знака.
    • +
    • Случайные числа от `0` до `1` генерируются с помощью `Math.random()`, остальные -- преобразованием из них.
    • +
    + +Существуют и другие математические функции. Вы можете ознакомиться с ними в справочнике в разделах Number и Math. + + + + + + + + diff --git a/1-js/4-data-structures/3-object/1-hello-object/solution.md b/1-js/4-data-structures/3-object/1-hello-object/solution.md new file mode 100644 index 00000000..465bf1ad --- /dev/null +++ b/1-js/4-data-structures/3-object/1-hello-object/solution.md @@ -0,0 +1,10 @@ + + +```js +var user = {}; +user.name = "Вася"; +user.surname = "Петров"; +user.name = "Сергей"; +delete user.name; +``` + diff --git a/1-js/4-data-structures/3-object/1-hello-object/task.md b/1-js/4-data-structures/3-object/1-hello-object/task.md new file mode 100644 index 00000000..b0dfdaa3 --- /dev/null +++ b/1-js/4-data-structures/3-object/1-hello-object/task.md @@ -0,0 +1,13 @@ +# Первый объект + +[importance 3] + +Мини-задача на синтаксис объектов. Напишите код, по строке на каждое действие. +
      +
    1. Создайте пустой объект `user`.
    2. +
    3. Добавьте свойство `name` со значением `Вася`.
    4. +
    5. Добавьте свойство `surname` со значением `Петров`.
    6. +
    7. Поменяйте значение `name` на `Сергей`.
    8. +
    9. Удалите свойство `name` из объекта.
    10. +
    + diff --git a/1-js/4-data-structures/3-object/article.md b/1-js/4-data-structures/3-object/article.md new file mode 100644 index 00000000..568ecdf4 --- /dev/null +++ b/1-js/4-data-structures/3-object/article.md @@ -0,0 +1,328 @@ +# Объекты как ассоциативные массивы + +Объекты в JavaScript являются "двуличными". Они сочетают в себе два важных функционала. + +Первый -- это ассоциативный массив: структура, пригодная для хранения любых данных. В этой главе мы рассмотрим использование объектов именно как массивов. + +Второй -- языковые возможности для объектно-ориентированного программирования. Эти возможности мы изучим в последующих разделах учебника. + +[cut] +## Ассоциативные массивы + +[Ассоциативный массив](http://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2) -- структура данных, в которой можно хранить любые данные в формате ключ-значение. + +Её можно легко представить как шкаф с подписанными ящиками. Все данные хранятся в ящичках. По имени можно легко найти ящик и взять то значение, которое в нём лежит. + +В отличие от реальных шкафов, в ассоциативный массив можно в любой момент добавить новые именованные "ящики" или удалить существующие. Далее мы увидим примеры, как это делается. + +Кстати, в других языках программирования такую структуру данных также называют *"словарь"* и *"хэш"*. + +## Создание объектов + +Пустой объект (*"пустой шкаф"*) может быть создан одним из двух синтаксисов: + +```js +1. o = new Object(); +2. o = {}; // пустые фигурные скобки +``` + +Обычно все пользуются синтаксисом `(2)`, т.к. он короче. + +## Операции с объектом + +Объект может содержать в себе любые значения, которые называются *свойствами объекта*. Доступ к свойствам осуществляется по *имени свойства* (иногда говорят *"по ключу"*). + +Например, создадим объект `person` для хранения информации о человеке: + +```js +var person = {}; // пока пустой +``` + +Основные операции с объектами -- это: +
      +
    1. **Присвоение свойства по ключу.**
    2. +
    3. **Чтение свойства по ключу.**
    4. +
    5. **Удаление свойства по ключу.**
    6. +
    + +Для обращения к свойствам используется запись "через точку", вида `объект.свойство`: + +```js +//+ run +var person = {}; + +// *!*1. присвоение*/!* +// при присвоении свойства в объекте автоматически создаётся "ящик" +// с именем "name" и в него записывается содержимое 'Вася' +person.name = 'Вася'; + +person.age = 25; // запишем ещё одно свойство: с именем 'age' и значением 25 + +// *!*2. чтение*/!* +alert(person.name + ': ' + person.age); // вывести значения + +// *!*3. удаление*/!* +delete person.name; // удалить "ящик" с именем "name" вместе со значением в нём +``` + +Следующая операция: +
      +
    1. **Проверка существования свойства с определенным ключом.**
    2. +
    + +Например, есть объект `person`, и нужно проверить, существует ли в нем свойство `age`. + +Для проверки существования есть оператор `in`. Его синтаксис: `"prop" in obj`, причем имя свойства -- в виде строки, например: + +```js +var person = { }; + +if (*!*"age" in person*/!*) { + alert("Свойство age существует!"); +} +``` + +Впрочем, чаще используется другой способ -- сравнение значения с `undefined`. + +Дело в том, что **в JavaScript можно обратиться к любому свойству объекта, даже если его нет**. Ошибки не будет. + +Но если свойство не существует, то вернется специальное значение `undefined`: + +```js +//+ run +var person = {}; + +alert(person.lalala); // undefined, нет свойства с ключом lalala +``` + +Таким образом **мы можем легко проверить существование свойства -- получив его и сравнив с `undefined`**: + +```js +//+ run +var person = { name: "Василий" }; + +alert(person.lalala === undefined); // true, свойства нет +alert(person.name === undefined); // false, свойство есть. +``` + +[smart header="Разница между проверками `in` и `=== undefined`"] + +Есть два средства для проверки наличия свойства в объекте: первое -- оператор `in`, второе -- получить его и сравнить его с `undefined`. + +Они почти идентичны, но есть одна небольшая разница. + +Дело в том, что технически возможно, что *свойство есть и равно `undefined`*: + +```js +//+ untrusted refresh run +var obj = {}; +obj.test = undefined; // добавили свойство со значением undefined + +*!* +// проверим наличие свойств test и заведомо отсутствующего blabla +alert(obj.test === undefined); // true +alert(obj.blabla === undefined); // true +*/!* +``` + +...При этом, как видно из кода, при простом сравнении наличие такого свойства будет неотличимо от его отсутствия. + +Но оператор `in` гарантирует правильный результат: + +```js +//+ untrusted refresh run +var obj = {}; +obj.test = undefined; + +*!* +alert( "test" in obj ); // true +alert( "blabla" in obj ); // false +*/!* +``` + +Как правило, в коде мы не будем присваивать `undefined`, чтобы корректно работали обе проверки. А в качестве значения, обозначающего неизвестность и неопределенность, будем использовать `null`. +[/smart] + +### Доступ через квадратные скобки + +Существует альтернативный синтаксис работы со свойствами, использующий квадратные скобки `объект['свойство']`: + +```js +//+ run +var person = {}; + +person['name'] = 'Вася'; // то же что и person.name = 'Вася' +``` + +Записи `person['name']` и `person.name` идентичны, но квадратные скобки позволяют использовать в качестве имени свойства любую строку: + +```js +//+ run +var person = {}; + +person['любимый стиль музыки'] = 'Джаз'; // то же что и person.name = 'Вася' +``` + +Такое присвоение было бы невозможно "через точку", так интерпретатор после первого пробела подумает, что свойство закончилось, и далее выдаст ошибку: + +```js +//+ run +person.любимый стиль музыки = 'Джаз'; // ??? ошибка +``` + +В обоих случаях, **имя свойства обязано быть строкой**. Если использовано значение другого типа -- JavaScript приведет его к строке автоматически. + +### Доступ к свойству через переменную + +Квадратные скобки также позволяют обратиться к свойству, имя которого хранится в переменной: + +```js +//+ run +var person = { age: 25 }; +var key = 'age'; + +alert( person[key] ); // выведет person['age'] +``` + +Вообще, если имя свойства хранится в переменной (`var key = "age"`), то единственный способ к нему обратиться -- это квадратные скобки `person[key]`. + +Доступ через точку используется, если мы на этапе написания программы уже знаем название свойства. А если оно будет определено по ходу выполнения, например, введено посетителем и записано в переменную, то единственный выбор -- квадратные скобки. + +### Объявление со свойствами + +Объект можно заполнить значениями при создании, указав их в фигурных скобках: `{ ключ1: значение1, ключ2: значение2, ... }`. + +Такой синтаксис называется *литеральным* (оригинал - *literal*), например: + + + +Следующие два фрагмента кода создают одинаковый объект: + +```js +var menuSetup = { + width: 300, + height: 200, + title: "Menu" +}; + +// то же самое, что: + +var menuSetup = {}; +menuSetup.width = 300; +menuSetup.height = 200; +menuSetup.title = 'Menu'; +``` + +**Названия свойств можно перечислять в кавычках или без, если они удовлетворяют ограничениям для имён переменных.** + +Например: + +```js +var menuSetup = { + width: 300, + 'height': 200, + "мама мыла раму": true +}; +``` + +**Значение у свойства может быть любое, в том числе и другой объект, который можно указать тут же:** + +```js +var user = { + name: "Таня", + age: 25, +*!* + size: { + top: 90, + middle: 60, + bottom: 90 + } +*/!* +} + +alert( user.name ) // "Таня" + +alert( user.size.top ) // 90 +``` + +Здесь значением свойства `size` является объект `{top: 90, middle: 60, bottom: 90 }`. +## Компактное представление объектов + +[warn header="Hardcore coders only"] +Эта секция относится ко внутреннему устройству структуры данных. Она не обязательна к прочтению. +[/warn] + +Браузер использует специальное "компактное" представление объектов, чтобы сэкономить память в том случае, когда однотипных объектов много. + +Например, посмотрим на такой объект: + +```js +var user = { + name: "Vasya", + age: 25 +}; +``` + +Здесь содержится информация о свойстве `name` и его строковом значении, а также о свойстве `age` и его численном значении. Представим, что таких объектов много. + +Получится, что информация об именах свойств `name` и `age` дублируется в каждом объекте. Чтобы этого избежать, браузер применяет оптимизацию. + +**При создании множества объектов одного и того же вида (с одинаковыми полями) интерпретатор выносит описание полей в отдельную структуру. А сам объект остаётся в виде непрерывной области памяти с данными.** + +Например, есть много объектов с полями `name` и `age`: + +```js +{name: "Вася", age: 25} +{name: "Петя", age: 22} +{name: "Маша", age: 19} +... +``` + +Для их эффективного хранения будет создана структура, которая описывает данный вид объектов. Выглядеть она будет примерно так: ``. А сами объекты будут представлены в памяти только данными: + +```js +<структура: string name, number age> +Вася 25 +Петя 22 +Маша 19 +``` + +При добавлении нового объекта такой структуры достаточно хранить значения полей, но не их имена. Экономия памяти -- налицо. + +А что происходит, если к объекту добавляется новое свойство? Например, к одному из них добавили свойство `isAdmin`: + +```js +user.isAdmin = true; +``` + +В этом случае браузер смотрит, есть ли уже структура, под которую подходит такой объект. Если нет -- она создаётся и объект привязывается к ней. + +**Эта оптимизация является примером того, что далеко не всё то, что мы пишем, один-в-один переносится в память.** + +Современные интерпретаторы очень стараются оптимизировать как код, так и структуры данных. Детали применения и реализации этого способа хранения варьируются от браузера к браузеру. О том, как это сделано в Chrome можно узнать, например, из презентации [Know Your Engines](http://www.slideshare.net/newmovie/know-yourengines-velocity2011). Она была некоторое время назад, но с тех пор мало что изменилось. + + +## Итого + +Объекты -- это ассоциативные массивы с дополнительными возможностями: + +
      +
    • Доступ к элементам осуществляется: +
        +
      • Напрямую по ключу `obj.prop = 5`
      • +
      • Через переменную, в которой хранится ключ: + +```js +var key = "prop"; +obj[key] = 5 +``` + +
      • +
      +
    • Удаление ключей: `delete obj.name`.
    • +
    • Существование свойства может проверять оператор `in`: `if ("prop" in obj)`, как правило, работает и просто сравнение `if (obj.prop !== undefined)`.
    • +
    + + + + diff --git a/1-js/4-data-structures/3-object/objectLiteral.png b/1-js/4-data-structures/3-object/objectLiteral.png new file mode 100755 index 00000000..fac01eb8 Binary files /dev/null and b/1-js/4-data-structures/3-object/objectLiteral.png differ diff --git a/1-js/4-data-structures/4-object-for-in/1-is-empty/_js.view/solution.js b/1-js/4-data-structures/4-object-for-in/1-is-empty/_js.view/solution.js new file mode 100644 index 00000000..ffd029a2 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/1-is-empty/_js.view/solution.js @@ -0,0 +1,6 @@ +function isEmpty(obj) { + for(var key in obj) { + return false; + } + return true; +} diff --git a/1-js/4-data-structures/4-object-for-in/1-is-empty/_js.view/test.js b/1-js/4-data-structures/4-object-for-in/1-is-empty/_js.view/test.js new file mode 100644 index 00000000..8a1e2796 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/1-is-empty/_js.view/test.js @@ -0,0 +1,9 @@ +describe("isEmpty", function() { + it("если объект пустой - возвращает true", function() { + assert.isTrue( isEmpty({}) ); + }); + + it("если у объекта есть любое свойство, не важно какое - возвращает false", function() { + assert.isFalse( isEmpty({ anything: false }) ); + }); +}); \ No newline at end of file diff --git a/1-js/4-data-structures/4-object-for-in/1-is-empty/solution.md b/1-js/4-data-structures/4-object-for-in/1-is-empty/solution.md new file mode 100644 index 00000000..8ecef652 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/1-is-empty/solution.md @@ -0,0 +1,20 @@ + + +```js +//+ run +function isEmpty(obj) { + for(var key in obj) { + return false; + } + return true; +} + +var schedule = {}; + +alert( isEmpty( schedule ) ); // true + +schedule["8:30"] = "подъём"; + +alert( isEmpty( schedule ) ); // false +``` + diff --git a/1-js/4-data-structures/4-object-for-in/1-is-empty/task.md b/1-js/4-data-structures/4-object-for-in/1-is-empty/task.md new file mode 100644 index 00000000..b3cae17d --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/1-is-empty/task.md @@ -0,0 +1,22 @@ +# Определите, пуст ли объект + +[importance 5] + +Создайте функцию `isEmpty(obj)`, которая возвращает `true`, если в объекте нет свойств и `false` -- если хоть одно свойство есть. + +Работать должно так: + +```js +function isEmpty(obj) { + /* ваш код */ +} + +var schedule = {}; + +alert( isEmpty( schedule ) ); // true + +schedule["8:30"] = "подъём"; + +alert( isEmpty( schedule ) ); // false +``` + diff --git a/1-js/4-data-structures/4-object-for-in/2-sum-salaries/solution.md b/1-js/4-data-structures/4-object-for-in/2-sum-salaries/solution.md new file mode 100644 index 00000000..6584b1e6 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/2-sum-salaries/solution.md @@ -0,0 +1,20 @@ + + +```js +//+ run +"use strict"; + +var salaries = { + "Вася": 100, + "Петя": 300, + "Даша": 250 +}; + +var sum = 0; +for(var name in salaries) { + sum += salaries[name]; +} + +alert(sum); +``` + diff --git a/1-js/4-data-structures/4-object-for-in/2-sum-salaries/task.md b/1-js/4-data-structures/4-object-for-in/2-sum-salaries/task.md new file mode 100644 index 00000000..2cf0015a --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/2-sum-salaries/task.md @@ -0,0 +1,23 @@ +# Сумма свойств + +[importance 5] + +Есть объект `salaries` с зарплатами. Напишите код, который выведет сумму всех зарплат. + +Если объект пустой, то результат должен быть `0`. + +Например: + +```js +"use strict"; + +var salaries = { + "Вася": 100, + "Петя": 300, + "Даша": 250 +}; + +//... ваш код выведет 650 +``` + +P.S. Сверху стоит `use strict`, чтобы не забыть объявить переменные. \ No newline at end of file diff --git a/1-js/4-data-structures/4-object-for-in/3-max-salary/solution.md b/1-js/4-data-structures/4-object-for-in/3-max-salary/solution.md new file mode 100644 index 00000000..f3ec028e --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/3-max-salary/solution.md @@ -0,0 +1,24 @@ + + +```js +//+ run +"use strict"; + +var salaries = { + "Вася": 100, + "Петя": 300, + "Даша": 250 +}; + +var max = 0; +var maxName = ""; +for(var name in salaries) { + if (max < salaries[name]) { + max = salaries[name]; + maxName = name; + } +} + +alert(maxName || "нет сотрудников"); +``` + diff --git a/1-js/4-data-structures/4-object-for-in/3-max-salary/task.md b/1-js/4-data-structures/4-object-for-in/3-max-salary/task.md new file mode 100644 index 00000000..6b95d9d7 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/3-max-salary/task.md @@ -0,0 +1,22 @@ +# Свойство с наибольшим значением + +[importance 5] + +Есть объект `salaries` с зарплатами. Напишите код, который выведет имя сотрудника, у которого самая большая зарплата. + +Если объект пустой, то пусть он выводит "нет сотрудников". + +Например: + +```js +"use strict"; + +var salaries = { + "Вася": 100, + "Петя": 300, + "Даша": 250 +}; + +// ... ваш код выведет "Петя" +``` + diff --git a/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/solution.js b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/solution.js new file mode 100644 index 00000000..fec115e0 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/solution.js @@ -0,0 +1,11 @@ +function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n) +} + +function multiplyNumeric(obj) { + for(var key in obj) { + if (isNumeric( obj[key] )) { + obj[key] *= 2; + } + } +} diff --git a/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/source.js b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/source.js new file mode 100644 index 00000000..558ef93e --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/source.js @@ -0,0 +1,5 @@ +function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n) +} + +// ... ваш код ... \ No newline at end of file diff --git a/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/test.js b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/test.js new file mode 100644 index 00000000..8bddc36d --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/_js.view/test.js @@ -0,0 +1,13 @@ +describe("multiplyNumeric", function() { + it("умножает численные свойства на 2", function() { + var menu = { + width: 200, + height: "300", + title: "Моё меню" + }; + multiplyNumeric(menu); + assert.equal( menu.width, 400 ); + assert.equal( menu.height, 600 ); + assert.equal( menu.title, "Моё меню" ); + }); +}); \ No newline at end of file diff --git a/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/solution.md b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/solution.md new file mode 100644 index 00000000..226af069 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/solution.md @@ -0,0 +1,27 @@ + + +```js +//+ run +var menu = { + width: 200, + height: 300, + title: "My menu" +}; + +function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +} + +function multiplyNumeric(obj) { + for(var key in obj) { + if (isNumeric( obj[key] )) { + obj[key] *= 2; + } + } +} + +multiplyNumeric(menu); + +alert("menu width="+menu.width+" height="+menu.height+" title="+menu.title); +``` + diff --git a/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/task.md b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/task.md new file mode 100644 index 00000000..15cd1fce --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/4-multiply-numeric/task.md @@ -0,0 +1,32 @@ +# Умножьте численные свойства на 2 + +[importance 3] + +Создайте функцию `multiplyNumeric`, которая получает объект и умножает все численные свойства на 2. Например: + +```js +// до вызова +var menu = { + width: 200, + height: 300, + title: "My menu" +}; + +multiplyNumeric(menu); + +// после вызова +menu = { + width: 400, + height: 600, + title: "My menu" +}; +``` + +P.S. Для проверки на число используйте функцию: + +```js +function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n) +} +``` + diff --git a/1-js/4-data-structures/4-object-for-in/article.md b/1-js/4-data-structures/4-object-for-in/article.md new file mode 100644 index 00000000..75940780 --- /dev/null +++ b/1-js/4-data-structures/4-object-for-in/article.md @@ -0,0 +1,173 @@ +# Объекты: перебор свойств + +Для перебора всех свойств из объекта используется цикл по свойствам `for..in`. Это специальная синтаксическая конструкция, которая работает не так, как обычный цикл. + +[cut] + +## for..in [#for..in] + +Синтаксис: + +```js +for (key in obj) { + /* ... делать что-то с obj[key] ... */ +} +``` + +При этом в `key` будут последовательно записаны имена свойств. Конечно, вместо `key` может быть любое другое имя переменной. + + +[smart header="Объявление переменной в цикле `for (var key in obj)`"] +Переменную можно объявить прямо в цикле: + +```js +for (*!*var key*/!* in menu) { + // ... +} +``` + +Так иногда пишут для краткости кода. +[/smart] + + +Например: + +```js +//+ run +var menu = { + width: 300, + height: 200, + title: "Menu" +}; + +for (var key in menu) { + // этот код будет вызван для каждого свойства объекта + // ..и выведет имя свойства и его значение + +*!* + alert("Ключ: " + key + " значение:" + menu[key]); +*/!* +} +``` + +Обратите внимание, мы использовали квадратные скобки `menu[key]`. Как уже говорилось, если имя свойства хранится в переменной, то обратиться к нему можно только так, не через точку. + +## Количество свойств в объекте + +Как узнать, сколько свойств хранит объект? + +Готового метода для этого нет. + +Самый кросс-браузерный способ -- это сделать цикл по свойствам и посчитать, вот так: + +```js +//+ run +var menu = { + width: 300, + height: 200, + title: "Menu" +}; + +*!* +var counter = 0; + +for (var key in menu) { + counter++; +} +*/!* + +alert("Всего свойств: " + counter); +``` + +В следующих главах мы пройдём массивы и познакомимся с другим, более коротким, вызовом: `Object.keys(menu).length`. + +## В каком порядке перебираются свойства? + +Для примера, рассмотрим объект, который задаёт список опций для выбора страны: + +```js +var codes = { + // телефонные коды в формате "код страны": "название" + "7": "Россия", + "38": "Украина", + // .., + "1": "США" +}; +``` + +Здесь мы предполагаем, что большинство посетителей из России, и поэтому начинаем с `7`, это зависит от проекта. + +При выборе телефонного кода мы хотели бы предлагать варианты, начиная с первого. Обычно на основе списка генерируется `select`, но здесь нам важно не это, а важно другое. + +**Правда ли, что при переборе `for(key in codes)` ключи `key` будут перечислены именно в том порядке, в котором заданы?** + +**По стандарту -- нет. Но некоторое соглашение об этом, всё же, есть.** + +Соглашение говорит, что если имя свойства -- нечисловая строка, то такие ключи всегда перебираются в том же порядке. Так получилось по историческим причинам и изменить это сложно: поломается много готового кода. + +С другой стороны, если имя свойства -- число, то все современные браузеры сортируют такие свойства в целях внутренней оптимизации. + +К примеру, рассмотрим объект с заведомо нечисловыми свойствами: + +```js +//+ run +var user = { + name: "Вася", + surname: "Петров" +}; +user.age = 25; + +*!* +// порядок перебора соответствует порядку присвоения свойства +*/!* +for (var prop in user) { + alert(prop); // name, surname, age +} +``` + +А теперь -- что будет, если перебрать объект с кодами? + +```js +//+ run +var codes = { + // телефонные коды в формате "код страны": "название" + "7": "Россия", + "38": "Украина", + "1": "США" +}; + +for(var code in codes) alert(code); // 1, 7, 38 +``` + +При запуске этого кода в современном браузере мы увидим, что на первое место попал код США! + +Нарушение порядка возникло, потому что ключи численные. Интерпретатор JavaScript видит, что строка на самом деле является числом и преобразует ключ в немного другой внутренний формат. Дополнительным эффектом внутренних оптимизаций является сортировка. + +**А что, если мы хотим, чтобы порядок был именно таким, какой мы задали?** + +Это возможно. Можно применить небольшой хак, который заключается в том, чтобы сделать все ключи нечисловыми, например, добавим в начало дополнительный символ `'+'`: + +```js +//+ run +var codes = { + "+7": "Россия", + "+38": "Украина", + "+1": "США" +}; + +for (var code in codes ) { + var value = codes[code]; + code = +code; // ..если нам нужно именно число, преобразуем: "+7" -> 7 + + alert( code + ": " + value ); // 7, 38, 1 во всех браузерах +} +``` + +## Итого + +
      +
    • Цикл по ключам: `for (key in obj)`.
    • +
    • Порядок перебора соответствует порядку объявления для нечисловых ключей, а числовые -- сортируются (в современных браузерах).
    • +
    • Для того, чтобы гарантировать перебор ключей в нужном порядке, их делают "нечисловыми", например добавляя в начало `+`, а потом, в процессе обработки, преобразуют ключи в числа.
    • +
    + diff --git a/1-js/4-data-structures/5-object-reference/article.md b/1-js/4-data-structures/5-object-reference/article.md new file mode 100644 index 00000000..7247f6d7 --- /dev/null +++ b/1-js/4-data-structures/5-object-reference/article.md @@ -0,0 +1,162 @@ +# Объекты: передача по ссылке + +Фундаментальным отличием объектов от примитивов, является их копирование "по ссылке". + +[cut] + +## Копирование по значению + +Обычные значения: строки, числа, булевы значения, `null/undefined` при присваивании переменных копируются целиком или, как говорят, *"по значению"*. + +```js +var message = "Привет"; +var phrase = message; +``` + +В результате такого копирования получились две полностью независимые переменные, в каждой из которых хранится значение `"Привет"`. + + + +## Копирование по ссылке + +С объектами -- всё не так. + +**В переменной, которой присвоен объект, хранится не сам объект, а "адрес его места в памяти", иными словами -- "ссылка" на него.** + +Например, обычную переменную можно изобразить как коробку с данными: + +```js +var message = "Привет"; // значение в переменной +``` + + + +А вот как выглядит переменная, которой присвоен объект: + +```js +var user = { name: "Вася" }; +``` + + + +Внимание: объект -- вне переменной. В переменной -- лишь ссылка на него. + +**При копировании переменной с объектом -- копируется эта ссылка, а объект по-прежнему остается в единственном экземпляре.** + +Например: + +```js +var user = { name: "Вася" }; // в переменной - ссылка + +var admin = user; // скопировали ссылку +``` + +Получили две переменные, в которых находятся ссылки на один и тот же объект: + + + +**Так как объект всего один, то изменения через любую переменную видны в других переменных:** + +```js +//+ run +var user = { name: 'Вася' }; + +var admin = user; + +*!*admin.name*/!* = 'Петя'; // поменяли данные через admin + +alert(*!*user.name*/!*); // 'Петя', изменения видны в user +``` + +[smart header="Переменная с объектом как \"ключ\" к сейфу с данными"] +Ещё одна аналогия: переменная, в которую присвоен объект, на самом деле хранит не сами данные, а ключ к сейфу, где они хранятся. + +При копировании её, получается что мы сделали копию ключа, но сейф по-прежнему один. По какому бы ключу мы не залезли в сейф, данные -- одни и те же. +[/smart] + +## Клонирование объектов + +Иногда, на практике -- очень редко, нужно скопировать объект целиком, создать именно полную копию, "независимый клон". + +Что ж, можно сделать и это. Для этого нужно пройти по объекту, достать данные и скопировать на уровне примитивов. + +Примерно так: + +```js +//+ run +var user = { + name: "Вася", + age: 30 +}; + +*!* +var clone = {}; // новый пустой объект + +// скопируем в него все свойства user +for(var key in user) { + clone[key] = user[key]; +} +*/!* + +// теперь clone -- полностью независимая копия +clone.name = "Петя"; // поменяли данные в clone + +alert(user.name); // по-прежнем "Вася" +``` + +В этом коде не учитывается, что свойства объектов, в свою очередь, могут хранить ссылки на другие объекты. Можно обойти такие подобъекты и тоже склонировать их. + +Это называют "глубоким" клонированием. Для того, чтобы это сделать, нужно рекурсивно обойти объект вместе с подобъектами. + +## Вывод в консоли + +Откройте консоль браузера (обычно [key F12]) и запустите следующий код: + +```js +//+ run +var time = { + year: 2345, + month: 11, + day: 10, + hour: 11, + minute: 12, + second: 13, + microsecond: 123456 +} + +console.log(time); // (*) +time.microsecond++; // (**) + +console.log(time); +time.microsecond++; + +console.log(time); +time.microsecond++; +``` + +Как видно, некий объект выводится `(*)`, затем он меняется `(**)` и снова выводится, и так несколько раз. Пока ничего необычного, типичная ситуация -- скрипт делает какую-то работу с объектом и выводит в консоли то, как она продвигается. + +Необычное -- в другом! + +При раскрытии каждый объект будет выглядеть примерно так (в Chrome): + + + +**Судя по выводу, свойство `microsecond` всегда было равно `123459`... Или нет?** + +Конечно, нет! Консоль нас просто дурит. + +**При "раскрытии" свойств объекта в консоли -- браузер всегда выводит их текущие (на момент раскрытия) значения.** + +Так происходит именно потому, что вывод не делает "копию" текущего содержимого, а сохраняет лишь ссылку на объект. В будущем, при отладке скриптов у вас не раз возникнет подобная ситуация :) + + +## Итого + +
      +
    • Объект присваивается и копируется "по ссылке". То есть, в переменной хранится не сам объект а, условно говоря, адрес в памяти, где он находится.
    • +
    • Если переменная-объект скопирована или передана в функцию, то копируется именно эта ссылка, а объект остаётся один в памяти.
    • +
    + +Это -- одно из ключевых отличий объекта от примитива (числа, строки...), который при присвоении как раз копируется "по значению", то есть полностью. + diff --git a/1-js/4-data-structures/5-object-reference/box-message-hello.png b/1-js/4-data-structures/5-object-reference/box-message-hello.png new file mode 100755 index 00000000..1a0a7efa Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/box-message-hello.png differ diff --git a/1-js/4-data-structures/5-object-reference/message_box_hello.png b/1-js/4-data-structures/5-object-reference/message_box_hello.png new file mode 100755 index 00000000..44e39e66 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/message_box_hello.png differ diff --git a/1-js/4-data-structures/5-object-reference/object-reference-console.png b/1-js/4-data-structures/5-object-reference/object-reference-console.png new file mode 100644 index 00000000..864f1f94 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/object-reference-console.png differ diff --git a/1-js/4-data-structures/5-object-reference/object-reference-console@2x.png b/1-js/4-data-structures/5-object-reference/object-reference-console@2x.png new file mode 100644 index 00000000..89345268 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/object-reference-console@2x.png differ diff --git a/1-js/4-data-structures/5-object-reference/phrase_box_hello.png b/1-js/4-data-structures/5-object-reference/phrase_box_hello.png new file mode 100755 index 00000000..cd963668 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/phrase_box_hello.png differ diff --git a/1-js/4-data-structures/5-object-reference/referenceUser.png b/1-js/4-data-structures/5-object-reference/referenceUser.png new file mode 100755 index 00000000..8884c2cd Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/referenceUser.png differ diff --git a/1-js/4-data-structures/5-object-reference/referenceUser@2x.png b/1-js/4-data-structures/5-object-reference/referenceUser@2x.png new file mode 100755 index 00000000..74768628 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/referenceUser@2x.png differ diff --git a/1-js/4-data-structures/5-object-reference/referenceUserAdmin.png b/1-js/4-data-structures/5-object-reference/referenceUserAdmin.png new file mode 100755 index 00000000..24910f93 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/referenceUserAdmin.png differ diff --git a/1-js/4-data-structures/5-object-reference/referenceUserAdmin@2x.png b/1-js/4-data-structures/5-object-reference/referenceUserAdmin@2x.png new file mode 100755 index 00000000..3ffccfe6 Binary files /dev/null and b/1-js/4-data-structures/5-object-reference/referenceUserAdmin@2x.png differ diff --git a/1-js/4-data-structures/6-array/1-get-last-in-array/solution.md b/1-js/4-data-structures/6-array/1-get-last-in-array/solution.md new file mode 100644 index 00000000..50760642 --- /dev/null +++ b/1-js/4-data-structures/6-array/1-get-last-in-array/solution.md @@ -0,0 +1,16 @@ +Последний элемент имеет индекс на `1` меньший, чем длина массива. + +Например: + +```js +var fruits = ["Яблоко", "Груша", "Слива"]; +``` + +Длина массива этого массива `fruits.length` равна `3`. Здесь "Яблоко" имеет индекс `0`, "Груша" -- индекс `1`, "Слива" -- индекс `2`. + +То есть, для массива длины `goods`: + +```js +var lastItem = goods[goods.length-1]; // получить последний элемент +``` + diff --git a/1-js/4-data-structures/6-array/1-get-last-in-array/task.md b/1-js/4-data-structures/6-array/1-get-last-in-array/task.md new file mode 100644 index 00000000..b33ea3d9 --- /dev/null +++ b/1-js/4-data-structures/6-array/1-get-last-in-array/task.md @@ -0,0 +1,9 @@ +# Получить последний элемент массива + +[importance 5] + +Как получить последний элемент из произвольного массива? + +У нас есть массив `goods`. Сколько в нем элементов -- не знаем, но можем прочитать из `goods.length`. + +Напишите код для получения последнего элемента `goods`. \ No newline at end of file diff --git a/1-js/4-data-structures/6-array/10-maximal-subarray/_js.view/solution.js b/1-js/4-data-structures/6-array/10-maximal-subarray/_js.view/solution.js new file mode 100644 index 00000000..727ee23b --- /dev/null +++ b/1-js/4-data-structures/6-array/10-maximal-subarray/_js.view/solution.js @@ -0,0 +1,10 @@ +function getMaxSubSum(arr) { + var maxSum = 0, partialSum = 0; + for (var i=0; i2), то есть при увеличении массива в 2 раза алгоритм требует в 4 раза больше времени. На больших массивах (1000, 10000 и более элементов) такие алгоритмы могут приводить к серьёзным "тормозам". + +# Подсказка (быстрое решение) + +Будем идти по массиву и накапливать в некоторой переменной `s` текущую частичную сумму. Если в какой-то момент s окажется отрицательной, то мы просто присвоим `s=0`. Утверждается, что максимум из всех значений переменной s, случившихся за время работы, и будет ответом на задачу. + +**Докажем этот алгоритм.** + +В самом деле, рассмотрим первый момент времени, когда сумма `s` стала отрицательной. Это означает, что, стартовав с нулевой частичной суммы, мы в итоге пришли к отрицательной частичной сумме -- значит, и весь этот префикс массива, равно как и любой его суффикс имеют отрицательную сумму. + +Следовательно, от всего этого префикса массива в дальнейшем не может быть никакой пользы: он может дать только отрицательную прибавку к ответу. + +# Быстрое решение + +```js +//+ run +function getMaxSubSum(arr) { + var maxSum = 0, partialSum = 0; + for (var i=0; i2), а лучше за O(n) операций. \ No newline at end of file diff --git a/1-js/4-data-structures/6-array/2-add-item-to-array/solution.md b/1-js/4-data-structures/6-array/2-add-item-to-array/solution.md new file mode 100644 index 00000000..9f72e68b --- /dev/null +++ b/1-js/4-data-structures/6-array/2-add-item-to-array/solution.md @@ -0,0 +1,6 @@ +Текущий последний элемент имеет индекс `goods.length-1`. Значит, индексом нового элемента будет `goods.length`: + +```js +goods[goods.length] = 'Компьютер' +``` + diff --git a/1-js/4-data-structures/6-array/2-add-item-to-array/task.md b/1-js/4-data-structures/6-array/2-add-item-to-array/task.md new file mode 100644 index 00000000..6f0e6d16 --- /dev/null +++ b/1-js/4-data-structures/6-array/2-add-item-to-array/task.md @@ -0,0 +1,7 @@ +# Добавить новый элемент в массив + +[importance 5] + +Как добавить элемент в конец произвольного массива? + +У нас есть массив `goods`. Напишите код для добавления в его конец значения "Компьютер". diff --git a/1-js/4-data-structures/6-array/3-create-array/solution.md b/1-js/4-data-structures/6-array/3-create-array/solution.md new file mode 100644 index 00000000..5f6093c8 --- /dev/null +++ b/1-js/4-data-structures/6-array/3-create-array/solution.md @@ -0,0 +1,11 @@ + + +```js +//+ run +var styles = ["Джаз", "Блюз"]; +styles.push("Рок-н-Ролл"); +styles[styles.length-2] = "Классика"; +alert( styles.shift() ); +styles.unshift( "Рэп", "Регги "); +``` + diff --git a/1-js/4-data-structures/6-array/3-create-array/task.md b/1-js/4-data-structures/6-array/3-create-array/task.md new file mode 100644 index 00000000..38434b47 --- /dev/null +++ b/1-js/4-data-structures/6-array/3-create-array/task.md @@ -0,0 +1,23 @@ +# Создание массива + +[importance 5] + +Задача из 5 шагов-строк: +
      +
    1. Создайте массив `styles` с элементами "Джаз", "Блюз".
    2. +
    3. Добавьте в конец значение "Рок-н-Ролл"
    4. +
    5. Замените предпоследнее значение с конца на "Классика". Код замены предпоследнего значения должен работать для массивов любой длины.
    6. +
    7. Удалите первое значение массива и выведите его `alert`.
    8. +
    9. Добавьте в начало значения "Рэп" и "Регги".
    10. +
    + +Массив в результате каждого шага: + +```js +Джаз, Блюз +Джаз, Блюз, Рок-н-Ролл +Джаз, Классика, Рок-н-Ролл +Классика, Рок-н-Ролл +Рэп, Регги, Классика, Рок-н-Ролл +``` + diff --git a/1-js/4-data-structures/6-array/4-random-from-array/solution.md b/1-js/4-data-structures/6-array/4-random-from-array/solution.md new file mode 100644 index 00000000..9f4d1195 --- /dev/null +++ b/1-js/4-data-structures/6-array/4-random-from-array/solution.md @@ -0,0 +1,11 @@ +Для вывода нужен случайный номер от `0` до `arr.length-1` включительно. + +```js +//+ run +var arr = ["Яблоко", "Апельсин", "Груша", "Лимон"]; + +var rand = Math.floor( Math.random() * arr.length ); + +alert(arr[rand]); +``` + diff --git a/1-js/4-data-structures/6-array/4-random-from-array/task.md b/1-js/4-data-structures/6-array/4-random-from-array/task.md new file mode 100644 index 00000000..71fde183 --- /dev/null +++ b/1-js/4-data-structures/6-array/4-random-from-array/task.md @@ -0,0 +1,16 @@ +# Получить случайное значение из массива + +[importance 3] + +Напишите код для вывода `alert` случайного значения из массива: + +```js +var arr = ["Яблоко", "Апельсин", "Груша", "Лимон"]; +``` + +P.S. Код для генерации случайного целого от `min` to `max` включительно: + +```js +var rand = min + Math.floor( Math.random() * (max+1-min) ); +``` + diff --git a/1-js/4-data-structures/6-array/5-calculator-for-input/solution.md b/1-js/4-data-structures/6-array/5-calculator-for-input/solution.md new file mode 100644 index 00000000..d2f7dfc5 --- /dev/null +++ b/1-js/4-data-structures/6-array/5-calculator-for-input/solution.md @@ -0,0 +1,23 @@ +В решение ниже обратите внимание: мы не приводим `value` к числу сразу после `prompt`, так как если сделать `value = +value`, то после этого отличить пустую строку от нуля уже никак нельзя. А нам здесь нужно при пустой строке прекращать ввод, а при нуле -- продолжать. + +```js +//+ run demo +var numbers = []; + +while(true) { + + var value = prompt("Введите число", 0); + + if (value === "" || value === null || isNaN(value)) break; + + numbers.push(+value); +} + +var sum = 0; +for(var i=0; i +
  • Запрашивает по очереди значения при помощи `prompt` и сохраняет их в массиве.
  • +
  • Заканчивает ввод, как только посетитель введёт пустую строку, не число или нажмёт "Отмена".
  • +
  • При этом ноль `0` не должен заканчивать ввод, это разрешённое число.
  • +
  • Выводит сумму всех значений массива
  • + + +[demo /] \ No newline at end of file diff --git a/1-js/4-data-structures/6-array/6-item-value/solution.md b/1-js/4-data-structures/6-array/6-item-value/solution.md new file mode 100644 index 00000000..38e515d0 --- /dev/null +++ b/1-js/4-data-structures/6-array/6-item-value/solution.md @@ -0,0 +1,24 @@ + + +```js +//+ run +var arr = [1,2,3]; + +var arr2 = arr; // (*) +arr2[0] = 5; + +alert(arr[0]); +alert(arr2[0]); +``` + +Код выведет `5` в обоих случаях, так как массив является объектом. В строке `(*)` в переменную `arr2` копируется ссылка на него, а сам объект в памяти по-прежнему один, в нём отражаются изменения, внесенные через `arr2` или `arr`. + +В частности, сравнение `arr2 == arr` даст `true`. + +Если нужно именно скопировать массив, то это можно сделать, например, так: + +```js +var arr2 = []; +for(var i=0; iArray#indexOf, которая работает именно таким образом. Имеет смысл ей воспользоваться, если браузер ее поддерживает. + +```js +//+ run +function find(array, value) { + if (array.indexOf) { // если метод существует + return array.indexOf(value); + } + + for(var i=0; i= a && arr[i] <= b) { + result.push(arr[i]); + } + } + + return result; +} \ No newline at end of file diff --git a/1-js/4-data-structures/6-array/8-filter-range/_js.view/test.js b/1-js/4-data-structures/6-array/8-filter-range/_js.view/test.js new file mode 100644 index 00000000..0007bbbe --- /dev/null +++ b/1-js/4-data-structures/6-array/8-filter-range/_js.view/test.js @@ -0,0 +1,15 @@ +describe("filterRange", function() { + it("не меняет исходный массив", function() { + var arr = [5, 4, 3, 8, 0]; + + filterRange(arr, 0, 10); + assert.deepEqual(arr, [5,4,3,8,0]); + }); + + it("оставляет только значения указанного интервала", function() { + var arr = [5, 4, 3, 8, 0]; + + var result = filterRange(arr, 3, 5); + assert.deepEqual(result, [5,4,3]); + }); +}); diff --git a/1-js/4-data-structures/6-array/8-filter-range/solution.md b/1-js/4-data-structures/6-array/8-filter-range/solution.md new file mode 100644 index 00000000..d37bc58b --- /dev/null +++ b/1-js/4-data-structures/6-array/8-filter-range/solution.md @@ -0,0 +1,29 @@ +# Алгоритм решения +
      +
    1. Создайте временный пустой массив `var results = []`.
    2. +
    3. Пройдите по элементам `arr` в цикле и заполните его.
    4. +
    5. Возвратите `results`.
    6. +
    + +# Решение + +```js +//+ run +function filterRange(arr, a, b) { + var result = []; + + for(var i=0; i= a && arr[i] <= b) { + result.push(arr[i]); + } + } + + return result; +} + +var arr = [5, 4, 3, 8, 0]; + +var filtered = filterRange(arr, 3, 5); +alert(filtered); +``` + diff --git a/1-js/4-data-structures/6-array/8-filter-range/task.md b/1-js/4-data-structures/6-array/8-filter-range/task.md new file mode 100644 index 00000000..150f7737 --- /dev/null +++ b/1-js/4-data-structures/6-array/8-filter-range/task.md @@ -0,0 +1,17 @@ +# Фильтр диапазона + +[importance 3] + +Создайте фунцию `filterRange(arr, a, b)`, которая принимает массив чисел `arr` и возвращает новый массив, который содержит только числа из `arr` из диапазона от `a` до `b`. То есть, проверка имеет вид `a ≤ arr[i] ≤ b`. +Функция не должна менять `arr`. + +Пример работы: + +```js +var arr = [5, 4, 3, 8, 0]; + +var filtered = filterRange(arr, 3, 5); +// теперь filtered = [5, 4, 3] +// arr не изменился +``` + diff --git a/1-js/4-data-structures/6-array/9-eratosthenes-sieve/solution.md b/1-js/4-data-structures/6-array/9-eratosthenes-sieve/solution.md new file mode 100644 index 00000000..03dc51b2 --- /dev/null +++ b/1-js/4-data-structures/6-array/9-eratosthenes-sieve/solution.md @@ -0,0 +1,40 @@ +Их сумма равна `1060`. + +```js +//+ run +// шаг 1 +var arr = []; + +for (var i=2; i<100; i++) { + arr[i] = true +} + +// шаг 2 +var p = 2; + +do { + // шаг 3 + for (i=2*p; i<100; i+=p) { + arr[i] = false; + } + + // шаг 4 + for (i=p+1; i<100; i++) { + if (arr[i]) break; + } + + p = i; +} while (p*p < 100); // шаг 5 + +// шаг 6 (готово) +// посчитать сумму +var sum = 0; +for (i=0; i +
  • Создать список последовательных чисел от `2` до `n`: `2, 3, 4, ..., n`.
  • +
  • Пусть `p=2`, это первое простое число.
  • +
  • Зачеркнуть все последующие числа в списке с разницей в `p`, т.е. `2*p, 3*p, 4*p` и т.д. В случае `p=2` это будут `4,6,8...`.
  • +
  • Поменять значение `p` на первое незачеркнутое число после `p`.
  • +
  • Повторить шаги 3-4 пока p2 < n.
  • +
  • Все оставшиеся незачеркнутыми числа -- простые.
  • + + +Посмотрите также анимацию алгоритма. + +Реализуйте "Решето Эратосфена" в JavaScript, используя массив. + +Найдите все простые числа до `100` и выведите их сумму. + + + diff --git a/1-js/4-data-structures/6-array/article.md b/1-js/4-data-structures/6-array/article.md new file mode 100644 index 00000000..fe381029 --- /dev/null +++ b/1-js/4-data-structures/6-array/article.md @@ -0,0 +1,440 @@ +# Массивы c числовыми индексами + +*Массив с числовыми индексами* -- это коллекция данных, которая хранит сколько угодно значений, причем у каждого значения -- свой уникальный номер. + +Если переменная -- это *коробка для данных*, то массив -- это *шкаф с нумерованными ячейками*, в каждой из которых могут быть свои данные. +[cut] +Например, при создании электронного магазина нужно хранить список товаров -- для таких задач и придуман массив. + +## Объявление + +Синтаксис для создания нового массива -- квадратные скобки со списком элементов внутри. + +Пустой массив: + +```js +var arr = []; +``` + +Массив `fruits` с тремя элементами: + +```js +var fruits = ["Яблоко", "Апельсин", "Слива"]; +``` + +**Элементы нумеруются, начиная с нуля.** Чтобы получить нужный элемент из массива -- указывается его номер в квадратных скобках: + +```js +//+ run +var fruits = ["Яблоко", "Апельсин", "Слива"]; + +alert(fruits[0]); // Яблоко +alert(fruits[1]); // Апельсин +alert(fruits[2]); // Слива +``` + +Элемент можно всегда заменить: + +```js +fruits[2] = 'Груша'; // теперь ["Яблоко", "Апельсин", "Груша"] +``` + +... Или добавить: + +```js +fruits[3] = 'Лимон'; // теперь ["Яблоко", "Апельсин", "Груша", "Лимон"] +``` + +Общее число элементов, хранимых в массиве, содержится в его свойстве `length`: + +```js +//+ run +var fruits = ["Яблоко", "Апельсин", "Груша"]; + +alert(fruits.length); // 3 +``` + +**Через `alert` можно вывести и массив целиком.** При этом его элементы будут перечислены через запятую: + +```js +//+ run +var fruits = ["Яблоко", "Апельсин", "Груша"]; + +alert(fruits); // Яблоко,Апельсин,Груша +``` + +**В массиве может храниться любое число элементов любого типа.** В том числе, строки, числа, объекты и т.п.: + +```js +//+ run +// микс значений +var arr = [ 1, 'Имя', { name: 'Петя' }, true ]; + +// получить объект из массива и тут же -- его свойство +alert( arr[2].name ); // Петя +``` + +## Методы pop/push, shift/unshift + +Одно из применений массива -- это [очередь](http://ru.wikipedia.org/wiki/%D0%9E%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29). В классическом программировании так называют упорядоченную коллекцию элементов, такую что элементы добавляются в конец, а обрабатываются -- с начала. + +В реальной жизни эта структура данных встречается очень часто. Например, очередь сообщений, которые надо отослать. + +Очень близка к очереди еще одна структура данных: [стек](http://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA). Это такая коллекция элементов, в которой новые элементы добавляются в конец и берутся с конца. + +Например, стеком является колода карт, в которую новые карты кладутся сверху, и берутся -- тоже сверху. + +Для того, чтобы реализовывать эти структуры данных, и просто для более удобной работы с началом и концом массива существуют специальные методы. + +### Конец массива + +
    +
    `pop`
    +
    Удаляет *последний* элемент из массива и возвращает его: + +```js +//+ run +var fruits = ["Яблоко", "Апельсин", "Груша"]; + +alert( fruits.pop() ); // удалили "Груша" + +alert(fruits); // Яблоко, Апельсин +``` + +
    +
    `push`
    +
    Добавляет элемент *в конец* массива: + +```js +//+ run +var fruits = ["Яблоко", "Апельсин"]; + +fruits.push("Груша"); + +alert(fruits); // Яблоко, Апельсин, Груша +``` + +Является полным аналогом `fruits[fruits.length] = ...`. +
    +
    + +### Начало массива + +
    +
    `shift`
    +
    Удаляет из массива *первый* элемент и возвращает его: + +```js +var fruits = ["Яблоко", "Апельсин", "Груша"]; + +alert( fruits.shift() ); // удалили Яблоко + +alert(fruits); // Апельсин, Груша +``` + +
    +
    `unshift`
    +
    Добавляет элемент *в начало* массива: + +```js +var fruits = ["Апельсин", "Груша"]; + +fruits.unshift('Яблоко'); + +alert(fruits); // Яблоко, Апельсин, Груша +``` + +
    +
    + + + +Методы `push` и `unshift` могут добавлять сразу по несколько элементов: + +```js +//+ run +var fruits = ["Яблоко"]; + +fruits.push("Апельсин", "Персик"); +fruits.unshift("Ананас", "Лимон"); + +// результат: ["Ананас", "Лимон", "Яблоко", "Апельсин", "Персик"] +alert(fruits); +``` + +## Внутреннее устройство массива + +Массив -- это объект, где в качестве ключей выбраны цифры, с дополнительными методами и свойством `length`. + +Так как это объект, то в функцию он передаётся по ссылке: + +```js +//+ run +function eat(arr) { + arr.pop(); +} + +var arr = ["нам", "не", "страшен", "серый", "волк"] + +alert(arr.length); // 5 +eat(arr); +eat(arr); +alert(arr.length); // 3, в функцию массив не скопирован, а передана ссылка +``` + +**Ещё одно следствие -- можно присваивать в массив любые свойства.** + +Например: + +```js +var fruits = []; // создать массив + +fruits[99999] = 5; // присвоить свойство с любым номером + +fruits.age = 25; // назначить свойство со строковым именем +``` + +.. Но массивы для того и придуманы в JavaScript, чтобы удобно работать именно *с упорядоченными, нумерованными данными*. Для этого в них существуют специальные методы и свойство `length`. + +Как правило, нет причин использовать массив как обычный объект, хотя технически это и возможно. + +[warn header="Вывод массива с \"дырами\""] +Если в массиве есть пропущенные индексы, то при выводе в большинстве браузеров появляются "лишние" запятые, например: + +```js +//+ run +var a = []; +a[0] = 0; +a[5] = 5; + +alert(a); // 0,,,,,5 +``` + +Эти запятые появляются потому, что алгоритм вывода массива идёт от `0` до `arr.length` и выводит всё через запятую. Отсутствие значений даёт несколько запятых подряд. +[/warn] + +### Влияние на быстродействие + +Методы `push/pop` выполняются быстро, а `shift/unshift` -- медленно. + +Чтобы понять, почему работать с концом массива -- быстрее, чем с его началом, разберём происходящее подробнее. + +Операция `shift` выполняет два действия: +
      +
    1. Удалить элемент в начале.
    2. +
    3. Обновить внутреннее свойство `length`.
    4. +
    + +При этом, так как все элементы находятся в своих ячейках, просто очистить ячейку с номером `0` недостаточно. Нужно еще и переместить все ячейки на `1` вниз (красным на рисунке подсвечены изменения): + +```js +*!*fruits.shift();*/!* // убрать 1 элемент с начала +``` + + + +Чем больше элементов в массиве, тем дольше их перемещать. + +Аналогично работает `unshift`: чтобы добавить элемент в начало массива, нужно сначала перенести все существующие. + +У методов `push/pop` таких проблем нет. Для того, чтобы удалить элемент, метод `pop` очищает ячейку и укорачивает `length`. + +```js +*!*fruits.pop();*/!* // убрать 1 элемент с конца +``` + + + +Аналогично работает `push`. + + +## Перебор элементов + +Для перебора элементов обычно используется цикл: + +```js +//+ run +var arr = ["Яблоко", "Апельсин", "Груша"]; + +*!* +for (var i=0; i +
  • Цикл `for..in` выведет *все свойства* объекта, а не только цифровые. + +В браузере, при работе с объектами страницы, встречаются коллекции элементов, которые по виду как массивы, но имеют дополнительные нецифровые свойства, которые будут видны в цикле `for..in`. + +Бывают и библиотеки, которые предоставляют такие коллекции. Например jQuery. С виду -- массив, но есть дополнительные свойства. Для перебора только цифровых свойств нужен цикл `for(var i=0; i +
  • Цикл `for (var i=0; i + + +**Если кратко: цикл `for(var i=0; i +
  • Не ставить массиву произвольные свойства, такие как `arr.test = 5`. То есть, работать именно как с массивом, а не как с объектом.
  • +
  • Заполнять массив непрерывно. Как только браузер встречает необычное поведение массива, например устанавливается значение `arr[0]`, а потом сразу `arr[1000]`, то он начинает работать с ним, как с обычным объектом. Как правило, это влечёт преобразование его в хэш-таблицу.
  • + + + +Если следовать этим принципам, то массивы будут занимать меньше памяти и быстрее работать. + +## Итого + +Массивы существуют для работы с упорядоченным набором элементов. + +**Объявление:** + +```js +// предпочтительное +var arr = [ элемент1, элемент2... ]; + +// new Array +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()` удаляет и возвращает первый элемент.
    • +
    + +Эти операции перенумеровывают все элементы, поэтому работают медленно. + +В следующей главе мы рассмотрим другие методы для работы с массивами. + + + + diff --git a/1-js/4-data-structures/6-array/pop.png b/1-js/4-data-structures/6-array/pop.png new file mode 100755 index 00000000..80551a8c Binary files /dev/null and b/1-js/4-data-structures/6-array/pop.png differ diff --git a/1-js/4-data-structures/6-array/shift.png b/1-js/4-data-structures/6-array/shift.png new file mode 100755 index 00000000..6c412948 Binary files /dev/null and b/1-js/4-data-structures/6-array/shift.png differ diff --git a/1-js/4-data-structures/6-array/shiftpush.png b/1-js/4-data-structures/6-array/shiftpush.png new file mode 100755 index 00000000..568b1b77 Binary files /dev/null and b/1-js/4-data-structures/6-array/shiftpush.png differ diff --git a/1-js/4-data-structures/7-array-methods/1-add-class/_js.view/solution.js b/1-js/4-data-structures/7-array-methods/1-add-class/_js.view/solution.js new file mode 100644 index 00000000..316e3087 --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/1-add-class/_js.view/solution.js @@ -0,0 +1,11 @@ +function addClass(obj, cls) { + var classes = obj.className ? obj.className.split(' ') : []; + + for(var i=0; i взо +киборг, гробик -> бгикор +... +``` + +По такой последовательности будем делать массив уникальным. + +Для этого воспользуемся вспомогательным объектом, в который будем записывать слова по отсортированному ключу: + +```js +//+ run +function aclean(arr) { + // этот объект будем использовать для уникальности + var obj = {}; + + for(var i=0; i +
  • Для первого элемента -- это обойдётся в `0` операций доступа к элементам `result` (он пока пустой).
  • +
  • Для второго элемента -- это обойдётся в `1` операцию доступа к элементам `result`.
  • +
  • Для третьего элемента -- это обойдётся в `2` операции доступа к элементам `result`.
  • +
  • ...Для n-го элемента -- это обойдётся в `n-1` операций доступа к элементам `result`.
  • + + +Всего 0 + 1 + 2 + ... + n-1 = (n-1)*n/2 = n2/2 - n/2 (как сумма арифметической прогрессии), то есть количество операций растёт примерно как квадрат от `n`. + +Это очень быстрый рост. Для `100` элементов -- `4950` операций, для `1000` -- `499500` (по формуле выше). + +Поэтому такое решение подойдёт только для небольших массивов. Вместо вложенного `for` можно использовать и `arr.indexOf`, ситуация от этого не поменяется, так как `indexOf` тоже ищет перебором. + +# Решение с объектом (быстрое) + +Наилучшая техника для выбора уникальных строк -- использование вспомогательного объекта. Ведь название свойства в объекте, с одной стороны -- строка, а с другой -- всегда уникально. Повторная запись в свойство с тем же именем перезапишет его. + +Например, если `"харе"` попало в объект один раз (`obj["харе"] = true`), то второе такое же присваивание ничего не изменит. + +Решение ниже создаёт объект `obj = {}` и записывает в него все строки как имена свойств. А затем собирает свойства из объекта в массив через `for..in`. Дубликатов уже не будет. + +```js +//+ run +function unique(arr) { + var obj = {}; + + for(var i=0; i b) { + arr.splice(i--, 1); + } + } + +} diff --git a/1-js/4-data-structures/7-array-methods/4-filter-in-place/_js.view/test.js b/1-js/4-data-structures/7-array-methods/4-filter-in-place/_js.view/test.js new file mode 100644 index 00000000..f8374db2 --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/4-filter-in-place/_js.view/test.js @@ -0,0 +1,9 @@ +describe("filterRangeInPlace", function() { + + it("меняет массив, оставляя только значения из диапазона", function() { + var arr = [5, 3, 8, 1]; + filterRangeInPlace(arr, 1, 4); + assert.deepEqual(arr, [3, 1]); + }); + +}); \ No newline at end of file diff --git a/1-js/4-data-structures/7-array-methods/4-filter-in-place/solution.md b/1-js/4-data-structures/7-array-methods/4-filter-in-place/solution.md new file mode 100644 index 00000000..05c7b005 --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/4-filter-in-place/solution.md @@ -0,0 +1,21 @@ + + +```js +//+ run +function filterRangeInPlace(arr, a, b) { + + for(var i = 0; i b) { + arr.splice(i--, 1); + } + } + +} + +var arr = [5, 3, 8, 1]; + +filterRangeInPlace(arr, 1, 4); +alert(arr); // [3, 1] +``` + diff --git a/1-js/4-data-structures/7-array-methods/4-filter-in-place/task.md b/1-js/4-data-structures/7-array-methods/4-filter-in-place/task.md new file mode 100644 index 00000000..ba90585a --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/4-filter-in-place/task.md @@ -0,0 +1,17 @@ +# Фильтрация массива "на месте" + +[importance 4] + +Создайте функцию `filterRangeInPlace(arr, a, b)`, которая получает массив с числами `arr` и удаляет из него все числа вне диапазона `a..b`. +То есть, проверка имеет вид `a ≤ arr[i] ≤ b`. Функция должна менять сам массив и ничего не возвращать. + +Например: + +```js +arr = [5, 3, 8, 1]; + +filterRangeInPlace(arr, 1, 4); // удалены числа вне диапазона 1..4 + +alert(arr); // массив изменился: остались [3, 1] +``` + diff --git a/1-js/4-data-structures/7-array-methods/5-sort-back/solution.md b/1-js/4-data-structures/7-array-methods/5-sort-back/solution.md new file mode 100644 index 00000000..0931342a --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/5-sort-back/solution.md @@ -0,0 +1,15 @@ + + +```js +//+ run +var arr = [ 5, 2, 1, -10, 8]; + +function compareReversed(a, b) { + return b - a; +} + +arr.sort(compareReversed); + +alert(arr); +``` + diff --git a/1-js/4-data-structures/7-array-methods/5-sort-back/task.md b/1-js/4-data-structures/7-array-methods/5-sort-back/task.md new file mode 100644 index 00000000..6771097f --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/5-sort-back/task.md @@ -0,0 +1,14 @@ +# Сортировать в обратном порядке + +[importance 5] + +Как отсортировать массив чисел в обратном порядке? + +```js +var arr = [ 5, 2, 1, -10, 8]; + +// отсортируйте? + +alert(arr); // 8, 5, 2, 1, -10 +``` + diff --git a/1-js/4-data-structures/7-array-methods/6-copy-sort-array/solution.md b/1-js/4-data-structures/7-array-methods/6-copy-sort-array/solution.md new file mode 100644 index 00000000..242fa0b9 --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/6-copy-sort-array/solution.md @@ -0,0 +1,14 @@ +Для копирования массива используем `slice()`, и тут же -- сортировку: + +```js +//+ run +var arr = [ "HTML", "JavaScript", "CSS" ]; + +*!* +var arrSorted = arr.slice().sort(); +*/!* + +alert(arrSorted); +alert(arr); +``` + diff --git a/1-js/4-data-structures/7-array-methods/6-copy-sort-array/task.md b/1-js/4-data-structures/7-array-methods/6-copy-sort-array/task.md new file mode 100644 index 00000000..52660134 --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/6-copy-sort-array/task.md @@ -0,0 +1,18 @@ +# Скопировать и отсортировать массив + +[importance 5] + +Есть массив строк `arr`. Создайте массив `arrSorted` -- из тех же элементов, но отсортированный. + +Исходный массив не должен меняться. + +```js +var arr = [ "HTML", "JavaScript", "CSS" ]; + +// ... ваш код ... + +alert(arrSorted); // CSS, HTML, JavaScript +alert(arr); // HTML, JavaScript, CSS (без изменений) +``` + +Постарайтесь сделать код как можно короче. \ No newline at end of file diff --git a/1-js/4-data-structures/7-array-methods/7-shuffle-array/solution.md b/1-js/4-data-structures/7-array-methods/7-shuffle-array/solution.md new file mode 100644 index 00000000..fa23ae9e --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/7-shuffle-array/solution.md @@ -0,0 +1,23 @@ +# Подсказка + +Функция сортировки должна возвращать случайный результат сравнения. Используйте для этого [Math.random](http://javascript.ru/Math.random). + +# Решение + +Обычно `Math.random()` возвращает результат от `0` до `1`. Вычтем `0.5`, чтобы область значений стала `[-0.5 ... 0.5)`. + +```js +//+ run +var arr = [1, 2, 3, 4, 5]; + +*!* +function compareRandom(a, b) { + return Math.random() - 0.5; +} + +arr.sort(compareRandom); +*/!* + +alert(arr); // элементы в случайном порядке, например [3,5,1,2,4] +``` + diff --git a/1-js/4-data-structures/7-array-methods/7-shuffle-array/task.md b/1-js/4-data-structures/7-array-methods/7-shuffle-array/task.md new file mode 100644 index 00000000..9a645e3f --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/7-shuffle-array/task.md @@ -0,0 +1,14 @@ +# Случайный порядок в массиве + +[importance 3] + +Используйте функцию `sort` для того, чтобы "перетрясти" элементы массива в случайном порядке. + +```js +var arr = [1, 2, 3, 4, 5]; + +arr.sort(ваша функция); + +alert(arr); // элементы в случайном порядке, например [3,5,1,2,4] +``` + diff --git a/1-js/4-data-structures/7-array-methods/8-sort-objects/solution.md b/1-js/4-data-structures/7-array-methods/8-sort-objects/solution.md new file mode 100644 index 00000000..a29d3cec --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/8-sort-objects/solution.md @@ -0,0 +1,26 @@ +Для сортировки объявим и передадим в `sort` анонимную функцию, которая сравнивает объекты по полю `age`: + +```js +//+ run +*!* +// Наша функция сравнения +function compareAge(personA, personB) { + return personA.age - personB.age; +} +*/!* + +// проверка +var vasya = { name: "Вася", age: 23 }; +var masha = { name: "Маша", age: 18 }; +var vovochka = { name: "Вовочка", age: 6 }; + +var people = [ vasya , masha , vovochka ]; + +people.sort(compareAge); + +// вывести +for(var i=0; i=0; i-- ) { + alert( arr[i] ); + } +} + +printReverseList(list); +``` + +**Обратный вывод без рекурсии быстрее.** + +По сути, рекурсивный вариант и нерекурсивный работают одинаково: они проходят список и запоминают его элементы, а потом выводят в обратном порядке. + +В случае с массивом это очевидно, а для рекурсии запоминание происходит в стеке (внутренней специальной структуре данных): когда вызывается вложенная функция, то интерпретатор сохраняет в стек текущие параметры. Вложенные вызовы заполняют стек, а потом он выводится в обратном порядке. + +При этом, при рекурсии в стеке сохраняется не только элемент списка, а другая вспомогательная информация, необходимая для возвращения из вложенного вызова. Поэтому тратится больше памяти. Все эти расходы отсутствуют во варианте без рекурсии, так как в массиве хранится именно то, что нужно. + +Преимущество рекурсии, с другой стороны -- более короткий и, зачастую, более простой код. \ No newline at end of file diff --git a/1-js/4-data-structures/7-array-methods/9-output-single-linked-list/task.md b/1-js/4-data-structures/7-array-methods/9-output-single-linked-list/task.md new file mode 100644 index 00000000..eb93d530 --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/9-output-single-linked-list/task.md @@ -0,0 +1,49 @@ +# Вывести односвязный список + +[importance 5] + +[Односвязный список](http://ru.wikipedia.org/wiki/Связный_список) -- это структура данных, которая состоит из *элементов*, каждый из которых хранит ссылку на следующий. Последний элемент может не иметь ссылки, либо она равна `null`. + +Например, объект ниже задаёт односвязный список, в `next` хранится ссылка на следующий элемент: + +```js +var list = { + value: 1, + next: { + value: 2, + next: { + value: 3, + next: { + value: 4, + next: null + } + } + } +}; +``` + +Графическое представление этого списка: + + +Альтернативный способ создания: + +```js +var list = { value: 1 }; +list.next = { value: 2 }; +list.next.next = { value: 3 }; +list.next.next.next = { value: 4 }; +``` + +Такая структура данных интересна тем, что можно очень быстро разбить список на части, объединить списки, удалить или добавить элемент в любое место, включая начало. При использовании массива такие действия требуют обширных перенумерований. + +Задачи: + +
      +
    1. Напишите функцию `printList(list)`, которая выводит элементы списка по очереди, при помощи цикла.
    2. +
    3. Напишите функцию `printList(list)` при помощи рекурсии.
    4. +
    5. Напишите функцию `printReverseList(list)`, которая выводит элементы списка в обратном порядке, при помощи рекурсии. +Для списка выше она должна выводить `4`,`3`,`2`,`1`
    6. +
    7. Сделайте вариант `printReverseList(list)`, использующий не рекурсию, а цикл.
    8. +
    + +Как лучше -- с рекурсией или без? \ No newline at end of file diff --git a/1-js/4-data-structures/7-array-methods/article.md b/1-js/4-data-structures/7-array-methods/article.md new file mode 100644 index 00000000..af8cf6fa --- /dev/null +++ b/1-js/4-data-structures/7-array-methods/article.md @@ -0,0 +1,445 @@ +# Массивы: методы + +В этой главе мы рассмотрим встроенные методы массивов JavaScript. +[cut] + +## Метод split + +Ситуация из реальной жизни. Мы пишем сервис отсылки сообщений и посетитель вводит имена тех, кому его отправить: `Маша, Петя, Марина, Василий...`. Но нам-то гораздо удобнее работать с массивом имен, чем с одной строкой. + +К счастью, есть метод `split(s)`, который позволяет превратить строку в массив, разбив ее по разделителю `s`. В примере ниже таким разделителем является строка из запятой и пробела. + +```js +//+ run +var names = 'Маша, Петя, Марина, Василий'; + +var arr = names.split(', '); + +for (var i=0; i +
    `arr.splice(index[, deleteCount, elem1, ..., elemN])`
    +
    Удалить `deleteCount` элементов, начиная с номера `index`, а затем вставить `elem1, ..., elemN` на их место.
    + + +Посмотрим примеры. + +```js +//+ run +var arr = ["Я", "изучаю", "JavaScript"]; + +*!* +arr.splice(1, 1); // начиная с позиции 1, удалить 1 элемент +*/!* + +alert(arr); // осталось ["Я", "JavaScript"] +``` + +Ниже продемонстрировано, как использовать `splice` для удаления одного элемента. Следующие за удаленным элементы сдвигаются, чтобы заполнить его место. + +```js +//+ run +var arr = ["Я", "изучаю", "JavaScript"]; + +*!* +arr.splice(0, 1); // удалить 1 элемент, начиная с позиции 0 +*/!* + +alert( arr[0] ); // "изучаю" стал первым элементом +``` + +Следующий пример показывает, как *заменять элементы*: + +```js +//+ run +var arr = [*!*"Я", "сейчас", "изучаю",*/!* "JavaScript"]; + +// удалить 3 первых элемента и добавить другие вместо них +arr.splice(0, 3, "Мы", "изучаем") + +alert( arr ) // теперь [*!*"Мы", "изучаем"*/!*, "JavaScript"] +``` + +Метод **`splice` возвращает массив из удаленных элементов**: + +```js +//+ run +var arr = [*!*"Я", "сейчас",*/!* "изучаю", "JavaScript"]; + +// удалить 2 первых элемента +var removed = arr.splice(0, 2); + +alert( removed ); // "Я", "сейчас" <-- array of removed elements +``` + +Метод **`splice` также может вставлять элементы без удаления**, для этого достаточно установить `deleteCount` в `0`: + +```js +//+ run +var arr = ["Я", "изучаю", "JavaScript"]; + +// с позиции 2 +// удалить 0 +// вставить "сложный", "язык" +arr.splice(2, 0, "сложный", "язык"); + +alert(arr); // "Я", "изучаю", "сложный", "язык", "JavaScript" +``` + +Допускается использование отрицательного номера позиции, которая в этом случае отсчитывается с конца: + +```js +//+ run +var arr = [1, 2, 5] + +// начиная с позиции индексом -1 (предпоследний элемент) +// удалить 0 элементов, +// затем вставить числа 3 и 4 +arr.splice(-1, 0, 3, 4); + +alert(arr); // результат: 1,2,3,4,5 +``` + +## Метод slice + +Метод `slice(begin, end)` копирует участок массива от `begin` до `end`, не включая `end`. Исходный массив при этом не меняется. + +Например: + +```js +//+ run +var arr = ["Почему", "надо", "учить", "JavaScript"]; + +var arr2 = arr.slice(1,3); // элементы 1, 2 (не включая 3) + +alert(arr2); // надо, учить +``` + +Аргументы ведут себя так же, как и в строковом `slice`: +
      +
    • Если не указать `end` -- копирование будет до конца массива: + +```js +//+ run +var arr = ["Почему", "надо", "учить", "JavaScript"]; + +alert( arr.slice(1) ); // взять все элементы, начиная с номера 1 +``` + +
    • +
    • Можно использовать отрицательные индексы, они отсчитываются с конца: + +```js +var arr2 = arr.slice(-2); // копировать от 2го элемента с конца и дальше +``` + +
    • +
    • Если вообще не указать аргументов -- скопируется весь массив: + +```js +var fullCopy = arr.slice(); +``` + +
    • +
    + +## Сортировка, метод sort(fn) + +Метод `sort()` сортирует массив *на месте*. Например: + +```js +//+ run +var arr = [ 1, 2, 15 ]; + +arr.sort(); + +alert( arr ); // *!*1, 15, 2*/!* +``` + +Не заметили ничего странного в этом примере? + +Порядок стал `1, 15, 2`. Это произошло потому, что **`sort` сортирует, преобразуя элементы к строке**. Поэтому и порядок у них строковый, ведь `"2" > "15"`. + +### Свой порядок сортировки + +Внутренняя реализация метода `arr.sort(fn)` умеет сортировать любые массивы, если указать функцию `fn` от двух элементов, которая умеет сравнивать их. + +Если эту функцию не указать, то элементы сортируются как строки. + +Например, укажем эту функцию явно, отсортируем элементы массива как числа: + +```js +//+ run +function compareNumeric(a, b) { + if (a > b) return 1; + if (a < b) return -1; +} + +var arr = [ 1, 2, 15 ]; + +*!* +arr.sort(compareNumeric); +*/!* + +alert(arr); // *!*1, 2, 15*/!* +``` + +Обратите внимание, мы передаём в `sort()` именно саму функцию `compareNumeric`, без вызова через скобки. Был бы ошибкой следующий код: + +```js +*!* +arr.sort( compareNumeric() ); // не сработает +*/!* +``` + +К функции, передаваемой `sort`, есть всего одно требование. + +Алгоритм сортировки, встроенный в JavaScript, будет передавать ей для сравнения элементы массива. Она должна возвращать: +
      +
    • Положительное значение, если `a > b`,
    • +
    • Отрицательное значение, если `a < b`,
    • +
    • Если равны -- не важно, что возвращать, их взаимный порядок не имеет значения.
    • +
    + + +[smart header="Алгоритм сортировки"] +В методе `sort`, внутри самого интерпретатора JavaScript, реализован универсальный алгоритм сортировки. Как правило, это ["\"быстрая сортировка\""](http://algolist.manual.ru/sort/quick_sort.php), дополнительно оптимизированная для небольших массивов. + +Ему совершенно неважно, что сортировать -- строки, числа, яблоки с апельсинами или посетителей. Это и не должно быть важно, алгоритм сортировки просто сортирует абстрактные "элементы массива". Всё, что ему нужно о них знать -- как эти элементы сравнивать между собой. + +Для этого мы передаём в `sort` функцию сравнения. А там уже алгоритм решает, что с чем сравнивать, чтобы отсортировать побыстрее. + +Кстати, те значения, с которыми `sort` вызывает функцию сравнения, можно увидеть, если вставить в неё `alert`: + +```js +//+ run +[1, -2, 15, 2, 0, 8].sort(function(a, b) { + alert(a + " <> " + b); +}); +``` + +[/smart] + +Функцию `compareNumeric` для сравнения элементов-чисел можно упростить до одной строчки. Как? + +[hide text="Показать простой вариант `compareNumeric`"] +Функция должна возвращать положительное число, если `a > b`, отрицательное, если наоборот, и, например, `0`, если числа равны. + +Всем этим требованиям удовлетворяет функция: + +```js +function compareNumeric(a, b) { + return a - b; +} +``` + +[/hide] + + +## reverse + +Метод [arr.reverse()](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reverse) меняет порядок элементов в массиве на обратный. + +```js +//+ run +var arr = [1,2,3]; +arr.reverse(); + +alert(arr); // 3,2,1 +``` + +## concat + +Метод [arr.concat(value1, value2, ... valueN)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/concat) создаёт новый массив, в который копируются элементы из `arr`, а также `value1, value2, ... valueN`. + +Например: + +```js +//+ run +var arr = [1,2]; +*!* +var newArr = arr.concat(3,4); +*/!* + +alert(newArr); // 1,2,3,4 +``` + +**Если `value` -- массив, то `concat` добавляет его элементы.** + +Например: + +```js +//+ run +var arr = [1,2]; +*!* +var newArr = arr.concat( [3,4], 5);// то же самое, что arr.concat(3,4,5) +*/!* + +alert(newArr); // 1,2,3,4,5 +``` + +## indexOf/lastIndexOf + +Эти методы не поддерживаются в IE<9. Для их поддержки подключите библиотеку [ES5-shim](https://github.com/kriskowal/es5-shim). + + +Метод ["arr.indexOf(searchElement[, fromIndex])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf) возвращает номер элемента `searchElement` в массиве `arr` или `-1`, если его нет. + +Поиск начинается с номера `fromIndex`, если он указан. Если нет -- с начала массива. + +**Для поиска используется строгое сравнение `===`.** + +Например: + +```js +//+ run +var arr = [ 1, 0, false ]; + +alert( arr.indexOf(0) ); // 1 +alert( arr.indexOf(false) ); // 2 +alert( arr.indexOf(null) ); // -1 +``` + +Как вы могли заметить, по синтаксису он полностью аналогичен методу [indexOf для строк](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/indexOf). + +Метод ["arr.lastIndexOf(searchElement[, fromIndex])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/lastIndexOf) ищет справа-налево: с конца массива или с номера `fromIndex`, если он указан. + +[warn header="Методы `indexOf/lastIndexOf` осуществляют поиск перебором"] +Если нужно проверить, существует ли значение в массиве -- его нужно перебрать. Только так. Внутренняя реализация `indexOf/lastIndexOf` осуществляет полный перебор, аналогичный циклу `for` по массиву. Чем длиннее массив, тем дольше он будет работать. +[/warn] + +[smart header="Коллекция уникальных элементов"] +Рассмотрим задачу -- есть коллекция строк, и нужно быстро проверять: есть ли в ней какой-то элемент. Массив для этого не подходит из-за медленного `indexOf`. Но подходит объект! Доступ к свойству объекта осуществляется очень быстро, так что можно сделать все элементы ключами объекта и проверять, есть ли уже такой ключ. + +Например, организуем такую проверку для коллекции строк `"div"`, `"a"` и `"form"`: + +```js +var store = { }; // объект для коллекции + +var items = ["div", "a", "form"]; + +for(var i=0; i +
  • `push/pop`, `shift/unshift`, `splice` -- для добавления и удаления элементов.
  • +
  • `join/split` -- для преобразования строки в массив и обратно.
  • +
  • `sort` -- для сортировки массива. Если не передать функцию сравнения -- сортирует элементы как строки.
  • +
  • `reverse` -- меняет порядок элементов на обратный.
  • +
  • `concat` -- объединяет массивы.
  • +
  • `indexOf/lastIndexOf` -- возвращают позицию элемента в массиве (не поддерживается в IE<9).
  • + + +Изученных нами методов достаточно в 95% случаях, но существуют и другие. Для знакомства с ними рекомендуется заглянуть в справочник Array и [Array в Mozilla Developer Network](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array). \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-iteration/1-rewrite-for-map/solution.md b/1-js/4-data-structures/8-array-iteration/1-rewrite-for-map/solution.md new file mode 100644 index 00000000..3b1d135e --- /dev/null +++ b/1-js/4-data-structures/8-array-iteration/1-rewrite-for-map/solution.md @@ -0,0 +1,15 @@ + + +```js +//+ run +var arr = ["Есть", "жизнь", "на", "Марсе"]; + +*!* +var arrLength = arr.map(function(item) { + return item.length; +}); +*/!* + +alert( arrLength ); // 4,5,2,5 +``` + diff --git a/1-js/4-data-structures/8-array-iteration/1-rewrite-for-map/task.md b/1-js/4-data-structures/8-array-iteration/1-rewrite-for-map/task.md new file mode 100644 index 00000000..3ef6e594 --- /dev/null +++ b/1-js/4-data-structures/8-array-iteration/1-rewrite-for-map/task.md @@ -0,0 +1,21 @@ +# Перепишите цикл через map + +[importance 5] + +Код ниже получает из массива строк новый массив, содержащий их длины: + +```js +//+ run +var arr = ["Есть", "жизнь", "на", "Марсе"]; + +*!* +var arrLength = []; +for(var i=0; i +
  • Функция не должна модифицировать входной массив.
  • +
  • В решении используйте метод `arr.reduce`.
  • + \ No newline at end of file diff --git a/1-js/4-data-structures/8-array-iteration/article.md b/1-js/4-data-structures/8-array-iteration/article.md new file mode 100644 index 00000000..90fe2ce4 --- /dev/null +++ b/1-js/4-data-structures/8-array-iteration/article.md @@ -0,0 +1,227 @@ +# Массив: перебирающие методы + +Современный стандарт JavaScript предоставляет много методов для "умного" перебора массивов, которые есть в современных браузерах... + +...Ну а для их поддержки в IE8- просто подключите библиотеку [ES5-shim](https://github.com/kriskowal/es5-shim). +[cut] +## forEach + +Метод ["arr.forEach(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach) используется для перебора массива. + +Он позволяет перебрать массив при помощи функции `callback`, что зачастую гораздо элегантнее, нежели цикл `for`. + +Функция `callback` вызывается для каждого элемента с тремя параметрами `callback(item, i, arr)`: + +
      +
    • `item` -- очередной элемент массива.
    • +
    • `i` -- его номер.
    • +
    • `arr` -- массив, который перебирается.
    • +
    + +Например: + +```js +//+ run +var arr = ["Яблоко", "Апельсин", "Груша"]; + +function show(item, i, arr) { + alert(i + ": " + item + " (массив:" + arr + ")"); +} + +arr.forEach(show); +``` + +Второй, необязательный аргумент `forEach` позволяет указать контекст `this` для `callback`. Мы обсудим его в деталях чуть позже, сейчас он нам не важен. + +Метод `forEach` ничего не возвращает, его используют только для перебора. + +## filter + +Метод ["arr.filter(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter) используется для *фильтрации* массива через функцию. + +Он создаёт новый массив, в который войдут только те элементы `arr`, для которых вызов `callback(item, i, arr)` возвратит `true`. + +Например: + +```js +//+ run +var arr = [1, -1, 2, -2, 3]; + +function isPositive(number) { + return number > 0; +} + +*!* +var positiveArr = arr.filter(isPositive); +*/!* + +alert(positiveArr); // 1,2,3 +``` + +## map + +Метод ["arr.map(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map) используется для *трансформации* массива. + +Он создаёт новый массив, который будет состоять из результатов вызова `callback(item, i, arr)` для каждого элемента `arr`. + +Например: + +```js +//+ run +var arr = [1, 2, 3, 4]; + +function square(number) { + return number * number; +} + +*!* +var squaredArr = arr.map(square); +*/!* + +alert(squaredArr); // получили массив квадратов чисел: 1, 4, 9, 16 +``` + +## every/some + +Эти методы используется для проверки массива. + +Метод ["arr.every(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/every) возвращает `true`, если вызов `callback` вернёт `true` для *каждого* элемента `arr`. + + +Метод ["arr.some(callback[, thisArg])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some) возвращает `true`, если вызов `callback` вернёт `true` для *какого-нибудь* элемента `arr`. + +```js +//+ run +var arr = [1, -1, 2, -2, 3]; + +function isPositive(number) { + return number > 0; +} + +*!* +alert( arr.every(isPositive) ); // false, не все положительные +alert( arr.some(isPositive) ); // true, есть хоть одно положительное +*/!* +``` + +## reduce/reduceRight + +Метод ["arr.reduce(callback[, initialValue])"](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce) используется для последовательной обработки каждого элемента массива с сохранением промежуточного результата. + +Это один из самых сложных методов для работы с массивами. Но его стоит освоить, потому что временами с его помощью можно в несколько строк решить задачу, которая иначе потребовала бы в разы больше места и времени. + +Метод `reduce` используется для вычисления на основе массива какого-либо единого значения, иначе говорят "для свёртки массива". Чуть далее мы разберём пример для вычисления суммы. + +Он применяет функцию `callback` по очереди к каждому элементу массива слева направо, сохраняя при этом промежуточный результат. + +Аргументы функции `callback(previousValue, currentItem, index, arr)`: + +
      +
    • `previousValue` -- последний результат вызова функции, он же "промежуточный результат". Значение `previousValue` при первом вызове равно `initialValue` (второй аргумент `reduce`) или, если у `reduce` нет второго аргумента, то оно равно первому элементу массива, а перебор начинается со второго.
    • +
    • `currentItem` -- текущий элемент массива, элементы перебираются по очереди слева-направо.
    • +
    • `index` -- номер текущего элемента.
    • +
    • `arr` -- обрабатываемый массив.
    • +
    + +Разберём работу метода `reduce` на примере. + +Пусть мы хотим вычислить сумму всех элементов массива. Можно сделать это при помощи цикла, но это как раз подходящий повод познакомиться с `reduce`. + +Вот решение в одну строку: + +```js +//+ run +var arr = [1, 2, 3, 4, 5] + +var result = arr.reduce(function(prev, current) { return prev + current }, 0); + +alert(result); // 15 +``` + +Разберём, что в нём происходит. + +Здесь начальное значение, с которого начинаются вычисления, равно нулю (второй аргумент `reduce`). + +Сначала анонимная функция вызывается с этим начальным значением и первым элементом массива, результат запоминается и передаётся в следующий вызов, уже со вторым аргументом массива, затем новое значение участвует в вычислениях с третьим аргументом и так далее. + +Таблица вычислений получается такая: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    `prev``current`результат
    первый вызов`0``1``1`
    второй вызов`1``2``3`
    третий вызов`3``3``6`
    четвёртый вызов`6``4``10`
    пятый вызов`10``5``15`
    + +Функция-аргумент `reduce` могла бы также использовать параметры `i` и `array`, но здесь в них нет нужды. + +**Можно сделать ещё короче!** + +Посмотрим, что будет, если не указать `initialValue` в вызове `arr.reduce`: + +```js +//+ run +var arr = [1, 2, 3, 4, 5] + +// убрали 0 в конце +var result = arr.reduce(function(prev, current) { return prev + current }); + +alert(result); // 15 +``` + +Результат -- точно такой же! Это потому, что при отсутствии `initialValue` в качестве первого значения берётся первый элемент массива, а перебор стартует со второго. + +Таблица вычислений будет такая же, за вычетом первой строки. + +**Метод [arr.reduceRight](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduceRight) работает аналогично, но идёт по массиву справа-налево:** + + + +## Итого + +Мы рассмотрели методы: +
      +
    • `forEach` -- для *перебора* массива.
    • +
    • `filter` -- для *фильтрации* массива.
    • +
    • `every/some` -- для *проверки* массива.
    • +
    • `map` -- для *трансформации* массива в массив.
    • +
    • `reduce/reduceRight` -- для *прохода по массиву с вычислением значения*.
    • +
    + +Во многих ситуациях их использование позволяет написать код короче и понятнее, чем обычный перебор через `for`. + \ No newline at end of file diff --git a/1-js/4-data-structures/9-arguments-pseudoarray/1-check-arguments-undefined/solution.md b/1-js/4-data-structures/9-arguments-pseudoarray/1-check-arguments-undefined/solution.md new file mode 100644 index 00000000..277201c7 --- /dev/null +++ b/1-js/4-data-structures/9-arguments-pseudoarray/1-check-arguments-undefined/solution.md @@ -0,0 +1,12 @@ +Узнать количество реально переданных аргументов можно по значению `arguments.length`: + +```js +//+ run +function f(x) { + alert(arguments.length ? 1 : 0); +} + +f(undefined); +f(); +``` + diff --git a/1-js/4-data-structures/9-arguments-pseudoarray/1-check-arguments-undefined/task.md b/1-js/4-data-structures/9-arguments-pseudoarray/1-check-arguments-undefined/task.md new file mode 100644 index 00000000..7cfd2562 --- /dev/null +++ b/1-js/4-data-structures/9-arguments-pseudoarray/1-check-arguments-undefined/task.md @@ -0,0 +1,16 @@ +# Проверка на аргумент-undefined + +[importance 5] + +Как в функции отличить отсутствующий аргумент от `undefined`? + +```js +function f(x) { + // ..ваш код.. + // выведите 1, если первый аргумент есть, и 0 - если нет +} + +f(undefined); // 1 +f(); // 0 +``` + diff --git a/1-js/4-data-structures/9-arguments-pseudoarray/2-sum-arguments/solution.md b/1-js/4-data-structures/9-arguments-pseudoarray/2-sum-arguments/solution.md new file mode 100644 index 00000000..c957acc6 --- /dev/null +++ b/1-js/4-data-structures/9-arguments-pseudoarray/2-sum-arguments/solution.md @@ -0,0 +1,21 @@ + + +```js +//+ run +function sum() { + var result = 0; + + for(var i=0; iarguments. + +Он содержит список аргументов по номерам: `arguments[0]`, `arguments[1]`..., а также свойство `length`. + +Например, выведем список всех аргументов: + +```js +//+ run +function sayHi() { + for (var i=0; i +
    copy(dst, src1, src2...)
    +
    Копирует свойства из объектов `src1, src2,...` в объект `dst`. Возвращает получившийся объект.
    + + +Использование: + +
      +
    • Для объединения нескольких объектов в один: + +```js +//+ run +var vasya = { + age: 21, + name: 'Вася', + surname: 'Петров' +}; + +var user = { + isAdmin: false, + isEmailConfirmed: true +}; + +var student = { + university: 'My university' +}; + +// добавить к vasya свойства из user и student +*!* +copy(vasya, user, student); +*/!* + +alert(vasya.isAdmin); // false +alert(vasya.university); // My university +``` + +
    • +
    • Для создания копии объекта `user`: + +```js +// скопирует все свойства в пустой объект +var userClone = copy({}, user); +``` + +Такой "клон" объекта может пригодиться там, где мы хотим изменять его свойства, при этом не трогая исходный объект `user`. + +В нашей реализации мы будем копировать только свойства первого уровня, то есть вложенные объекты как-то особым образом не обрабатываются. Впрочем, её можно расширить.
    • +
    + +А вот и реализация: + +```js +//+ autorun +function copy() { + var dst = arguments[0]; + for (var i=1; i +
  • Полный список аргументов, с которыми вызвана функция, доступен через `arguments`.
  • +
  • Это псевдомассив, то есть объект, который похож на массив, в нём есть нумерованные свойства и `length`, но методов массива у него нет.
  • +
  • В старом стандарте было свойство `arguments.callee` со ссылкой на текущую функцию, а также свойство `arguments.callee.caller`, содержащее ссылку на функцию, которая вызвала данную. Эти свойства устарели, при `use strict` обращение к ним приведёт к ошибке.
  • +
  • Для указания аргументов по умолчанию, в тех случаях, когда они заведомо не `false`, удобен оператор `||`.
  • + + +В тех случаях, когда возможных аргументов много и, в особенности, когда большинство их имеют значения по умолчанию, вместо работы с `arguments` организуют передачу данных через объект, который как правило называют `options`. + +Возможен и гибридный подход, при котором первый аргумент обязателен, а второй -- `options`, который содержит всевозможные дополнительные параметры: + +```js +function showMessage(text, options) { + // показать сообщение text, настройки показа указаны в options +} +``` + diff --git a/1-js/4-data-structures/index.md b/1-js/4-data-structures/index.md new file mode 100644 index 00000000..774dee46 --- /dev/null +++ b/1-js/4-data-structures/index.md @@ -0,0 +1,3 @@ +# Структуры данных + +Изучаем JavaScript: расширенное знакомство со встроенными типами данных, их особенностями. \ No newline at end of file diff --git a/1-js/5-functions-closures/1-global-object/1-window-and-variable/solution.md b/1-js/5-functions-closures/1-global-object/1-window-and-variable/solution.md new file mode 100644 index 00000000..f889e997 --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/1-window-and-variable/solution.md @@ -0,0 +1,24 @@ +Ответ: `1`. + +```js +//+ run untrusted refresh +if ("a" in window) { + var a = 1; +} +alert(a); +``` + +Посмотрим, почему. + +На стадии подготовки к выполнению, из `var a` создается `window.a`: + +```js +// window = {a:undefined} + +if ("a" in window) { // в if видно что window.a уже есть + var a = 1; // поэтому эта строка сработает +} +alert(a); +``` + +В результате `a` становится `1`. \ No newline at end of file diff --git a/1-js/5-functions-closures/1-global-object/1-window-and-variable/task.md b/1-js/5-functions-closures/1-global-object/1-window-and-variable/task.md new file mode 100644 index 00000000..d06c144a --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/1-window-and-variable/task.md @@ -0,0 +1,13 @@ +# Window и переменная + +[importance 5] + +Каков будет результат кода? + +```js +if ("a" in window) { + var a = 1; +} +alert(a); +``` + diff --git a/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/solution.md b/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/solution.md new file mode 100644 index 00000000..379979fc --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/solution.md @@ -0,0 +1,12 @@ +Ответ: **ошибка**. + +Переменной `a` нет, так что условие `"a" in window` не выполнится. В результате на последней строчке - обращение к неопределенной переменной. + +```js +//+ run untrusted refresh +if ("a" in window) { + a = 1; +} +alert(a); // <-- error! +``` + diff --git a/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/task.md b/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/task.md new file mode 100644 index 00000000..b88a32f9 --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/2-window-and-variable-2/task.md @@ -0,0 +1,13 @@ +# Window и переменная 2 + +[importance 5] + +Каков будет результат (перед `a` нет `var`)? + +```js +if ("a" in window) { + a = 1; +} +alert(a); +``` + diff --git a/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/solution.md b/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/solution.md new file mode 100644 index 00000000..dd910b4d --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/solution.md @@ -0,0 +1,14 @@ +Ответ: `1`. + +Переменная `a` создается до начала выполнения кода, так что условие `"a" in window` выполнится и сработает `a = 1`. + +```js +//+ run untrusted refresh +if ("a" in window) { + a = 1; +} +var a; + +alert(a); // 1 +``` + diff --git a/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/task.md b/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/task.md new file mode 100644 index 00000000..6575a422 --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/3-window-and-variable-3/task.md @@ -0,0 +1,15 @@ +# Window и переменная 3 + +[importance 5] + +Каков будет результат (перед `a` нет `var`, а ниже есть)? + +```js +if ("a" in window) { + a = 1; +} +var a; + +alert(a); +``` + diff --git a/1-js/5-functions-closures/1-global-object/4-function-and-variable/solution.md b/1-js/5-functions-closures/1-global-object/4-function-and-variable/solution.md new file mode 100644 index 00000000..33fe8cf2 --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/4-function-and-variable/solution.md @@ -0,0 +1,19 @@ +Ответ: `5`. + +```js +//+ run untrusted +var a = 5; + +function a() { } + +alert(a); +``` + +Чтобы понять, почему -- разберём внимательно как работает этот код. +
      +
    1. До начала выполнения создаётся переменная `a` и функция `a`. Стандарт написан так, что функция создаётся первой и переменная ее не перезаписывает. То есть, функция имеет приоритет. Но в данном случае это совершенно неважно, потому что... +
    2. +
    3. ...После инициализации, когда код начинает выполняться -- срабатывает присваивание `a = 5`, перезаписывая `a`, и уже не важно, что там лежало.
    4. +
    5. Объявление `Function Declaration` на стадии выполнения игнорируется (уже обработано).
    6. +
    7. В результате `alert(a)` выводит 5.
    8. +
    diff --git a/1-js/5-functions-closures/1-global-object/4-function-and-variable/task.md b/1-js/5-functions-closures/1-global-object/4-function-and-variable/task.md new file mode 100644 index 00000000..ee8502d0 --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/4-function-and-variable/task.md @@ -0,0 +1,15 @@ +# Функция и переменная + +[importance 3] + +Каков будет результат кода? Почему? + +```js +var a = 5; + +function a() { } + +alert(a); +``` + +P.S. Это задание -- учебное, на понимание процесса инициализации и выполнения. В реальной жизни мы, конечно же, не будем называть переменную и функцию одинаково. diff --git a/1-js/5-functions-closures/1-global-object/article.md b/1-js/5-functions-closures/1-global-object/article.md new file mode 100644 index 00000000..e4d9ad5d --- /dev/null +++ b/1-js/5-functions-closures/1-global-object/article.md @@ -0,0 +1,242 @@ +# Глобальный объект + +Механизм работы функций и переменных в JavaScript очень отличается от большинства языков. + +Чтобы его понять, мы в этой главе рассмотрим переменные и функции в глобальной области. А в следующей -- пойдём дальше. + +[cut] + +## Глобальный объект + +*Глобальными* называют переменные и функции, которые не находятся внутри какой-то функции. То есть, иными словами, если переменная или функция не находятся внутри конструкции `function`, то они -- "глобальные". + +**В JavaScript все глобальные переменные и функции являются свойствами специального объекта, который называется *"глобальный объект"* (`global object`).** + +В браузере этот объект явно доступен под именем `window`. Объект `window` одновременно является глобальным объектом и содержит ряд свойств и методов для работы с окном браузера, но нас здесь интересует только его роль как глобального объекта. + +В других окружениях, например Node.JS, глобальный объект может быть недоступен в явном виде, но суть происходящего от этого не изменяется, поэтому далее для обозначения глобального объекта мы будем использовать `"window"`. + +**Присваивая или читая глобальную переменную, мы, фактически, работаем со свойствами `window`.** + +Например: + +```js +//+ run untrusted refresh +var a = 5; // объявление var создаёт свойство window.a +alert(window.a); // 5 +``` + +Создать переменную можно и явным присваиванием в `window`: + +```js +//+ run untrusted refresh +window.a = 5; +alert(a); // 5 +``` + +## Порядок инициализации + +Выполнение скрипта происходит в две фазы: +
      +
    1. На первой фазе происходит инициализация, подготовка к запуску. + +Во время инициализации скрипт сканируется на предмет объявления функций вида [Function Declaration](/function-declaration-expression), а затем -- на предмет объявления переменных `var`. Каждое такое объявление добавляется в `window`. + +**Функции, объявленные как Function Declaration, создаются сразу работающими, а переменные -- равными `undefined`.** +
    2. +
    3. На второй фазе -- собственно, выполнение. + +Присваивание (`=`) значений переменных происходит на второй фазе, когда поток выполнения доходит до соответствующей строчки кода. +
    4. +
    + +В начале кода ниже указано содержание глобального объекта на момент окончания инициализации: + +```js +// По окончании инициализации, до выполнения кода: +*!* +// window = { f: function, a: undefined, g: undefined } +*/!* + +var a = 5; // при инициализации даёт: window.a=undefined + +function f(arg) { /*...*/ } // при инициализации даёт: window.f = function + +var g = function(arg) { /*...*/ }; // при инициализации даёт: window.g = undefined +``` + +Кстати, тот факт, что к началу выполнения кода переменные и функции *уже* содержатся в `window`, можно легко проверить: + +```js +//+ run untrusted refresh + +alert("a" in window); // *!*true*/!*, т.к. есть свойство window.a +alert(a); // равно *!*undefined*/!*, присваивание будет выполнено далее +alert(f); // *!*function ...*/!*, готовая к выполнению функция +alert(g); // *!*undefined*/!*, т.к. это переменная, а не Function Declaration + +var a = 5; +function f() { /*...*/ } +var g = function() { /*...*/ }; +``` + +[smart header="Присвоение переменной без объявления"] +В старом стандарте JavaScript переменную можно было создать и без объявления `var`: + +```js +//+ run +a = 5; + +alert(a); // 5 +``` + +Такое присвоение, как и `var a = 5`, создает свойство `window.a = 5`. Отличие от `var a = 5` -- в том, что переменная будет создана не на этапе входа в область видимости, а в момент присвоения. + +Сравните два кода ниже. + +Первый выведет `undefined`, так как переменная была добавлена в `window` на фазе инициализации: + +```js +//+ run untrusted refresh +*!* +alert(a); // undefined +*/!* + +var a = 5; +``` + +Второй код выведет ошибку, так как переменной ещё не существует: + +```js +//+ run untrusted refresh +*!* +alert(a); // error, a is not defined +*/!* + +a = 5; +``` + +**Вообще, рекомендуется всегда объявлять переменные через `var`.** + +В современном стандарте присваивание без `var` вызовет ошибку: + +```js +//+ run +'use strict'; +a = 5; // error, a is not defined +``` + +[/smart] + +[smart header="Конструкции `for, if...` не влияют на видимость переменных"] +Фигурные скобки, которые используются в `for, while, if`, в отличие от объявлений функции, имеют "декоративный" характер. + +В JavaScript нет разницы между объявлением вне блока: + +```js +*!*var*/!* i; +{ + i = 5; +} +``` + +...И внутри него: + +```js +i = 5; +{ + *!*var*/!* i; +} +``` + +**Также нет разницы между объявлением в цикле и вне его:** + +```js +//+ run untrusted refresh +for (*!*var*/!* i=0; i<5; i++) { } +``` + +Идентичный по функциональности код: + +```js +//+ run untrusted refresh +*!*var i;*/!* +for (i=0; i<5; i++) { } +``` + +В обоих случаях переменная будет создана до выполнения цикла, на стадии инициализации, и ее значение будет сохранено после окончания цикла. + +[/smart] + +[smart header="Не важно, где и сколько раз объявлена переменная"] + +Объявлений `var` может быть сколько угодно: + +```js +var i = 10; + +for (var i=0; i<20; i++) { + ... +} + +var i = 5; +``` + +**Все `var` будут обработаны один раз, на фазе инициализации.** + +На фазе исполнения объявления `var` будут проигнорированы: они уже были обработаны. Зато будут выполнены присваивания. +[/smart] + + +[warn header="Ошибки при работе с `window` в IE8-"] + +В старых IE есть две забавные ошибки при работе с переменными в `window`: + +
      +
    1. Переопределение переменной, у которой такое же имя, как и `id` элемента, приведет к ошибке: + +```html + +
      ...
      + +``` + +А если сделать через `var`, то всё будет хорошо. + +Это была реклама того, что надо везде ставить `var`. + +
    2. +
    3. Ошибка при рекурсии через функцию-свойство `window`. Следующий код "умрет" в IE<9: + +```html + + +``` + +Проблема здесь возникает из-за того, что функция напрямую присвоена в `window.recurse = ...`. Ее не будет при обычном объявлении функции. + +**Этот пример выдаст ошибку только в настоящем IE8!** Не IE9 в режиме эмуляции. Вообще, режим эмуляции позволяет отлавливать где-то 95% несовместимостей и проблем, а для оставшихся 5% вам нужен будет настоящий IE8 в виртуальной машине. +
    4. +
    +[/warn] + + +## Итого + +В результате инициализации, к началу выполнения кода: +
      +
    1. Функции, объявленные как `Function Declaration`, создаются полностью и готовы к использованию.
    2. +
    3. Переменные объявлены, но равны `undefined`. Присваивания выполнятся позже, когда выполнение дойдет до них.
    4. +
    + + diff --git a/1-js/5-functions-closures/2-closures/1-say-phrase-first/solution.md b/1-js/5-functions-closures/2-closures/1-say-phrase-first/solution.md new file mode 100644 index 00000000..7d7d24e9 --- /dev/null +++ b/1-js/5-functions-closures/2-closures/1-say-phrase-first/solution.md @@ -0,0 +1,17 @@ +Ошибки не будет, выведет `"Вася, undefined"`. + +```js +//+ run + +*!* +say('Вася'); // Что выведет? Не будет ли ошибки? +*/!* + +var phrase = 'Привет'; + +function say(name) { + alert(name + ", " + phrase); +} +``` + +Переменная как таковая существует, вот только на момент запуска функции она равна `undefined`. \ No newline at end of file diff --git a/1-js/5-functions-closures/2-closures/1-say-phrase-first/task.md b/1-js/5-functions-closures/2-closures/1-say-phrase-first/task.md new file mode 100644 index 00000000..37162cd1 --- /dev/null +++ b/1-js/5-functions-closures/2-closures/1-say-phrase-first/task.md @@ -0,0 +1,19 @@ +# Что выведет say в начале кода? + +[importance 5] + +Что будет, если вызов `sayHi('Вася');` стоит в самом-самом начале, в первой строке кода? + +```js + +*!* +say('Вася'); // Что выведет? Не будет ли ошибки? +*/!* + +var phrase = 'Привет'; + +function say(name) { + alert(name + ", " + phrase); +} +``` + diff --git a/1-js/5-functions-closures/2-closures/1.png b/1-js/5-functions-closures/2-closures/1.png new file mode 100755 index 00000000..bbbbb2bc Binary files /dev/null and b/1-js/5-functions-closures/2-closures/1.png differ diff --git a/1-js/5-functions-closures/2-closures/1@2x.png b/1-js/5-functions-closures/2-closures/1@2x.png new file mode 100755 index 00000000..d6fea260 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/1@2x.png differ diff --git a/1-js/5-functions-closures/2-closures/2-which-value-is-modified/solution.md b/1-js/5-functions-closures/2-closures/2-which-value-is-modified/solution.md new file mode 100644 index 00000000..eed571f3 --- /dev/null +++ b/1-js/5-functions-closures/2-closures/2-which-value-is-modified/solution.md @@ -0,0 +1,9 @@ +**Результатом будет `true`**, т.к. `var` обработается и переменная будет создана до выполнения кода. + +Соответственно, присвоение `value=true` сработает на локальной переменной, и `alert` выведет `true`. + +**Внешняя переменная не изменится.** + +P.S. Если `var` нет, то в функции переменная не будет найдена. Интерпретатор обратится за ней в `window` и изменит её там. + +**Так что без `var` результат будет также `true`, но внешняя переменная изменится.** diff --git a/1-js/5-functions-closures/2-closures/2-which-value-is-modified/task.md b/1-js/5-functions-closures/2-closures/2-which-value-is-modified/task.md new file mode 100644 index 00000000..9472005a --- /dev/null +++ b/1-js/5-functions-closures/2-closures/2-which-value-is-modified/task.md @@ -0,0 +1,25 @@ +# В какую переменную будет присвоено значение? + +[importance 5] + +Каков будет результат выполнения этого кода? + +```js +var value = 0; + +function f() { + if (1) { + value = true; + } else { + var value = false; + } + + alert(value); +} + +f(); +``` + +Изменится ли внешняя переменная `value` ? + +P.S. Какими будут ответы, если из строки `var value = false` убрать `var`? \ No newline at end of file diff --git a/1-js/5-functions-closures/2-closures/2.png b/1-js/5-functions-closures/2-closures/2.png new file mode 100755 index 00000000..19e1a6d2 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/2.png differ diff --git a/1-js/5-functions-closures/2-closures/2@2x.png b/1-js/5-functions-closures/2-closures/2@2x.png new file mode 100755 index 00000000..fc810e06 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/2@2x.png differ diff --git a/1-js/5-functions-closures/2-closures/3-var-window/solution.md b/1-js/5-functions-closures/2-closures/3-var-window/solution.md new file mode 100644 index 00000000..da66ef7a --- /dev/null +++ b/1-js/5-functions-closures/2-closures/3-var-window/solution.md @@ -0,0 +1,29 @@ +Результатом будет `undefined`, затем `5`. + +```js +//+ run +function test() { + + alert(window); + + var window = 5; + + alert(window); +} + +test(); +``` + +Такой результат получился потом, что `window` -- это глобальная переменная, но ничто не мешает объявить такую же локальную. + +Директива `var window` обработается до начала выполнения кода функции и будет создана локальная переменная, т.е. свойство `LexicalEnvironment.window`: + +```js +LexicalEnvironment = { + window: undefined +} +``` + +Когда выполнение кода начнется и сработает `alert`, он выведет уже локальную переменную, которая на тот момент равна `undefined`. + +Затем сработает присваивание, и второй `alert` выведет уже `5`. \ No newline at end of file diff --git a/1-js/5-functions-closures/2-closures/3-var-window/task.md b/1-js/5-functions-closures/2-closures/3-var-window/task.md new file mode 100644 index 00000000..1aa6c7a9 --- /dev/null +++ b/1-js/5-functions-closures/2-closures/3-var-window/task.md @@ -0,0 +1,19 @@ +# var window + +[importance 5] + +Каков будет результат выполнения этого кода? Почему? + +```js +function test() { + + alert(window); + + var window = 5; + + alert(window); +} + +test(); +``` + diff --git a/1-js/5-functions-closures/2-closures/3.png b/1-js/5-functions-closures/2-closures/3.png new file mode 100755 index 00000000..caa7d158 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/3.png differ diff --git a/1-js/5-functions-closures/2-closures/3@2x.png b/1-js/5-functions-closures/2-closures/3@2x.png new file mode 100755 index 00000000..68c14b75 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/3@2x.png differ diff --git a/1-js/5-functions-closures/2-closures/4-call-inplace/solution.md b/1-js/5-functions-closures/2-closures/4-call-inplace/solution.md new file mode 100644 index 00000000..204c334e --- /dev/null +++ b/1-js/5-functions-closures/2-closures/4-call-inplace/solution.md @@ -0,0 +1,37 @@ +Результат - **ошибка**. Попробуйте: + +```js +//+ run +var a = 5 + +(function() { + alert(a) +})() +``` + +Дело в том, что после `var a = 5` нет точки с запятой. + +JavaScript воспринимает этот код как если бы перевода строки не было: + +```js +//+ run +var a = 5(function() { + alert(a) +})() +``` + +То есть, он пытается вызвать *функцию* `5`, что и приводит к ошибке. + +Если точку с запятой поставить, все будет хорошо: + +```js +//+ run +var a = 5; + +(function() { + alert(a) +})() +``` + +Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто *не* ставит точки с запятой. + diff --git a/1-js/5-functions-closures/2-closures/4-call-inplace/task.md b/1-js/5-functions-closures/2-closures/4-call-inplace/task.md new file mode 100644 index 00000000..988f0273 --- /dev/null +++ b/1-js/5-functions-closures/2-closures/4-call-inplace/task.md @@ -0,0 +1,16 @@ +# Вызов "на месте" + +[importance 4] + +Каков будет результат выполнения кода? Почему? + +```js +var a = 5 + +(function() { + alert(a) +})() +``` + +P.S. *Подумайте хорошо! Здесь все ошибаются!* +P.P.S. *Внимание, здесь подводный камень! Ок, вы предупреждены.* diff --git a/1-js/5-functions-closures/2-closures/4.png b/1-js/5-functions-closures/2-closures/4.png new file mode 100755 index 00000000..4282808b Binary files /dev/null and b/1-js/5-functions-closures/2-closures/4.png differ diff --git a/1-js/5-functions-closures/2-closures/4@2x.png b/1-js/5-functions-closures/2-closures/4@2x.png new file mode 100755 index 00000000..adc4f5ff Binary files /dev/null and b/1-js/5-functions-closures/2-closures/4@2x.png differ diff --git a/1-js/5-functions-closures/2-closures/5-access-outer-variable/solution.md b/1-js/5-functions-closures/2-closures/5-access-outer-variable/solution.md new file mode 100644 index 00000000..4e0a619e --- /dev/null +++ b/1-js/5-functions-closures/2-closures/5-access-outer-variable/solution.md @@ -0,0 +1,3 @@ +Нет, нельзя. + +Локальная переменная полностью перекрывает внешнюю. diff --git a/1-js/5-functions-closures/2-closures/5-access-outer-variable/task.md b/1-js/5-functions-closures/2-closures/5-access-outer-variable/task.md new file mode 100644 index 00000000..10e9de72 --- /dev/null +++ b/1-js/5-functions-closures/2-closures/5-access-outer-variable/task.md @@ -0,0 +1,17 @@ +# Перекрытие переменной + +[importance 4] + +Если во внутренней функции есть своя переменная с именем `currentCount` -- можно ли в ней получить `currentCount` из внешней функции? + +```js +function makeCounter() { + var currentCount = 1; + + return function() { + var currentCount; + // можно ли здесь вывести currentCount из внешней функции (равный 1)? + }; +} +``` + diff --git a/1-js/5-functions-closures/2-closures/5.png b/1-js/5-functions-closures/2-closures/5.png new file mode 100755 index 00000000..42dd9304 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/5.png differ diff --git a/1-js/5-functions-closures/2-closures/5@2x.png b/1-js/5-functions-closures/2-closures/5@2x.png new file mode 100755 index 00000000..fca45921 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/5@2x.png differ diff --git a/1-js/5-functions-closures/2-closures/6-counter-window-variable/solution.md b/1-js/5-functions-closures/2-closures/6-counter-window-variable/solution.md new file mode 100644 index 00000000..fc3e459d --- /dev/null +++ b/1-js/5-functions-closures/2-closures/6-counter-window-variable/solution.md @@ -0,0 +1,30 @@ +Выведут **1,2,3,4.** + +Здесь внутренняя функция будет искать -- и находить `currentCount` каждый раз в самом внешнем объекте переменных: глобальном объекте `window`. + +В результате все счётчики будут разделять единое, глобальное текущее значение. + +```js +//+ run +var currentCount = 1; + +function makeCounter() { + return function() { + return currentCount++; + }; +} + +var counter = makeCounter(); +var counter2 = makeCounter(); + +*!* +alert( counter() ); // ? +alert( counter() ); // ? +*/!* + +*!* +alert( counter2() ); // ? +alert( counter2() ); // ? +*/!* +``` + diff --git a/1-js/5-functions-closures/2-closures/6-counter-window-variable/task.md b/1-js/5-functions-closures/2-closures/6-counter-window-variable/task.md new file mode 100644 index 00000000..304e59fd --- /dev/null +++ b/1-js/5-functions-closures/2-closures/6-counter-window-variable/task.md @@ -0,0 +1,29 @@ +# Глобальный счётчик + +[importance 5] + +Что выведут эти вызовы, если переменная `currentCount` находится вне `makeCounter`? + +```js +var currentCount = 1; + +function makeCounter() { + return function() { + return currentCount++; + }; +} + +var counter = makeCounter(); +var counter2 = makeCounter(); + +*!* +alert( counter() ); // ? +alert( counter() ); // ? +*/!* + +*!* +alert( counter2() ); // ? +alert( counter2() ); // ? +*/!* +``` + diff --git a/1-js/5-functions-closures/2-closures/6.png b/1-js/5-functions-closures/2-closures/6.png new file mode 100755 index 00000000..7e01b2c5 Binary files /dev/null and b/1-js/5-functions-closures/2-closures/6.png differ diff --git a/1-js/5-functions-closures/2-closures/6@2x.png b/1-js/5-functions-closures/2-closures/6@2x.png new file mode 100755 index 00000000..38209f2c Binary files /dev/null and b/1-js/5-functions-closures/2-closures/6@2x.png differ diff --git a/1-js/5-functions-closures/2-closures/article.md b/1-js/5-functions-closures/2-closures/article.md new file mode 100644 index 00000000..e79b77bb --- /dev/null +++ b/1-js/5-functions-closures/2-closures/article.md @@ -0,0 +1,435 @@ +# Замыкания, функции изнутри + +В этой главе мы продолжим рассматривать, как работают переменные, и, как следствие, познакомимся с замыканиями. От глобального объекта мы переходим к работе внутри функций. +[cut] +## Лексическое окружение + +Все переменные внутри функции -- это свойства специального внутреннего объекта `LexicalEnvironment`, который создаётся при её запуске. + +Мы будем называть этот объект "лексическое окружение" или просто "объект переменных". + +При запуске функция создает объект `LexicalEnvironment`, записывает туда аргументы, функции и переменные. Процесс инициализации выполняется в том же порядке, что и для глобального объекта, который, вообще говоря, является частным случаем лексического окружения. + +В отличие от `window`, объект `LexicalEnvironment` является внутренним, он скрыт от прямого доступа. + +### Пример + +Посмотрим пример, чтобы лучше понимать, как это работает: + +```js +function sayHi(name) { + var phrase = "Привет, " + name; + alert(phrase); +} + +sayHi('Вася'); +``` + +При вызове функции: +
      +
    1. До выполнения первой строчки её кода, на стадии инициализации, интерпретатор создает пустой объект `LexicalEnvironment` и заполняет его. + +В данном случае туда попадает аргумент `name` и единственная переменная `phrase`: + +```js +function sayHi(name) { +*!* + // LexicalEnvironment = { name: 'Вася', phrase: undefined } +*/!* + var phrase = "Привет, " + name; + alert(phrase); +} + +sayHi('Вася'); +``` + +
    2. +
    3. Функция выполняется. + +Во время выполнения происходит присвоение локальной переменной `phrase`, то есть, другими словами, присвоение свойству `LexicalEnvironment.phrase` нового значения: + +```js +function sayHi(name) { + // LexicalEnvironment = { name: 'Вася', phrase: undefined } + var phrase = "Привет, " + name; + +*!* + // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'} +*/!* + alert(phrase); +} + +sayHi('Вася'); +``` + +
    4. +
    5. В конце выполнения функции объект с переменными обычно выбрасывается и память очищается.
    6. +
    + +[smart header="Тонкости спецификации"] +Если почитать спецификацию ECMA-262, то мы увидим, что речь идёт о двух объектах: `VariableEnvironment` и `LexicalEnvironment`. + +Но там же замечено, что в реализациях эти два объекта могут быть объединены. Так что мы избегаем лишних деталей и используем везде термин `LexicalEnvironment`, это достаточно точно позволяет описать происходящее. + +Более формальное описание находится в спецификации ECMA-262, секции 10.2-10.5 и 13. +[/smart] + + +## Доступ ко внешним переменным + +Из функции мы можем обратиться не только к локальной переменной, но и к внешней: + +```js +var a = 5; + +function f() { + alert(a); // 5 +} +``` + +**Интерпретатор, при доступе к переменной, сначала пытается найти переменную в текущем `LexicalEnvironment`, а затем, если её нет -- ищет во внешнем объекте переменных. В данном случае им является `window`.** + +Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в специальном внутреннем свойстве функции, которое называется `[[Scope]]`. Это свойство закрыто от прямого доступа, но знание о нём очень важно для понимания того, как работает JavaScript. + +Рассмотрим, как `[[Scope]]` создаётся и используется: + +
      +
    1. Всё начинается с момента создания функции. Функция создается не в вакууме, а в некотором лексическом окружении. + +В случае выше функция создается в глобальном лексическом окружении `window`: + + + +**Для того, чтобы функция могла в будущем обратиться к внешним переменным, в момент создания она получает скрытое свойство `[[Scope]]`, которое ссылается на лексическое окружение, в котором она была создана:** + + + +Эта ссылка появляется одновременно с функцией и умирает вместе с ней. Программист не может как-либо получить или изменить её. +
    2. +
    3. Позже, приходит время и функция запускается. + +Интерпретатор вспоминает, что у неё есть свойство `f.[[Scope]]`: + + + +...И использует его при создании объекта переменных для функции. + +**Новый объект `LexicalEnvironment` получает ссылку на "внешнее лексическое окружение" со значением из `[[Scope]]`. Эта ссылка используется для поиска переменных, которых нет в текущей функции.** + + +Например, `alert(a)` сначала ищет в текущем объекте переменных: он пустой. А потом, как показано зеленой стрелкой на рисунке ниже -- по ссылке, во внешнем окружении. + + + + +На уровне кода это выглядит как поиск во внешней области видимости, вне функции: + + + +
    4. +
    + +Если обобщить: +
      +
    • Каждая функция при создании получает ссылку `[[Scope]]` на объект с переменными, в контексте которого была создана.
    • +
    • При запуске функции создаётся новый объект с переменными `LexicalEnvironment`. Он получает ссылку на внешний объект переменных из `[[Scope]]`.
    • +
    • При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом -- по этой ссылке. Благодаря этому в функции доступны внешние переменные.
    • +
    + +### Значение переменных -- всегда текущее + +Значение переменной из внешней области берётся всегда текущее, на момент запуска, а не то, которое было на момент создания функции. + +Например, в коде ниже функция `sayHi` берёт `phrase` из внешней области: + +```js +//+ run + +var phrase = 'Привет'; + +function say(name) { + alert(name + ", " + phrase); +} + +*!* +say('Вася'); // Вася, Привет (*) +*/!* + +phrase = 'Пока'; + +*!* +say('Вася'); // Вася, Пока (**) +*/!* +``` + +На момент выполнения строки `(*)`, переменная `phrase` имела значение `'Привет'`, а потом изменила его на `'Пока'`. Функция всегда берёт то внешнее значение, которое есть сейчас. + +Это естественно, ведь для доступа к внешним переменным функция использует ссылку на внешний объект с ними -- на внешний объект целиком! А не на каждое его свойство (переменную) по отдельности. Если переменная меняется, то при новом доступе функция всегда получит текущее, последнее её значение. + + + + +## Вложенные функции + +Внутри функции можно объявлять не только локальные переменные, но и другие функции. + +К примеру, вложенная функция может помочь лучше организовать код: + +```js +//+ run +function sayHi(firstName, lastName) { + + alert( "Привет, " + getFullName() ); + +*!* + function getFullName() { + return firstName + " " + lastName; + } +*/!* + +} + +sayHi("Вася", "Пупкин"); // Привет, Вася Пупкин +``` + +Здесь, для наглядности, для вычислений создана функция `getFullName()`. + +**Вложенные функции обрабатываются в точности так же, как и глобальные. Единственная разница -- они создаются в объекте переменных внешней функции, а не в `window`.** + +При запуске функции `sayHi("Вася", "Пупкин")`: +
      +
    • Интерпретатор создаст объект для переменных.
    • +
    • Заполнит его аргументами текущего вызова и локальными переменными: + +```js +function sayHi(firstName, lastName) { +*!* +// LexicalEnvironment = { +// firstName: "Вася", +// lastName: "Пупкин", +// getFullName: function +// } +*/!* + + alert( "Привет, " + getFullName() ); // (*) + + function getFullName() { + return firstName + " " + lastName; + } + +} + +sayHi("Вася", "Пупкин"); // Привет, Вася Пупкин +``` + +
    • +
    • Далее, в строке `(*)`, при вызове `getFullName()` -- она получит ссылку на внешний объект переменных, достанет оттуда `firstName` и `lastName`.
    • +
    + + +## Возврат функции + +Рассмотрим более "продвинутый" вариант, при котором внутри одной функции создаётся другая и возвращается в качестве результата. + +Здесь мы будем создавать функцию-счётчик. Это, конечно, учебный пример, дальше будут задачи посложнее, поближе к реальности, ну а при изучении интерфейсов создавать и передавать туда-сюда функцию будет вообще стандартным приёмом разработки. + +В примере ниже `makeCounter` создает функцию, которая считает свои вызовы: + +```js +//+ run +function makeCounter() { + var currentCount = 1; + + return function() { // (**) + return currentCount++; + }; +} + +var counter = makeCounter(); // (*) + +// каждый вызов увеличивает счётчик и возвращает результат +alert( counter() ); // 1 +alert( counter() ); // 2 +alert( counter() ); // 3 + +// создать другой счётчик, он будет независим от первого +var counter2 = makeCounter(); +alert( counter2() ); // 1 +``` + +Хранение текущего числа вызовов осуществляется в переменной `currentCount` внешней функции. + +Что здесь, вообще, происходит? + +**Первый этап -- вызов `makeCounter()`:** + +
      +
    1. В строке `(*)` запускается `makeCounter()`. При этом создаётся `LexicalEnvironment` для переменных текущего вызова. В функции есть одна переменная `var currentCount`, которая станет свойством этого объекта. Она изначально инициализуется в `undefined`, затем, в процессе выполнения, получит значение `1`: + +```js +function makeCounter() { +*!* + // LexicalEnvironment = { currentCount: undefined } -> window +*/!* + + var currentCount = 1; + +*!* + // LexicalEnvironment = { currentCount: 1 } -> window +*/!* + + return function() { // [[Scope]] -> LexicalEnvironment (**) + return currentCount++; + }; +} + +var counter = makeCounter(); // (*) +``` + +
    2. +
    3. В процессе выполнения `makeCounter()` создаёт функцию в строке `(**)`. При создании эта функция получает внутреннее свойство `[[Scope]]` со ссылкой на текущий `LexicalEnvironment`.
    4. +
    5. Далее вызов `makeCounter()` завершается и функция `(**)` возвращается и сохраняется во внешней переменной `counter` `(*)`.
    6. +
    + +**На этом первый этап можно считать завершённым.** + +В результате вызова `makeCounter` в переменную `counter` была записана функция: + +```js +var counter = function() { // [[Scope]] -> {currentCount: 1} -> window + return currentCount++; +}; +``` + +**Возвращённая из `makeCounter()` функция `counter` отличается от "просто функции" тем, что она помнит (через `[[Scope]]`) о том, в каком окружении была создана.** + +Скорее всего, когда-нибудь функция `counter` будет вызвана. Мы не знаем, когда это произойдёт. Может быть, прямо сейчас, но, вообще говоря, совсем не факт. Этот вызов может быть сильно отделён по времени, поэтому назовём происходящее "вторым этапом". + +**Второй этап -- вызов `counter`:** + +
      +
    1. Эта функция состоит из одной строки: `return currentCount++`, ни переменных ни параметров в ней нет, поэтому её собственный объект переменных, для краткости назовём его `LE` -- будет пуст. + +Единственное, что у него есть -- так это ссылка на внешний `LexicalEnvironment`, которую он получит из `[[Scope]]`: + +```js +var counter = function() { + //в процессе запуска LE = {} -> {currentCount: 1} -> window + return currentCount++; +}; +``` + +
    2. +
    3. Чтобы увеличить и вернуть `currentCount`, интерпретатор ищет в текущем объекте переменных `LE`, но он пуст, затем идёт во внешний объект, там находит, изменяет и возвращает новое значение: + +```js +//+ run +function makeCounter() { + var currentCount = 1; + + return function() { + return currentCount++; + }; +} + +var counter = makeCounter(); // [[Scope]] -> {currentCount: 1} -> window + +alert( counter() ); // 1, [[Scope]] -> {currentCount: 1} -> window +alert( counter() ); // 2, [[Scope]] -> {currentCount: 2} -> window +alert( counter() ); // 3, [[Scope]] -> {currentCount: 3} -> window +``` + +
    4. +
    + +Можно создать несколько счётчиков. Все они будут взаимно независимы: + +```js +var counter = makeCounter(); + +var counter2 = makeCounter(); + +alert( counter() ); // 1 +alert( counter() ); // 2 +alert( counter() ); // 3 + +alert( counter2() ); // 1, *!*счётчики независимы*/!* +``` + +Они независимы, потому что при каждом запуске `makeCounter` создаётся свой `LexicalEnvironment`, на который имеет ссылку соответствующий счётчик. + + +## Альтернатива -- свойство функции + +Функция в JavaScript является объектом, поэтому можно присваивать свойства прямо к ней. + +Перепишем пример со счётчиком, используя запись в функцию: + +```js +//+ run +*!* +function makeCounter() { + function counter() { + return counter.currentCount++; + }; + counter.currentCount = 1; + + return counter; +} +*/!* + +var counter = makeCounter(); +alert( counter() ); // 1 +alert( counter() ); // 2 +alert (counter() ); // 3 +``` + +Как видно, с виду пример работает также. Но внутри всё по-другому. + +**Свойство функции, в отличие от переменной из замыкания -- общедоступно. К нему имеет доступ любой, у кого есть объект функции.** + +Например, можно взять и поменять счётчик из внешнего кода: + +```js +var counter = makeCounter(); +alert( counter() ); // 1 + +*!* +counter.currentCount = 5; +*/!* + +alert( counter() ); // 5 +``` + +[smart header="Статические переменные"] +Иногда свойства, привязанные к функции, называют "статическими переменными". + +В некоторых языках программирования можно объявлять переменную, которая сохраняет значение между вызовами функции. В JavaScript ближайший аналог -- это свойство функции. +[/smart] + + +## Итого: замыкания + +[Замыкание](http://en.wikipedia.org/wiki/Closure_(computer_science)) -- это функция вместе со всеми внешними переменными, которые ей доступны. + +Таково стандартное определение, которое есть в Wikipedia и большинстве серьёзных источников по программированию. То есть, замыкание -- это функция + внешние переменные. + +Тем не менее, в JavaScript есть небольшая терминологическая особенность. + +**Обычно, говоря "замыкание функции", подразумевают не саму эту функцию, а именно внешние переменные.** + +**Иногда говорят "переменная берётся из замыкания". Это означает -- из внешнего объекта переменных.** + + +[smart header="Что это такое -- \"понимать замыкания?\""] +Иногда говорят "Вася крут, Вася понимает замыкания!". Что это такое -- "понимать замыкания", какой смысл обычно вкладывают в эти слова? + +"Понимать замыкания" в JavaScript означает понимать следующие вещи: +
      +
    1. Все переменные и параметры функций являются свойствами объекта переменных `LexicalEnvironment`. Каждый запуск функции создает новый такой объект. На верхнем уровне роль `LexicalEnvironment` играет "глобальный объект", в браузере это `window`.
    2. +
    3. При создании функция получает системное свойство `[[Scope]]`, которое ссылается на `LexicalEnvironment`, в котором она была создана (кроме `new Function`).
    4. +
    5. Свойство `[[Scope]]` создаётся вместе с функцией и далее не меняется. Когда бы ни была вызвана функция, куда бы её ни передали в коде -- она будет искать переменные сначала у себя, а затем во внешних `LexicalEnvironment` с места своего создания.
    6. +
    + +В следующих главах мы углубим и расширим это понимание дополнительными примерами, а также рассмотрим, что происходит с памятью. +[/smart] diff --git a/1-js/5-functions-closures/3-scope-new-function/article.md b/1-js/5-functions-closures/3-scope-new-function/article.md new file mode 100644 index 00000000..95e2de45 --- /dev/null +++ b/1-js/5-functions-closures/3-scope-new-function/article.md @@ -0,0 +1,90 @@ +# [[Scope]] для new Function + + +## Присвоение [[Scope]] для new Function [#scope-Function] + +Есть одно исключение из общего правила присвоения `[[Scope]]`, которое мы рассматривали в предыдущей главе. + +**При создании функции с использованием `new Function`, её свойство `[[Scope]]` ссылается не на текущий `LexicalEnvironment`, а на `window`.** + +## Пример + +Следующий пример демонстрирует как функция, созданная `new Function`, игнорирует внешнюю переменную `a` и выводит глобальную вместо нее. + +Сначала обычное поведение: + +```js +//+ run untrusted refresh +var a = 1; +function getFunc() { + var a = 2; + +*!* + var func = function() { alert(a); }; +*/!* + + return func; +} + +getFunc()(); // *!*2*/!*, из LexicalEnvironment функции getFunc +``` + +А теперь -- для функции, созданной через `new Function`: + +```js +//+ run untrusted refresh +var a = 1; +function getFunc() { + var a = 2; + +*!* + var func = new Function('', 'alert(a)'); +*/!* + + return func; +} + +getFunc()(); // *!*1*/!*, из window +``` + +## Почему так сделано? + +[warn header="Продвинутые знания"] +Содержимое этой секции содержит информацию теоретического характера, которая прямо сейчас не обязательна для дальнейшего изучения JavaScript. +[/warn] + +**Эта особенность `new Function`, хоть и выглядит странно, на самом деле весьма полезна.** + +Представьте себе, что нам действительно нужно создать функцию из строки кода. Наверняка код этой функции неизвестен на момент написания скрипта (иначе зачем `new Function`), но станет известен позже, например получен с сервера или из других источников данных. + +При выполнении кода на боевом сервере он наверняка сжат минификатором -- специальной программой, которая уменьшает размер кода, убирая из него лишние комментарии, пробелы, что очень важно -- переименовывает локальные переменные на более короткие. + +То есть, обычно если внутри функции есть `var userName`, то минификатор заменит её на `var u` (или другую букву, чтобы не было конфликта), предполагая, что так как переменная видна только внутри функции, то этого всё равно никто не заметит, а код станет короче. И обычно проблем нет. + +...Но если бы `new Function` могла обращаться к внешним переменным, то при попытке доступа к `userName` в сжатом коде была бы ошибка, так как минификатор переименовал её. + +**Получается, что даже если бы мы захотели использовать локальные переменные в `new Function`, то после сжатия были бы проблемы, так как минификатор переименовывает локальные переменные.** + +Описанная особенность `new Function` просто-таки спасает нас от ошибок. + +Если внутри функции, создаваемой через `new Function`, всё же нужно использовать локальные переменные -- нужно всего лишь предусмотреть соответствующие параметры и передавать их явным образом, например так: + +```js +//+ run untrusted refresh +*!* +var sum = new Function('a, b', ' return a + b; '); +*/!* + +var a = 1, b = 2; + +*!* +alert( sum(a, b) ); // 3 +*/!* +``` + +## Итого + +
      +
    • Функции, создаваемые через `new Function`, имеют значением `[[Scope]]` не внешний объект переменных, а `window`.
    • +
    • Следствие -- такие функции не могут использовать замыкание. Но это хорошо, так как бережёт от ошибок проектирования, да и при сжатии JavaScript проблем не будет. Если же внешние переменные реально нужны -- их можно передать в качестве параметров.
    • +
    diff --git a/1-js/5-functions-closures/4-closures-module/article.md b/1-js/5-functions-closures/4-closures-module/article.md new file mode 100644 index 00000000..a88a3f51 --- /dev/null +++ b/1-js/5-functions-closures/4-closures-module/article.md @@ -0,0 +1,304 @@ +# Модули через замыкания + +Приём программирования "модуль" имеет громадное количество вариаций. + +Его цель -- скрыть внутренние детали реализации скрипта. В том числе: временные переменные, константы, вспомогательные мини-функции и т.п. + +## Зачем нужен модуль? + +Допустим, мы хотим разработать скрипт, который делает что-то полезное. + +В браузере скрипты могут делать много чего -- если бы мы умели работать со страницей, то могли бы сделать так, чтобы все блоки кода красиво расцвечивались, так сделано на этом сайте. + +Но, так как пока мы со страницей работать не умеем (скоро научимся), то пусть скрипт просто выводит сообщение: + +Файл `highlight.js` + +```js +//+ run +// глобальная переменная нашего скрипта +var message = "Привет"; + +// функция для вывода этой переменной +function showMessage() { + alert(message); +} + +// выводим сообщение +showMessage(); +``` + +**У этого скрипта есть свои внутренние переменные и функции.** + +В данном случае это `message` и `showMessage`. + +**Если подключить подобный скрипт к странице "как есть", то возможен конфликт с переменными, которые она использует.** + +То есть, при подключении к такой странице он её "сломает": + +```html + + + + + +``` + +Будет выведено два раза слово "Привет". + +[edit src="highlight-conflict"/] + +Если же убрать скрипт `highlight.js`, то страница будет выводить правильное сообщение. + +**Проблема возникла потому, что переменная `message` из скрипта `highlight.js` перезаписала объявленную на странице.** + +## Приём проектирования "Модуль" + +Чтобы проблемы не было, нам всего-то нужно, чтобы у скрипта была *своя собственная область видимости*, чтобы его переменные не попали на страницу. + +Для этого мы завернём всё его содержимое в функцию, которую тут же запустим. + +Файл `highlight.js`, оформленный как модуль: + +```js +//+ run +(function() { + + // глобальная переменная нашего скрипта + var message = "Привет"; + + // функция для вывода этой переменной + function showMessage() { + alert(message); + } + + // выводим сообщение + showMessage(); + +})(); +``` + +Этот скрипт при подключении к той же странице будет работать корректно. + +Будет выводиться "Привет", а затем "Пожалуйста, нажмите на кнопку". + +[edit src="highlight-module"/] + +### Зачем скобки вокруг функции? + +В примере выше объявление модуля выглядит так: + +```js +//+ run +(function() { + + alert("объявляем локальные переменные, функции, работаем"); + // ... + +}()); +``` + +**В начале и в конце стоят скобки, так как иначе была бы ошибка.** + +Вот неверный вариант: + +```js +//+ run +function() { + // будет ошибка +}(); +``` + +Ошибка при его запуске произойдет потому, что браузер, видя ключевое слово `function` в основном потоке кода, попытается прочитать `Function Declaration`, а здесь имени нет. + +Впрочем, даже если имя поставить, то работать тоже не будет: + +```js +//+ run +function work() { + // ... +}(); // syntax error +``` + +**Дело в том, что "на месте" разрешено вызывать *только* `Function Expression`.** + +Общее правило таково: + +
      +
    • **Если браузер видит `function` в основном потоке кода -- он считает, что это `Function Declaration`.**
    • +
    • **Если же `function` идёт в составе более сложного выражения, то он считает, что это `Function Expression`.**
    • +
    + +Для этого и нужны скобки -- показать, что у нас `Function Expression`, который по правилам JavaScript можно вызвать "на месте". + +Можно показать это другим способом, например поставив перед функцией оператор: + +```js +//+ run ++function() { + alert('Вызов на месте'); +}(); + +!function() { + alert('Так тоже будет работать'); +}(); +``` + +## Библиотека + +Приём "модуль" используется почти во всех современных библиотеках. + +Ведь что такое библиотека? Это полезные функции, ради которых её подключают, плюс временные переменные и вспомогательные функции, которые библиотека использует внутри себя. + +Посмотрим, к примеру, на библиотеку [Lodash](http://lodash.com/), хотя могли бы и [jQuery](http://jquery.com/), там почти то же самое. + +Если её подключить, то появится функция `lodash` (она же `_`), в которую можно обернуть любой объект, так что `lodash(obj)` -- это обёртка, добавляющая к объекту функциональность. + +Кроме того, `lodash` имеет ряд полезных свойств-функций, например [lodash.defaults(object, source)](http://lodash.com/docs#defaults) для удобного добавления в объект `object` значений свойств "по умолчанию", описанных в `source`. + +Выдержка из файла [lodash.js](https://github.com/lodash/lodash/blob/master/dist/lodash.js) для демонстрации того, как организована библиотека: + +```js +//+ run +;(function() { + +*!* + // порядок не важен, но сначала объявим то, что нужно только внутри библиотеки + // version, objectTypes, assignDefaults +*/!* + var version = '2.4.1'; + + var objectTypes = { + 'function': true, + 'object': true + }; + + function assignDefaults(objectValue, sourceValue) { + return typeof objectValue == 'undefined' ? sourceValue : objectValue; + } + +*!* + // а это функция, которая станет lodash.defaults +*/!* + function defaults(object) { + if (!object || arguments.length < 2) { + return object; + } + var args = slice(arguments); + args.push(assignDefaults); + return assign.apply(null, args); + } + +*!* + // lodash - основная функция для библиотеки, единственное, что пойдёт наружу +*/!* + function lodash(value) { + // ... + } + +*!* + // присвоим ей defaults и другие функции, которые нужно вынести из модуля +*/!* + lodash.defaults = defaults; + // lodash... = ... + +*!* + // root - это window в браузере + // в других окружениях, где window нет, root = this +*/!* + var root = (objectTypes[typeof window] && window) || this; + + root.lodash = lodash; // в браузере будет window.lodash = lodash + +}.call(this)); // this = window в браузере, в Node.JS - по-другому +``` + +**Внутри внешней функции:** +
      +
    1. **Происходит что угодно, объявляются свои локальные переменные, функции.**
    2. +
    3. **В `window` выносится то, что нужно снаружи.**
    4. +
    + +Технически, мы могли бы вынести в `window` не только `lodash`, но и вообще все объекты и функции. На практике, обычно модуль -- это один объект, глобальную область во избежание конфликтов хранят максимально чистой. + +[smart header="Зачем точка с запятой в начале?"] +Если получится, что несколько JS-файлы объединены в один (и, скорее всего, сжаты минификатором, но это не важно), то без точки с запятой будет ошибка: + +```js +//+ run +// a.js, в конце забыта точка с запятой! +*!* +var a = 5 +*/!* + +// lib.js, библиотека +(function() { + // ... +})(); +``` + +Ошибка при запуске будет потому, что JavaScript интерпретирует код как `var a = 5(function ...)`, то есть пытается вызвать число `5` как функцию. + +Таковы правила языка, и поэтому рекомендуется явно ставить точку с запятой. В данном случае автор Lodash ставит `;` перед функцией, чтобы предупредить эту ошибку. +[/smart] + +Использование: + +```html + +

    Подключим библиотеку

    + + +

    Используем _.defaults().

    + +``` + +## Экспортирование через return + +Можно оформить модуль и чуть по-другому, например передать значение через `return`: + +```js +var lodash = (function() { + + var version; + function assignDefaults() { ... } + + return { + defaults: function() { } + } + +})(); +``` + +Здесь, кстати, скобки вокруг внешней `function() { ... }` не обязательны, ведь функция и так объявлена внутри выражения присваивания, а значит -- является Function Expression. + +Тем не менее, лучше их ставить, для улучшения читаемости кода, чтобы было сразу видно, что это не простое присвоение функции. + +## Итого + +Модуль при помощи замыканий -- это оборачивание пакета функционала в единую внешнюю функцию, которая тут же выполняется. + +**Все функции модуля будут иметь доступ к другим переменным и внутренним функциям этого же модуля через замыкание.** + +Например, `defaults` из примера выше имеет доступ к `assignDefaults`. + +**Но снаружи программист, использующий модуль, может обращаться напрямую только к тем, которые экспортированы.** + +Благодаря этому будут скрыты внутренние аспекты реализации, которые нужны только разработчику модуля. + +Можно придумать и много других вариаций такого подхода. В конце концов, "модуль" -- это всего лишь функция-обёртка для скрытия переменных. + + diff --git a/1-js/5-functions-closures/4-closures-module/highlight-conflict.view/highlight.js b/1-js/5-functions-closures/4-closures-module/highlight-conflict.view/highlight.js new file mode 100755 index 00000000..09c9f943 --- /dev/null +++ b/1-js/5-functions-closures/4-closures-module/highlight-conflict.view/highlight.js @@ -0,0 +1,10 @@ +// глобальная переменная нашего скрипта +var message = "Привет"; + +// функция для вывода этой переменной +function showMessage() { + alert(message); +} + +// выводим сообщение +showMessage(); diff --git a/1-js/5-functions-closures/4-closures-module/highlight-conflict.view/index.html b/1-js/5-functions-closures/4-closures-module/highlight-conflict.view/index.html new file mode 100755 index 00000000..42649598 --- /dev/null +++ b/1-js/5-functions-closures/4-closures-module/highlight-conflict.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-module/highlight-module.view/highlight.js b/1-js/5-functions-closures/4-closures-module/highlight-module.view/highlight.js new file mode 100755 index 00000000..c517f401 --- /dev/null +++ b/1-js/5-functions-closures/4-closures-module/highlight-module.view/highlight.js @@ -0,0 +1,14 @@ +(function() { + + // глобальная переменная нашего скрипта + var message = "Привет"; + + // функция для вывода этой переменной + function showMessage() { + alert(message); + } + + // выводим сообщение + showMessage(); + +})(); \ No newline at end of file diff --git a/1-js/5-functions-closures/4-closures-module/highlight-module.view/index.html b/1-js/5-functions-closures/4-closures-module/highlight-module.view/index.html new file mode 100755 index 00000000..42649598 --- /dev/null +++ b/1-js/5-functions-closures/4-closures-module/highlight-module.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-usage/1-closure-sum/solution.md b/1-js/5-functions-closures/5-closures-usage/1-closure-sum/solution.md new file mode 100644 index 00000000..48af7992 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/1-closure-sum/solution.md @@ -0,0 +1,18 @@ +Чтобы вторые скобки в вызове работали - первые должны возвращать функцию. + +Эта функция должна знать про `a` и уметь прибавлять `a` к `b`. Вот так: + +```js +//+ run +function sum(a) { + + return function(b) { + return a + b; // возьмет a из внешнего LexicalEnvironment + }; + +} + +alert( sum(1)(2) ); +alert( sum(5)(-1) ); +``` + diff --git a/1-js/5-functions-closures/5-closures-usage/1-closure-sum/task.md b/1-js/5-functions-closures/5-closures-usage/1-closure-sum/task.md new file mode 100644 index 00000000..5de93f5e --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/1-closure-sum/task.md @@ -0,0 +1,13 @@ +# Сумма через замыкание + +[importance 4] + +Напишите функцию `sum`, которая работает так: `sum(a)(b) = a+b`. + +Да, именно так, через двойные скобки (это не опечатка). Например: + +```js +sum(1)(2) = 3 +sum(5)(-1) = 4 +``` + diff --git a/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/_js.view/solution.js b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/_js.view/solution.js new file mode 100644 index 00000000..e1f3870b --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/_js.view/solution.js @@ -0,0 +1,10 @@ +function makeBuffer() { + var text = ''; + + return function(piece) { + if (arguments.length == 0) { // вызов без аргументов + return text; + } + text += piece; + }; +}; \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/_js.view/test.js b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/_js.view/test.js new file mode 100644 index 00000000..45aee8dc --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/_js.view/test.js @@ -0,0 +1,22 @@ +var buffer; +beforeEach(function() { + buffer = makeBuffer(); +}); + +it("возвращает пустую строку по умолчанию", function() { + assert.strictEqual( buffer(), ""); +}); + +it("добавляет аргументы в буффер", function() { + buffer('Замыкания'); + buffer(' Использовать'); + buffer(' Нужно!'); + assert.equal( buffer(), 'Замыкания Использовать Нужно!'); +}); + +it("приводит всё к строке", function() { + buffer(null); + buffer(false); + assert.equal( buffer(), "nullfalse"); +}); + diff --git a/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/solution.md b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/solution.md new file mode 100644 index 00000000..af2beea3 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/solution.md @@ -0,0 +1,30 @@ +Текущее значение текста удобно хранить в замыкании, в локальной переменной `makeBuffer`: + +```js +//+ run +function makeBuffer() { + var text = ''; + + return function(piece) { + if (arguments.length == 0) { // вызов без аргументов + return text; + } + text += piece; + }; +}; + +var buffer = makeBuffer(); + +// добавить значения к буферу +buffer('Замыкания'); +buffer(' Использовать'); +buffer(' Нужно!'); +alert( buffer() ); // 'Замыкания Использовать Нужно!' + +var buffer2 = makeBuffer(); +buffer2(0); buffer2(1); buffer2(0); + +alert( buffer2() ); // '010' +``` + +Начальное значение `text = ''` -- пустая строка. Поэтому операция `text += piece` прибавляет `piece` к строке, автоматически преобразуя его к строковому типу, как и требовалось в условии. diff --git a/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/task.md b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/task.md new file mode 100644 index 00000000..e892ca4a --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/2-stringbuffer/task.md @@ -0,0 +1,43 @@ +# Функция - строковый буфер + +[importance 5] + +В некоторых языках программирования существует объект "строковый буфер", который аккумулирует внутри себя значения. Его функционал состоит из двух возможностей: +
      +
    1. Добавить значение в буфер.
    2. +
    3. Получить текущее содержимое.
    4. +
    + +**Задача -- реализовать строковый буфер на функциях в JavaScript, со следующим синтаксисом:** + +
      +
    • Создание объекта: `var buffer = makeBuffer();`.
    • +
    • Вызов `makeBuffer` должен возвращать такую функцию `buffer`, которая при вызове `buffer(value)` добавляет значение в некоторое внутреннее хранилище, а при вызове без аргументов `buffer()` -- возвращает его.
    • +
    + +Вот пример работы: + +```js +function makeBuffer() { /* ваш код */ } + +var buffer = makeBuffer(); + +// добавить значения к буферу +buffer('Замыкания'); +buffer(' Использовать'); +buffer(' Нужно!'); + +// получить текущее значение +alert( buffer() ); // Замыкания Использовать Нужно! +``` + +Буфер должен преобразовывать все данные к строковому типу: + +```js +var buffer = makeBuffer(); +buffer(0); buffer(1); buffer(0); + +alert( buffer() ); // '010' +``` + +Решение не должно использовать глобальные переменные. diff --git a/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/_js.view/solution.js b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/_js.view/solution.js new file mode 100644 index 00000000..6b044530 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/_js.view/solution.js @@ -0,0 +1,16 @@ +function makeBuffer() { + var text = ''; + + function buffer(piece) { + if (arguments.length == 0) { // вызов без аргументов + return text; + } + text += piece; + }; + + buffer.clear = function() { + text = ""; + } + + return buffer; +}; \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/_js.view/test.js b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/_js.view/test.js new file mode 100644 index 00000000..e9c1b8fc --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/_js.view/test.js @@ -0,0 +1,30 @@ +var buffer; + +beforeEach(function() { + buffer = makeBuffer(); +}); + +it("возвращает пустую строку по умолчанию", function() { + assert.strictEqual( buffer(), ""); +}); + +it("добавляет аргументы в буффер", function() { + buffer('Замыкания'); + buffer(' Использовать'); + buffer(' Нужно!'); + assert.equal( buffer(), 'Замыкания Использовать Нужно!'); +}); + +it("приводит всё к строке", function() { + buffer(null); + buffer(false); + assert.equal( buffer(), "nullfalse"); +}); + +it("очищает буфер вызовом clear", function() { + buffer("test"); + buffer.clear(); + buffer("первый"); + buffer("второй"); + assert.equal( buffer(), "первыйвторой"); +}); diff --git a/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/solution.md b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/solution.md new file mode 100644 index 00000000..ae3b8a62 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/solution.md @@ -0,0 +1,34 @@ + + +```js +//+ run +function makeBuffer() { + var text = ''; + + function buffer(piece) { + if (arguments.length == 0) { // вызов без аргументов + return text; + } + text += piece; + }; + + buffer.clear = function() { + text = ""; + } + + return buffer; +}; + +var buffer = makeBuffer(); + +buffer("Тест"); +buffer(" тебя не съест "); +alert( buffer() ); // Тест тебя не съест + +*!* +buffer.clear(); +*/!* + +alert( buffer() ); // "" +``` + diff --git a/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/task.md b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/task.md new file mode 100644 index 00000000..319af876 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/3-stringbuffer-with-clear/task.md @@ -0,0 +1,24 @@ +# Строковый буфер с очисткой + +[importance 5] + +Добавьте буферу из решения задачи [](/task/stringbuffer) метод `buffer.clear()`, который будет очищать текущее содержимое буфера: + +```js +function makeBuffer() { + ...ваш код... +} + +var buffer = makeBuffer(); + +buffer("Тест"); +buffer(" тебя не съест "); +alert( buffer() ); // Тест тебя не съест + +*!* +buffer.clear(); +*/!* + +alert( buffer() ); // "" +``` + diff --git a/1-js/5-functions-closures/5-closures-usage/4-sort-by-field/solution.md b/1-js/5-functions-closures/5-closures-usage/4-sort-by-field/solution.md new file mode 100644 index 00000000..8ba3409d --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/4-sort-by-field/solution.md @@ -0,0 +1,25 @@ + + +```js +//+ run +var users = [ + { name: "Вася", surname: 'Иванов', age: 20 }, + { name: "Петя", surname: 'Чапаев', age: 25 }, + { name: "Маша", surname: 'Медведева', age: 18 } +]; + +*!* +function byField(field) { + return function(a, b) { + return a[field] > b[field] ? 1: -1; + } +} +*/!* + +users.sort(byField('name')); +users.forEach(function(user) { alert(user.name); }); + +users.sort(byField('age')); +users.forEach(function(user) { alert(user.name); }); +``` + diff --git a/1-js/5-functions-closures/5-closures-usage/4-sort-by-field/task.md b/1-js/5-functions-closures/5-closures-usage/4-sort-by-field/task.md new file mode 100644 index 00000000..154b2358 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/4-sort-by-field/task.md @@ -0,0 +1,41 @@ +# Сортировка + +[importance 5] + +У нас есть массив объектов: + +```js +var users = [ + { name: "Вася", surname: 'Иванов', age: 20 }, + { name: "Петя", surname: 'Чапаев', age: 25 }, + { name: "Маша", surname: 'Медведева', age: 18 } +]; +``` + +Обычно сортировка по нужному полю происходит так: + +```js +// по полю name (Вася, Маша, Петя) +users.sort(function(a, b) { + return a.name > b.name ? 1 : -1; +}); + +// по полю age (Маша, Вася, Петя) +users.sort(function(a, b) { + return a.age > b.age ? 1 : -1; +}); +``` + +Мы хотели бы упростить синтаксис до одной строки, вот так: + +```js +users.sort(byField('name')); +users.forEach(function(user) { alert(user.name); }); // Вася, Маша, Петя + +users.sort(byField('age')); +users.forEach(function(user) { alert(user.name); }); // Маша, Вася, Петя +``` + +То есть, вместо того, чтобы каждый раз писать в `sort` `function...` -- будем использовать `byField(...)` + +Напишите функцию `byField(field)`, которую можно использовать в `sort` для сравнения объектов по полю `field`, чтобы пример выше заработал. diff --git a/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/solution.js b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/solution.js new file mode 100644 index 00000000..45f3087d --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/solution.js @@ -0,0 +1,24 @@ +function filter(arr, func) { + var result = []; + + for(var i=0; i=a && x <= b; + }; +} diff --git a/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/source.js b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/source.js new file mode 100644 index 00000000..677f2997 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/source.js @@ -0,0 +1,12 @@ +function filter(arr, fuc) { + // ...ваш код... +} + +function inBetween(a, b) { + // ...ваш код... +} + +function inArray(arr) { + // ...ваш код... +} + diff --git a/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/test.js b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/test.js new file mode 100644 index 00000000..1cefcabd --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/_js.view/test.js @@ -0,0 +1,54 @@ +var arr; + +before(function() { + arr = [1, 2, 3, 4, 5, 6, 7]; +}); + +describe("inArray", function() { + var checkInArr; + + before(function() { + checkInArr = inArray(arr); + }); + + it("возвращает фильтр для значений в массиве", function() { + assert.isTrue( checkInArr(5) ); + assert.isFalse( checkInArr(0) ); + }); +}); + + +describe("inBetween", function() { + var checkBetween36; + + before(function() { + checkBetween36 = inBetween(3, 6); + }); + + it("возвращает фильтрa для значений в промежутке", function() { + assert.isTrue( checkBetween36(5) ); + assert.isFalse( checkBetween36(0) ); + }); +}); + + +describe("filter", function() { + + it("фильтрует через func", function() { + assert.deepEqual( filter(arr, function(a) { return a % 2 == 0; }), [2,4,6] ); + }); + + it("не модифицирует исходный массив", function() { + filter(arr, function(a) { return a % 2 == 0; }); + assert.deepEqual( arr, [1, 2, 3, 4, 5, 6, 7] ); + }); + + it("поддерживает фильтр inBetween", function() { + assert.deepEqual( filter(arr,inBetween(3,6)), [3,4,5,6]); + }); + + it("поддерживает фильтр inArray", function() { + assert.deepEqual( filter(arr, inArray([1,2,3])), [1,2,3]); + }); + +}); diff --git a/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/solution.md b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/solution.md new file mode 100644 index 00000000..2974267c --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/5-filter-through-function/solution.md @@ -0,0 +1,80 @@ +# Функция фильтрации + +```js +//+ run +function filter(arr, func) { + var result = []; + + for(var i=0; i=a && x <= b; + }; +} +*/!* + +var arr = [1, 2, 3, 4, 5, 6, 7]; +alert( filter(arr, inBetween(3,6)) ); // 3,4,5,6 +``` + +# Фильтр inArray + +```js +//+ run +function filter(arr, func) { + var result = []; + + for(var i=0; i +
  • Создайте функцию `filter(arr, func)`, которая получает массив `arr` и возвращает новый, в который входят только те элементы `arr`, для которых `func` возвращает `true`.
  • +
  • Создайте набор "готовых фильтров": `inBetween(a,b)` -- "между a,b", `inArray([...])` -- "в массиве `[...]`". +Использование должно быть таким: +
      +
    • `filter(arr, inBetween(3,6))` -- выберет только числа от 3 до 6,
    • +
    • `filter(arr, inArray([1,2,3]))` -- выберет только элементы, совпадающие с одним из значений массива.
    • +
    +
  • + +Пример, как это должно работать: + +```js +/* .. ваш код для filter, inBetween, inArray */ +var arr = [1, 2, 3, 4, 5, 6, 7]; + +alert( filter(arr, function(a) { return a % 2 == 0 }) ); // 2,4,6 + +alert( filter(arr, inBetween(3,6)) ); // 3,4,5,6 + +alert( filter(arr, inArray([1,2,10])) ); // 1,2 +``` + diff --git a/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/solution.js b/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/solution.js new file mode 100644 index 00000000..b19f094d --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/solution.js @@ -0,0 +1,19 @@ +function makeArmy() { + + var shooters = []; + + for(var i=0; i<10; i++) { + + var shooter = (function(x) { + + return function() { + alert( x ); + }; + + })(i); + + shooters.push(shooter); + } + + return shooters; +} diff --git a/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/source.js b/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/source.js new file mode 100644 index 00000000..a8678c17 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/source.js @@ -0,0 +1,13 @@ +function makeArmy() { + + var shooters = []; + + for(var i=0; i<10; i++) { + var shooter = function() { // функция-стрелок + alert(i); // выводит свой номер + }; + shooters.push(shooter); + } + + return shooters; +} diff --git a/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/test.js b/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/test.js new file mode 100644 index 00000000..fdfabf66 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/6-make-army/_js.view/test.js @@ -0,0 +1,20 @@ +var army; +before(function() { + army = makeArmy(); + window.alert = sinon.stub(window, "alert"); +}); + +it("army[0] выводит 0", function() { + army[0](); + assert(alert.calledWith(0)); +}); + + +it("army[5] функция выводит 5", function() { + army[5](); + assert(alert.calledWith(5)); +}); + +after(function() { + window.alert.restore(); +}); \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-usage/6-make-army/solution.md b/1-js/5-functions-closures/5-closures-usage/6-make-army/solution.md new file mode 100644 index 00000000..edef84d0 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/6-make-army/solution.md @@ -0,0 +1,217 @@ +# Что происходит в этом коде + +Функция `makeArmy` делает следующее: +
      +
    1. Создаёт пустой массив `shooter`: + +```js +var shooters = []; +``` + +
    2. +
    3. В цикле заполняет массив элементами через `shooter.push`. +При этом каждый элемент массива -- это функция, так что в итоге после цикла массив будет таким: + +```js +shooters = [ + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); }, + function () { alert(i); } +]; +``` + +Этот массив возвращается из функции. +
    4. +
    5. Вызов `army[5]()` -- это получение элемента массива (им будет функция), и тут же -- её запуск.
    6. +
    + +# Почему ошибка + +Вначале разберемся, почему все стрелки выводят одно и то же значение. + +В функциях-стрелках `shooter` отсутствует переменная `i`. Когда такая функция вызывается, то `i` она берет из внешнего `LexicalEnvironment`. + +Чему же будет равно это значение `i`? + +К моменту вызова `army[0]()`, функция `makeArmy` уже закончила работу. Цикл завершился, последнее значение было `i=10`. + +В результате все функции `shooter` получают из внешнего лексического кружения это, одно и то же, последнее, значение `i=10`. + +Попробуйте исправить проблему самостоятельно. + +# Исправление (3 варианта) + +Есть несколько способов исправить ситуацию. + +
      +
    1. **Первый способ исправить код - это привязать значение непосредственно к функции-стрелку:** + +```js +//+ run +function makeArmy() { + + var shooters = []; + + for(var i=0; i<10; i++) { + +*!* + var shooter = function me() { + alert( me.i ); + }; + shooter.i = i; +*/!* + + shooters.push(shooter); + } + + return shooters; +} + +var army = makeArmy(); + +army[0](); // 0 +army[1](); // 1 +``` + +В этом случае каждая функция хранит в себе свой собственный номер. + +Кстати, обратите внимание на использование Named Function Expression, вот в этом участке: + +```js +... +var shooter = function me() { + alert( me.i ); +}; +... +``` + +Если убрать имя `me` и оставить обращение через `shooter`, то работать не будет: + +```js +for(var i=0; i<10; i++) { + var shooter = function() { +*!* + alert(shooter.i); // вывести свой номер (не работает!) + // потому что откуда функция возьмёт переменную shooter? + // ..правильно, из внешнего объекта, а там она одна на всех +*/!* + }; + shooter.i = i; + shooters.push(shooter); +} +``` + +Вызов `alert(shooter.i)` при вызове будет искать переменную `shooter`, а эта переменная меняет значение по ходу цикла, и к моменту вызову она равна последней функции, созданной в цикле. + +Если использовать Named Function Expression, то имя жёстко привязывается к конкретной функции, и поэтому в коде выше `me.i` возвращает правильный `i`. + +
    2. +
    3. **Другое, более продвинутое решение --- использовать дополнительную функцию для того, чтобы "поймать" текущее значение `i`**: + +```js +//+ run +function makeArmy() { + + var shooters = []; + + for(var i=0; i<10; i++) { + +*!* + var shooter = (function(x) { + + return function() { + alert( x ); + }; + + })(i); +*/!* + + shooters.push(shooter); + } + + return shooters; +} + +var army = makeArmy(); + +army[0](); // 0 +army[1](); // 1 +``` + +Посмотрим выделенный фрагмент более внимательно, чтобы понять, что происходит: + +```js +var shooter = (function(x) { + return function() { + alert( x ); + }; +})(i); +``` + +Функция `shooter` создана как результат вызова промежуточного функционального выражения `function(x)`, которое объявляется -- и тут же выполняется, получая `x = i`. + +Так как `function(x)` тут же завершается, то значение `x` больше не меняется. Оно и будет использовано в возвращаемой функции-стрелке. + +Для красоты можно изменить название переменной `x` на `i`, суть происходящего при этом не изменится: + +```js +var shooter = (function(i) { + return function() { + alert( i ); + }; +})(i); +``` + +**Кстати, обратите внимание -- скобки вокруг `function(i)` не нужны**, можно и так: + +```js +var shooter = function(i) { // *!*без скобок вокруг function(i)*/!* + return function() { + alert( i ); + }; +}(i); +``` + +Скобки добавлены в код для лучшей читаемости, чтобы человек, который просматривает его, не подумал, что `var shooter = function`, а понял что это вызов "на месте", и присваивается его результат. +
    4. +
    5. **Еще один забавный способ - обернуть весь цикл во временную функцию**: + +```js +//+ run +function makeArmy() { + + var shooters = []; + +*!* + for(var i=0; i<10; i++) (function(i) { + + var shooter = function() { + alert( i ); + }; + + shooters.push(shooter); + + })(i); +*/!* + + return shooters; +} + +var army = makeArmy(); + +army[0](); // 0 +army[1](); // 1 +``` + +Вызов `(function(i) { ... })` обернут в скобки, чтобы интерпретатор понял, что это `Function Expression`. + +Плюс этого способа - в большей читаемости. Фактически, мы не меняем создание `shooter`, а просто обертываем итерацию в функцию. +
    6. +
    \ No newline at end of file diff --git a/1-js/5-functions-closures/5-closures-usage/6-make-army/task.md b/1-js/5-functions-closures/5-closures-usage/6-make-army/task.md new file mode 100644 index 00000000..04d0eef1 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/6-make-army/task.md @@ -0,0 +1,31 @@ +# Армия функций + +[importance 5] + +Следующий код создает массив функций-стрелков `shooters`. По замыслу, каждый стрелок должен выводить свой номер: + +```js +//+ run +function makeArmy() { + + var shooters = []; + + for(var i=0; i<10; i++) { + var shooter = function() { // функция-стрелок + alert(i); // выводит свой номер + }; + shooters.push(shooter); + } + + return shooters; +} + +var army = makeArmy(); + +army[0](); // стрелок выводит 10, а должен 0 +army[5](); // стрелок выводит 10... +// .. все стрелки выводят 10 вместо 0,1,2...9 +``` + +Почему все стрелки́ выводят одно и то же? Поправьте код, чтобы стрелки работали как задумано. Предложите несколько вариантов исправления. + diff --git a/1-js/5-functions-closures/5-closures-usage/article.md b/1-js/5-functions-closures/5-closures-usage/article.md new file mode 100644 index 00000000..041edea7 --- /dev/null +++ b/1-js/5-functions-closures/5-closures-usage/article.md @@ -0,0 +1,126 @@ +# Использование замыканий + +Замыкания можно использовать сотнями способов. Иногда люди сами не замечают, что использовали замыкания -- настолько это просто и естественно. + +В этой главе мы рассмотрим дополнительные примеры использования замыканий и задачи на эту тему. + +[cut] + +## Локальные переменные для объекта + +Ранее мы сделали счётчик. + +Напомню, как он выглядел: + +```js +//+ run +function makeCounter() { + var currentCount = 1; + + return function() { + return currentCount++; + }; +} + +var counter = makeCounter(); + +// каждый вызов увеличивает счётчик и возвращает результат +alert( counter() ); // 1 +alert( counter() ); // 2 +alert( counter() ); // 3 +``` + +Счётчик получился вполне рабочий, но вот только возможностей ему не хватает. Хорошо бы, чтобы можно было сбрасывать значение счётчика или начинать отсчёт с другого значения вместо `1` или... Да много чего можно захотеть от простого счётчика и, тем более, в более сложных проектах. + +**Чтобы добавить счётчику возможностей -- перейдём с функции на полноценный объект:** + +```js +//+ run +function makeCounter() { + var currentCount = 1; + + return { // возвратим объект вместо функции + getNext: function() { + return currentCount++; + }, + + set: function(value) { + currentCount = value; + }, + + reset: function() { + currentCount = 0; + } + }; +} + +var counter = makeCounter(); + +alert( counter.getNext() ); // 1 +alert( counter.getNext() ); // 2 + +counter.set(5); +alert( counter.getNext() ); // 5 +``` + +Теперь функция `makeCounter` возвращает не одну функцию, а объект с несколькими методами: + +
      +
    • `getNext()` -- получить следующее значение, то, что раньше делал вызов `counter()`.
    • +
    • `set(value)` -- поставить значение.
    • +
    • `reset()` -- обнулить счётчик.
    • +
    + +Все они получают ссылку `[[Scope]]` на текущий (внешний) объект переменных. Поэтому вызов любого из этих методов будет получать или модифицировать одно и то же внешнее значение `currentCount`. + +## Объект счётчика + функция + +Изначально, счётчик делался функцией во многом ради красивого вызова: `counter()`, который увеличивал значение и возвращал результат. + +К сожалению, при переходе на объект короткий вызов пропал, вместо него теперь `counter.getNext()`. Но он ведь был таким простым и удобным... + +Поэтому давайте вернём его! + +```js +//+ run +function makeCounter() { + var currentCount = 1; + +*!* + // возвращаемся к функции + function counter() { + return currentCount++; + } +*/!* + + // ...и добавляем ей методы! + counter.set = function(value) { + currentCount = value; + }; + + counter.reset = function() { + currentCount = 0; + }; + + return counter; +} + +var counter = makeCounter(); + +*!* +alert( counter() ); // 1 +alert( counter() ); // 2 + +counter.set(5); +alert( counter() ); // 5 +*/!* +``` + +Красиво, не правда ли? Получился полноценный объект, который можно вдобавок ещё и вызывать. + +Этот трюк часто используется при разработке JavaScript-библиотек. Например, популярная библиотека [jQuery](http://jquery.com) предоставляет глобальную переменную с именем [jQuery](http://api.jquery.com/jQuery/) (доступна также под коротким именем `$`), которая с одной стороны является функцией и может вызываться как `jQuery(...)`, а с другой -- у неё есть различные методы, например `jQuery.type(123)` возвращает тип аргумента. + + +## Задачи на понимание замыканий + + diff --git a/1-js/5-functions-closures/6-memory-management/article.md b/1-js/5-functions-closures/6-memory-management/article.md new file mode 100644 index 00000000..095c372a --- /dev/null +++ b/1-js/5-functions-closures/6-memory-management/article.md @@ -0,0 +1,491 @@ +# Управление памятью в JavaScript + +Управление памятью обычно незаметно. Мы создаём примитивы, объекты, функции.. Всё это занимает память. + +Что происходит с объектом, когда он становится "не нужен"? Возможно ли "переполнение" памяти? Для ответа на эти вопросы -- залезем "под капот" интерпретатора. + +[cut] +## Управление памятью в JavaScript + +Главной концепцией управления памятью в JavaScript является принцип *достижимости* (англ. reachability). + +
      +
    1. Определённое множество значений считается достижимым изначально, в частности: +
        +
      • Значения, ссылки на которые содержатся в стеке вызова, то есть -- все локальные переменные и параметры функций, которые в настоящий момент выполняются или находятся в ожидании окончания вложенного вызова.
      • +
      • Все глобальные переменные.
      • +
      + +Эти значения гарантированно хранятся в памяти. Мы будем называть их *корнями*. +
    2. +
    3. **Любое другое значение сохраняется в памяти лишь до тех пор, пока доступно из корня по ссылке или цепочке ссылок.**
    4. +
    + +Для очистки памяти от недостижимых значений в браузерах используется автоматический Сборщик мусора (англ. Garbage collection, GC), встроенный в интерпретатор, который наблюдает за объектами и время от времени удаляет недостижимые. + +Далее мы посмотрим ряд примеров, которые помогут в этом разобраться. + +### Достижимость и наличие ссылок + +Можно сказать просто: "значение остаётся в памяти, пока на него есть ссылка". Но такое упрощение будет не совсем верным. + +
      +
    • **Верно -- в том плане, что если на значение не остаётся ссылок, то память из-под него очищается.** + +Например, была создана ссылка в переменной, и эту переменную тут же перезаписали: + +```js +var user = { name: "Вася" }; +user = null; +``` + +Теперь объект `{ name: "Вася" }` более недоступен. Память будет освобождена. +
    • +
    • **Неверно -- может быть так, что ссылка есть, но при этом значение недостижимо и должно быть удалено из памяти.** + +Такая ситуация возникает с объектами, при наличии ссылок друг на друга: + +```js +var vasya = {}; +var petya = {}; +vasya.friend = petya; +petya.friend = vasya; + +vasya = petya = null; +``` + +Несмотря на то, что на объекты `vasya`, `petya` ссылаются друг на друга через ссылку `friend`, то есть можно сказать, что на каждый из них есть ссылка, последняя строка делает эти объекты в совокупности недостижимыми. + +Поэтому они будут удалены из памяти. + +Чтобы отследить такие сложные случаи, придуман [сборщик мусора](http://ru.wikipedia.org/wiki/%D0%A1%D0%B1%D0%BE%D1%80%D0%BA%D0%B0_%D0%BC%D1%83%D1%81%D0%BE%D1%80%D0%B0), который время от времени перебирает объекты и ищет недоступные, с использованием хитрых алгоритмов и оптимизаций, чтобы это было быстро и незаметно. +
    • +
    + +## Управление памятью в картинках + +Рассмотрим пример объекта "семья": + + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { }; + +family.father = { + name: "Вася" +}; + +family.mother = { + name: "Маша" +}; +``` + + + + +
    +Этот код создаёт объект `family` и два дополнительных объекта, доступных по ссылкам `family.father` и `family.mother`. + +### Недостижимый объект + +Теперь посмотрим, что будет, если удалить ссылку `family.father` при помощи `delete`: + + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { }; + +family.father = { + name: "Вася" +}; + +family.mother = { + name: "Маша" +}; + +*!* +delete family.father; +*/!* +``` + + + +
    + +### Пришёл сборщик мусора + +Сборщик мусора ищет недоступные объекты. Базовый алгоритм поиска -- это идти от корня (`window`) по ссылкам и помечать все объекты, которые встретит. Тогда после окончания обхода непомеченными останутся как раз недостижимые объекты. + +В нашем случае таким объектом будет бывший `family.father`. Он стал недостижимым и будет удалён вместе со своим "поддеревом", которое также более недоступно из программы. + + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { + father: { + name: "Вася" + }, + + mother: { + name: "Маша" + } +}; + +*!* +delete family.father; +*/!* +``` + + + +
    + +### После сборщика + +После того, как сработает сборщик мусора, картина в памяти будет такой: + + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { + father: { + name: "Вася" + }, + + mother: { + name: "Маша" + } +}; + +*!* +delete family.father; +*/!* +``` + + + +
    + + +### Достижимость -- только по входящим ссылкам + +Вернёмся к исходному коду. + +**Пусть внутренние объекты ссылаются друг на друга:** + + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { + father: { + name: "Вася" + }, + + mother: { + name: "Маша" + } +}; + +// добавим перекрёстных ссылок +*!* +family.father.wife = family.mother; +family.mother.husband = family.father; +family.father.we = family; +family.mother.we = family; +*/!* +``` + + + +
    + +Получилась сложная структура, с круговыми ссылками. + +**Если удалить ссылки `family.father` и `family.mother.husband` (см. иллюстрацию ниже), то получится объект, который имеет исходящие ссылки, но не имеет входящих:** + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { + father: { + name: "Вася" + }, + + mother: { + name: "Маша" + } +}; + +family.father.wife = family.mother; +family.mother.husband = family.father; +family.father.we = family; +family.mother.we = family; + +*!* +delete family.father; +delete family.mother.husband; +*/!* +``` + + + +
    + +При стандартном алгоритме очистки памяти, сборщик мусора пойдёт от корня и не сможет достичь объект, помеченный серым. Поэтому он будет удалён. + +**И совершенно неважно, что из объекта выходят какие-то ссылки `wife`, `we`, они не влияют на достижимость этого объекта.** + +### Недостижимый остров + +Всё "семейство" объектов, которое мы рассматривали выше, достижимо исключительно через глобальную переменную `family` или, иными словами, через свойство `window.family`. + +Если записать в `window.family` что-то ещё, то все они, вместе со своими внутренними ссылками станут "недостижимым островом" и будут удалены: + + + + + + + + + +
    КодСтруктура в памяти
    + +```js +var family = { + father: { + name: "Вася" + }, + + mother: { + name: "Маша" + } +}; + +family.father.wife = family.mother; +family.mother.husband = family.father; +family.father.we = family; +family.mother.we = family; + +*!* +family = null; +*/!* +``` + + + +
    + +## Замыкания + +Замыкания следуют тем же правилам, что и обычные объекты. + +**Объект переменных внешней функции существует в памяти до тех пор, пока существует хоть одна внутренняя функция, ссылающаяся на него через свойство `[[Scope]]`.** + +Например: + +
      +
    • Обычно объект переменных удаляется по завершении работы функции. Даже если в нём есть объявление внутренней функции: + +```js +function f() { + var value = Math.random(); + + function g() { } // g видна только изнутри +} + +f(); +``` + +В коде выше внутренняя функция объявлена, но она осталась внутри. После окончания работы `f()` она станет недоступной для вызовов, так что будет убрана из памяти вместе с остальными локальными переменными. +
    • +
    • ...А вот в этом случае лексическое окружение, включая переменную `value`, будет сохранено: + +```js +function f() { + var value = Math.random(); + + function g() { } + +*!* + return g; +*/!* +} + +var g = f(); // функция g будет жить и сохранит ссылку на объект переменных +``` + +Причина сохранения проста: в скрытом свойстве `g.[[Scope]]` находится ссылка на объект переменных, в котором была создана `g`. +
    • +
    • +Если `f()` будет вызываться много раз, а полученные функции будут сохраняться, например, складываться в массив, то будут сохраняться и объекты `LexicalEnvironment` с соответствующими значениями `value`: + +```js +function f() { + var value = Math.random(); + + return function() { }; +} + +// 3 функции, каждая ссылается на свой объект переменных, +// со своим значением value +var arr = [f(), f(), f()]; +``` + +При этом совершенно не важно, имеет ли вложенная функция имя или нет. +
    • +
    • Объект `LexicalEnvironment` живёт ровно до тех пор, пока на него существуют ссылки. В коде ниже замыкание сначала сохраняется в памяти, а после удаления ссылки на `g` умирает: + +```js +function f() { + var value = Math.random(); + + function g() { } + + return g; +} + +var g = f(); // функция g жива +// а значит в памяти остается соответствующий объект переменных + +g = null; // ..а вот теперь память будет очищена +``` + +
    • +
    + +### Оптимизация в V8 и её последствия + +Современные JS-движки делают оптимизации замыканий по памяти. Они анализируют использование переменных и в случае, когда переменная из замыкания абсолютно точно не используется, удаляют её. + +В коде выше переменная `value` никак не используется. Поэтому она будет удалена из памяти. + +**Важный побочный эффект в V8 (Chrome, Opera) состоит в том, что удалённая переменная станет недоступна и при отладке!** + +Попробуйте запустить пример ниже с открытой консолью Chrome. Когда он остановится, в консоли наберите `alert(value)`. + +```js +//+ run +function f() { + var value = Math.random(); + + function g() { + debugger; // выполните в консоли alert(value); Нет такой переменной! + } + + return g; +} + +var g = f(); +g(); +``` + +Это может привести к забавным казусам при отладке, вплоть до того что вместо этой переменной будет другая, внешняя: + +```js +//+ run +var value = "Сюрприз"; + +function f() { + var value = "..."; + + function g() { + debugger; // выполните в консоли alert(value); Сюрприз! + } + + return g; +} + +var g = f(); +g(); +``` + +[warn header="Ещё увидимся"] +Об этой особенности важно знать. Если вы отлаживаете под Chrome/Opera, то наверняка рано или поздно с ней встретитесь! + +Это не глюк отладчика, а особенность работы V8, которая, возможно, будет когда-нибудь изменена. Вы всегда сможете проверить, не изменилось ли чего, запустив примеры на этой странице. +[/warn] + +## Влияние управления памятью на скорость + +На создание новых объектов и их удаление тратится время. Это важно иметь в виду в случае, когда важна производительность. + +В качестве примера рассмотрим рекурсию. При вложенных вызовах каждый раз создаётся новый объект с переменными и помещается в стек. Потом память из-под него нужно очистить. Поэтому рекурсивный код будет всегда медленнее использующего цикл, но насколько? + +Пример ниже тестирует сложение чисел до данного через рекурсию по сравнению с обычным циклом: + +```js +//+ run +function sumTo(n) { // обычный цикл 1+2+...+n + var result = 0; + for (var i=1; i<=n; i++) { + result += i; + } + return result; +} + +function sumToRec(n) { // рекурсия sumToRec(n) = n+SumToRec(n-1) + return n == 1 ? 1 : n + sumToRec(n-1); +} + +var timeLoop = performance.now(); +for (var i=1;i<1000;i++) sumTo(1000); // цикл +timeLoop = performance.now() - timeLoop; + +var timeRecursion = performance.now(); +for (var i=1;i<1000;i++) sumToRec(1000); // рекурсия +timeRecursion = performance.now() - timeRecursion; + +alert("Разница в " + ( timeRecursion / timeLoop ) + " раз"); +``` + +Различие в скорости на таком примере может составлять, в зависимости от интерпретатора, 2-10 раз. + +В большинстве ситуаций оптимизация по количеству создаваемых объектов несущественна, просто потому что "JavaScript и так достаточно быстр". Но она может быть важной для "узких мест" кода, а также при написании компьютерной графики и сложных вычислений на JS. diff --git a/1-js/5-functions-closures/6-memory-management/family-ext-nofatherlink-nohusband.png b/1-js/5-functions-closures/6-memory-management/family-ext-nofatherlink-nohusband.png new file mode 100755 index 00000000..51715238 Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family-ext-nofatherlink-nohusband.png differ diff --git a/1-js/5-functions-closures/6-memory-management/family-ext-nolink.png b/1-js/5-functions-closures/6-memory-management/family-ext-nolink.png new file mode 100755 index 00000000..dd9d5760 Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family-ext-nolink.png differ diff --git a/1-js/5-functions-closures/6-memory-management/family-ext.png b/1-js/5-functions-closures/6-memory-management/family-ext.png new file mode 100755 index 00000000..c1108d59 Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family-ext.png differ diff --git a/1-js/5-functions-closures/6-memory-management/family-nofatherlink-junk-cleanup.png b/1-js/5-functions-closures/6-memory-management/family-nofatherlink-junk-cleanup.png new file mode 100755 index 00000000..ea1c8300 Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family-nofatherlink-junk-cleanup.png differ diff --git a/1-js/5-functions-closures/6-memory-management/family-nofatherlink-junk.png b/1-js/5-functions-closures/6-memory-management/family-nofatherlink-junk.png new file mode 100755 index 00000000..73f971b0 Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family-nofatherlink-junk.png differ diff --git a/1-js/5-functions-closures/6-memory-management/family-nofatherlink.png b/1-js/5-functions-closures/6-memory-management/family-nofatherlink.png new file mode 100755 index 00000000..f9073786 Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family-nofatherlink.png differ diff --git a/1-js/5-functions-closures/6-memory-management/family.png b/1-js/5-functions-closures/6-memory-management/family.png new file mode 100755 index 00000000..93e7247d Binary files /dev/null and b/1-js/5-functions-closures/6-memory-management/family.png differ diff --git a/1-js/5-functions-closures/7-with/1-with-function/solution.md b/1-js/5-functions-closures/7-with/1-with-function/solution.md new file mode 100644 index 00000000..fd30089e --- /dev/null +++ b/1-js/5-functions-closures/7-with/1-with-function/solution.md @@ -0,0 +1,17 @@ +Вторая (`2`), т.к. при обращении к любой переменной внутри `with` -- она ищется прежде всего в объекте. + +Соответственно, будет выведено `2`: + +```js +//+ run +function f() { alert(1) } + +var obj = { + f: function() { alert(2) } +}; + +with(obj) { + f(); +} +``` + diff --git a/1-js/5-functions-closures/7-with/1-with-function/task.md b/1-js/5-functions-closures/7-with/1-with-function/task.md new file mode 100644 index 00000000..05e73c4e --- /dev/null +++ b/1-js/5-functions-closures/7-with/1-with-function/task.md @@ -0,0 +1,18 @@ +# With + функция + +[importance 5] + +Какая из функций будет вызвана? + +```js +function f() { alert(1) } + +var obj = { + f: function() { alert(2) } +}; + +with(obj) { + f(); +} +``` + diff --git a/1-js/5-functions-closures/7-with/2-with-variables/solution.md b/1-js/5-functions-closures/7-with/2-with-variables/solution.md new file mode 100644 index 00000000..79d5f180 --- /dev/null +++ b/1-js/5-functions-closures/7-with/2-with-variables/solution.md @@ -0,0 +1,20 @@ +Выведет `3`. + +**Конструкция `with` не создаёт области видимости,** её создают только функции. Поэтому объявление `var b` внутри конструкции работает также, как если бы оно было вне её. + +Код в задаче эквивалентен такому: + +```js +//+ run +var a = 1; +*!* +var b; +*/!* + +var obj = { b: 2 } + +with(obj) { + alert( a + b ); +} +``` + diff --git a/1-js/5-functions-closures/7-with/2-with-variables/task.md b/1-js/5-functions-closures/7-with/2-with-variables/task.md new file mode 100644 index 00000000..c979fc47 --- /dev/null +++ b/1-js/5-functions-closures/7-with/2-with-variables/task.md @@ -0,0 +1,17 @@ +# With + переменные + +[importance 5] + +Что выведет этот код? + +```js +var a = 1; + +var obj = { b: 2 }; + +with(obj) { + var b; + alert( a + b ); +} +``` + diff --git a/1-js/5-functions-closures/7-with/article.md b/1-js/5-functions-closures/7-with/article.md new file mode 100644 index 00000000..966dc0e6 --- /dev/null +++ b/1-js/5-functions-closures/7-with/article.md @@ -0,0 +1,181 @@ +# Устаревшая конструкция "with" + +Конструкция `with` позволяет использовать в качестве области видимости для переменных произвольный объект. + +В современном JavaScript от этой конструкции отказались. С `use strict` она не работает, но её ещё можно найти в старом коде, так что стоит познакомиться с ней, чтобы если что -- понимать, о чём речь. + +[cut] +Синтаксис: + +```js +with(obj) { + ... код ... +} +``` + +**Любое обращение к переменной внутри `with` сначала ищет её среди свойств `obj`, а только потом -- вне `with`.** + +## Пример + +В примере ниже переменная будет взята не из глобальной области, а из `obj`: + +```js +//+ run +var a = 5; + +var obj = { a : 10 }; + +*!* +with(obj) { + alert(a); // 10, из obj +} +*/!* +``` + +Попробуем получить переменную, которой в `obj` нет: + +```js +//+ run +var b = 1; + +var obj = { a : 10 }; + +*!* +with(obj) { + alert(b); // 1, из window +} +*/!* +``` + +Здесь интерпретатор сначала проверяет наличие `obj.b`, не находит и идет вне `with`. + +Особенно забавно выглядит применение вложенных `with`: + +```js +//+ run +var obj = { + weight: 10, + size: { + width: 5, + height: 7 + } +}; + +with(obj) { + with(size) { // size будет взят из obj +*!* + alert( width*height / weight ); // width,height из size, weight из obj +*/!* + } +} +``` + +Свойства из разных объектов используются как обычные переменные... Магия! Порядок поиска переменных в выделенном коде: `size => obj => window` + + + +## Изменения переменной + +При использовании `with`, как и во вложенных функциях -- переменная изменяется в той области, где была найдена. + +Например: + +```js +//+ run +var obj = { a : 10 } + +*!* +with(obj) { + a = 20; +} +*/!* +alert(obj.a); // 20, переменная была изменена в объекте +``` + +## Почему отказались от with? + +Есть несколько причин. + +
      +
    1. В современном стандарте `JavaScript` отказались от `with`, потому что **конструкция `with` подвержена ошибкам и непрозрачна.** + +Проблемы возникают в том случае, когда в `with(obj)` присваивается переменная, которая по замыслу должна быть в свойствах `obj`, но ее там нет. + +Например: + +```js +//+ run +var obj = { weight: 10 }; + +with(obj) { + weight = 20; // (1) + size = 35; // (2) +} + +alert(obj.size); +alert(window.size); +``` + +В строке `(2)` присваивается свойство, отсутствующее в `obj`. В результате интерпретатор, не найдя его, создает новую глобальную переменную `window.size`. + +Такие ошибки редки, но очень сложны в отладке, особенно если `size` изменилась не в `window`, а где-нибудь во внешнем `LexicalEnvironment`. +
    2. +
    3. Еще одна причина -- **алгоритмы сжатия JavaScript не любят `with`**. Перед выкладкой на сервер JavaScript сжимают. Для этого есть много инструментов, например [Closure Compiler](http://code.google.com/intl/ru-RU/closure/compiler/) и [UglifyJS](https://github.com/mishoo/UglifyJS). Если вкратце -- они либо сжимают код с `with` с ошибками, либо оставляют его частично несжатым.
    4. +
    5. Ну и, наконец, **производительность -- усложнение поиска переменной из-за `with` влечет дополнительные накладные расходы**. Современные движки применяют много внутренних оптимизаций, ряд которых не могут быть применены к коду, в котором есть `with`. + +Вот, к примеру, запустите этот код в современном браузере. Производительность функции `fast` существенно отличается `slow` с пустым(!) `with`. И дело тут именно в `with`, т.к. наличие этой конструкции препятствует оптимизации. + +```js +//+ run +var i = 0; + +function fast() { + i++; +} + +function slow() { + with(i) {} + i++; +} + + +var time = new Date(); +while(i < 1000000) fast(); +alert(new Date - time); + +var time = new Date(); +i=0; +while(i < 1000000) slow(); +alert(new Date - time); +``` + +
    6. +
    + +### Замена with + +Вместо `with` рекомендуется использовать временную переменную, например: + +```js +/* вместо +with(elem.style) { + top = '10px'; + left = '20px'; +} +*/ + +var s = elem.style; + +s.top = '10px'; +s.left = '0'; +``` + +Это не так элегантно, но убирает лишний уровень вложенности и абсолютно точно понятно, что будет происходить и куда присвоятся свойства. + +## Итого + +
      +
    • Конструкция `with(obj) { ... }` использует `obj` как дополнительную область видимости. Все переменные, к которым идет обращение внутри блока, сначала ищутся в `obj`.
    • +
    • Конструкция `with` устарела и не рекомендуется по ряду причин. Избегайте её.
    • +
    + diff --git a/1-js/5-functions-closures/7-with/with_obj_size.png b/1-js/5-functions-closures/7-with/with_obj_size.png new file mode 100755 index 00000000..1967895d Binary files /dev/null and b/1-js/5-functions-closures/7-with/with_obj_size.png differ diff --git a/1-js/5-functions-closures/index.md b/1-js/5-functions-closures/index.md new file mode 100644 index 00000000..faff5532 --- /dev/null +++ b/1-js/5-functions-closures/index.md @@ -0,0 +1,5 @@ +# Замыкания, область видимости + +Понимание "области видимости" и "замыканий" -- ключевое в изучении JavaScript, без них "каши не сваришь". + +В этом разделе мы более глубоко изучаем переменные и функции -- и замыкания в том числе. \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/1-call-array-this/solution.md b/1-js/6-objects-more/1-object-methods/1-call-array-this/solution.md new file mode 100644 index 00000000..f3b7cde8 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/1-call-array-this/solution.md @@ -0,0 +1,13 @@ +Вызов `arr[2]()` -- это обращение к методу объекта `obj[method]()`, в роли `obj` выступает `arr`, а в роли метода: `2`. + +Поэтому, как это бывает при вызове функции как метода, функция `arr[2]` получит `this = arr` и выведет массив: + +```js +//+ run +var arr = ["a", "b"]; + +arr.push( function() { alert(this); } ) + +arr[2](); // "a","b",function +``` + diff --git a/1-js/6-objects-more/1-object-methods/1-call-array-this/task.md b/1-js/6-objects-more/1-object-methods/1-call-array-this/task.md new file mode 100644 index 00000000..a46c5456 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/1-call-array-this/task.md @@ -0,0 +1,14 @@ +# Вызов в контексте массива + +[importance 5] + +Каким будет результат? Почему? + +```js +var arr = ["a", "b"]; + +arr.push( function() { alert(this); } ) + +arr[2](); // ? +``` + diff --git a/1-js/6-objects-more/1-object-methods/2-check-syntax/solution.md b/1-js/6-objects-more/1-object-methods/2-check-syntax/solution.md new file mode 100644 index 00000000..b73f4b2d --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/2-check-syntax/solution.md @@ -0,0 +1,24 @@ +**Ошибка**! + +Попробуйте: + +```js +//+ run +var obj = { + go: function() { alert(this) } +} + +(obj.go)() // error! +``` + +Причем сообщение об ошибке в большинстве браузеров не даёт понять, что на самом деле не так. + +**Ошибка возникла из-за того, что после объявления `obj` пропущена точка с запятой.** + +JavaScript игнорирует перевод строки перед скобкой `(obj.go)()` и читает этот код как: + +```js +var obj = { go:... }(obj.go)() +``` + +Интерпретатор попытается вычислить это выражение, которое обозначает вызов объекта `{ go: ... }` как функции с аргументом `(obj.go)`. При этом, естественно, возникнет ошибка. diff --git a/1-js/6-objects-more/1-object-methods/2-check-syntax/task.md b/1-js/6-objects-more/1-object-methods/2-check-syntax/task.md new file mode 100644 index 00000000..f8fe23ec --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/2-check-syntax/task.md @@ -0,0 +1,15 @@ +# Проверка синтаксиса + +[importance 2] + +Каков будет результат этого кода? + +```js +var obj = { + go: function() { alert(this) } +} + +(obj.go)() +``` + +P.S. Есть подвох :) \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/3-why-this/solution.md b/1-js/6-objects-more/1-object-methods/3-why-this/solution.md new file mode 100644 index 00000000..b7e4eea6 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/3-why-this/solution.md @@ -0,0 +1,31 @@ +
      +
    1. Обычный вызов функции в контексте объекта.
    2. +
    3. То же самое, скобки ни на что не влияют.
    4. +
    5. Здесь не просто вызов `obj.method()`, а более сложный вызов вида `(выражение).method()`. Такой вызов работает, как если бы он был разбит на две строки: + +```js +f = obj.go; // сначала вычислить выражение +f(); // потом вызвать то, что получилось +``` + +При этом `f()` выполняется как обычная функция, без передачи `this`. +
    6. +
    7. Здесь также слева от точки находится выражение, вызов аналогичен двум строкам.
    8. +
    + +В спецификации это объясняется при помощи специального внутреннего типа [Reference Type](http://es5.github.com/x8.html#x8.7). + +Если подробнее -- то `obj.go()` состоит из двух операций: +
      +
    1. Сначала получить свойство `obj.go`.
    2. +
    3. Потом вызвать его как функцию.
    4. +
    + +Но откуда на шаге 2 получить `this`? Как раз для этого операция получения свойства `obj.go` возвращает значение особого типа `Reference Type`, который в дополнение к свойству `go` содержит информацию об `obj`. Далее, на втором шаге, вызов его при помощи скобок `()` правильно устанавливает `this`. + +**Любые другие операции, кроме вызова, превращают `Reference Type` в обычный тип, в данном случае -- функцию `go` (так уж этот тип устроен).** + +Поэтому получается, что `(method = obj.go)` присваивает в переменную `method` функцию `go`, уже без всякой информации об объекте `obj`. + +Аналогичная ситуация и в случае `(4)`: оператор ИЛИ `||` делает из `Reference Type` обычную функцию. + diff --git a/1-js/6-objects-more/1-object-methods/3-why-this/task.md b/1-js/6-objects-more/1-object-methods/3-why-this/task.md new file mode 100644 index 00000000..5ff97a04 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/3-why-this/task.md @@ -0,0 +1,26 @@ +# Почему this присваивается именно так? + +[importance 3] + +Вызовы `(1)` и `(2)` в примере ниже работают не так, как `(3)` и `(4)`: + +```js +//+ run +"use strict" + +var obj, f; + +obj = { + go: function() { alert(this); } +}; + +obj.go(); // (1) object + +(obj.go)(); // (2) object + +(method = obj.go)(); // (3) undefined + +(obj.go || obj.stop)(); // (4) undefined +``` + +В чём дело? Объясните логику работы `this`. diff --git a/1-js/6-objects-more/1-object-methods/4-object-property-this/solution.md b/1-js/6-objects-more/1-object-methods/4-object-property-this/solution.md new file mode 100644 index 00000000..e26f24d8 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/4-object-property-this/solution.md @@ -0,0 +1,22 @@ +**Ответ: пустая строка.** + +```js +//+ run +var name = ""; + +var user = { + name: "Василий", + +*!* + export: this // (*) +*/!* +}; + +alert(user.export.name); +``` + +Объявление объекта само по себе не влияет на `this`. Никаких функций, которые могли бы повлиять на контекст, здесь нет. + +Так как код находится вообще вне любых функций, то `this` в нём равен `window` (при `use strict` было бы `undefined`). + +Получается, что в строке `(*)` мы имеем `export: window`, так что далее `alert(user.export.name)` выводит свойство `window.name`, то есть глобальную переменную `name`, которая равна пустой строке. diff --git a/1-js/6-objects-more/1-object-methods/4-object-property-this/task.md b/1-js/6-objects-more/1-object-methods/4-object-property-this/task.md new file mode 100644 index 00000000..95421894 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/4-object-property-this/task.md @@ -0,0 +1,18 @@ +# Значение this в объявлении объекта + +[importance 5] + +Что выведет `alert` в этом коде? Почему? + +```js +var name = ""; + +var user = { + name: "Василий", + + export: this +}; + +alert(user.export.name); +``` + diff --git a/1-js/6-objects-more/1-object-methods/5-return-this/solution.md b/1-js/6-objects-more/1-object-methods/5-return-this/solution.md new file mode 100644 index 00000000..b34977b4 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/5-return-this/solution.md @@ -0,0 +1,5 @@ +**Ответ: `Василий`.** + +Вызов `user.export()` использует `this`, который равен объекту до точки, то есть внутри `user.export()` строка `return this` возвращает объект `user`. + +В итоге выводится свойство `name` объекта `user`, равное `"Василий"`. diff --git a/1-js/6-objects-more/1-object-methods/5-return-this/task.md b/1-js/6-objects-more/1-object-methods/5-return-this/task.md new file mode 100644 index 00000000..e9065794 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/5-return-this/task.md @@ -0,0 +1,21 @@ +# Возврат this + +[importance 5] + +Что выведет `alert` в этом коде? Почему? + +```js +var name = ""; + +var user = { + name: "Василий", + + export: function() { + return this; + } + +}; + +alert(user.export().name); +``` + diff --git a/1-js/6-objects-more/1-object-methods/6-return-object-this/solution.md b/1-js/6-objects-more/1-object-methods/6-return-object-this/solution.md new file mode 100644 index 00000000..67dda01b --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/6-return-object-this/solution.md @@ -0,0 +1,7 @@ +**Ответ: `Василий`.** + +Во время выполнения `user.export()` значение `this = user`. + +При создании объекта `{ value: this }`, в свойство `value` копируется ссылка на текущий контекст, то есть на `user`. + +Получается что `user.export().value == user`. diff --git a/1-js/6-objects-more/1-object-methods/6-return-object-this/task.md b/1-js/6-objects-more/1-object-methods/6-return-object-this/task.md new file mode 100644 index 00000000..fe585cfa --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/6-return-object-this/task.md @@ -0,0 +1,23 @@ +# Возврат объекта с this + +[importance 5] + +Что выведет `alert` в этом коде? Почему? + +```js +var name = ""; + +var user = { + name: "Василий", + + export: function() { + return { + value: this + }; + } + +}; + +alert(user.export().value.name); +``` + diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/solution.js b/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/solution.js new file mode 100644 index 00000000..ca2ca33b --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/solution.js @@ -0,0 +1,15 @@ +var calculator = { + sum: function() { + return this.a + this.b; + }, + + mul: function() { + return this.a * this.b; + }, + + read: function() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + } +} + diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/test.js b/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/test.js new file mode 100644 index 00000000..761f881e --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/7-calculator/_js.view/test.js @@ -0,0 +1,22 @@ +sinon.stub(window, "prompt"); + +prompt.onCall(0).returns("2"); +prompt.onCall(1).returns("3"); + +describe("calculator", function() { + before(function() { + calculator.read(); + }); + + it("при вводе 2 и 3 сумма равна 5", function() { + assert.equal( calculator.sum(), 5 ); + }); + + it("при вводе 2 и 3 произведение равно 6", function() { + assert.equal( calculator.mul(), 6 ); + }); +}); + +after(function() { + prompt.restore(); +}); \ No newline at end of file diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/solution.md b/1-js/6-objects-more/1-object-methods/7-calculator/solution.md new file mode 100644 index 00000000..1849cc94 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/7-calculator/solution.md @@ -0,0 +1,24 @@ + + +```js +//+ run demo +var calculator = { + sum: function() { + return this.a + this.b; + }, + + mul: function() { + return this.a * this.b; + }, + + read: function() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + } +} + +calculator.read(); +alert( calculator.sum() ); +alert( calculator.mul() ); +``` + diff --git a/1-js/6-objects-more/1-object-methods/7-calculator/task.md b/1-js/6-objects-more/1-object-methods/7-calculator/task.md new file mode 100644 index 00000000..d45d1b1e --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/7-calculator/task.md @@ -0,0 +1,22 @@ +# Создайте калькулятор + +[importance 5] + +Создайте объект `calculator` с тремя методами: +
      +
    • `read()` запрашивает `prompt` два значения и сохраняет их как свойства объекта
    • +
    • `sum()` возвращает сумму этих двух значений
    • +
    • `mul()` возвращает произведение этих двух значений
    • +
    + +```js +var calculator = { + ... ваш код... +} + +calculator.read(); +alert( calculator.sum() ); +alert( calculator.mul() ); +``` + +[demo /] diff --git a/1-js/6-objects-more/1-object-methods/8-chain-calls/solution.md b/1-js/6-objects-more/1-object-methods/8-chain-calls/solution.md new file mode 100644 index 00000000..d412c95e --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/8-chain-calls/solution.md @@ -0,0 +1,23 @@ +Решение состоит в том, чтобы каждый раз возвращать текущий объект. Это делается добавлением `return this` в конце каждого метода: + +```js +//+ run +var ladder = { + step: 0, + up: function() { + this.step++; + return this; + }, + down: function() { + this.step--; + return this; + }, + showStep: function() { + alert(this.step); + return this; + } +} + +ladder.up().up().down().up().down().showStep(); // 1 +``` + diff --git a/1-js/6-objects-more/1-object-methods/8-chain-calls/task.md b/1-js/6-objects-more/1-object-methods/8-chain-calls/task.md new file mode 100644 index 00000000..0fa9218a --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/8-chain-calls/task.md @@ -0,0 +1,38 @@ +# Цепочка вызовов + +[importance 2] + +Есть объект "лестница" ladder: + +```js +var ladder = { + step: 0, + up: function() { // вверх по лестнице + this.step++; + }, + down: function() { // вниз по лестнице + this.step--; + }, + showStep: function() { // вывести текущую ступеньку + alert(this.step); + } +}; +``` + +Сейчас, если нужно последовательно вызвать несколько методов объекта, это можно сделать так: + +```js +ladder.up(); +ladder.up(); +ladder.down(); +ladder.showStep(); // 1 +``` + +Модифицируйте код методов объекта, чтобы вызовы можно было делать цепочкой, вот так: + +```js +ladder.up().up().down().up().down().showStep(); // 1 +``` + +Такой подход называется "чейнинг" (chaining) и используется, например, во фреймворке jQuery. + diff --git a/1-js/6-objects-more/1-object-methods/article.md b/1-js/6-objects-more/1-object-methods/article.md new file mode 100644 index 00000000..80403568 --- /dev/null +++ b/1-js/6-objects-more/1-object-methods/article.md @@ -0,0 +1,228 @@ +# Методы объектов, this + +До этого мы говорили об объекте лишь как о хранилище значений. Теперь пойдём дальше и поговорим об объектах как о сущностях со своими функциями ("методами"). +[cut] + +## Методы у объектов + +При объявлении объекта можно указать свойство-функцию, например: + +```js +//+ run +var user = { + name: 'Василий', + +*!* + // метод +*/!* + sayHi: function() { + alert('Привет!'); + } + +}; + +*!* +// Вызов +user.sayHi(); +*/!* +``` + +Свойства-функции называют "методами" объектов. Их можно добавлять и удалять в любой момент, в том числе и явным присваиванием: + +```js +//+ run +var user = { + name: 'Василий' +}; + +*!* +user.sayHi = function() { // присвоили метод после создания объекта + alert('Привет!'); +}; +*/!* + +// Вызов метода: +*!*user.sayHi();*/!* +``` + +## Доступ к объекту через this + +Для полноценной работы метод должен иметь доступ к данным объекта. В частности, вызов `user.sayHi()` может захотеть вывести имя пользователя. + +**Для доступа к текущему объекту из метода используется ключевое слово `this`**. + +Значением `this` является объект перед "точкой", в контексте которого вызван метод, например: + +```js +//+ run +var user = { + name: 'Василий', + + sayHi: function() { + alert( *!*this.name*/!* ); + } +}; + +user.sayHi(); // sayHi в контексте user +``` + +Здесь при выполнении функции `user.sayHi()` в `this` будет храниться ссылка на текущий объект `user`. + +Вместо `this` внутри `sayHi` можно было бы обратиться к объекту, используя переменную `user`: + +```js +... + sayHi: function() { + alert( *!*user.name*/!* ); + } +... +``` + +...Однако, такое решение нестабильно. Если мы решим скопировать объект в другую переменную, например `admin = user`, а в переменную `user` записать что-то другое -- обращение будет совсем не по адресу: + +```js +//+ run +var user = { + name: 'Василий', + + sayHi: function() { + alert( *!*user.name*/!* ); // приведёт к ошибке + } +}; + +var admin = user; +user = null; + +admin.sayHi(); // упс! внутри sayHi обращение по старому имени, ошибка! +``` + +**Использование `this` гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана!** + +Через `this` метод может обратиться к любому свойству объекта, а, при желании, и передать объект куда-либо: + +```js +//+ run +var user = { + name: 'Василий', + +*!* + sayHi: function() { + showName(this); // передать текущий объект в showName + } +*/!* +}; + +function showName(obj) { + alert( obj.name ); +} + +user.sayHi(); // Василий +``` + +## Подробнее про this + +Любая функция может иметь в себе `this`. Совершенно неважно, объявлена она в объекте или вне него. + +Значение `this` называется *контекстом вызова* и будет определено в момент вызова функции. + +Например, такая функция, объявленная без объекта, вполне допустима: + +```js +function sayHi() { + alert( *!*this.firstName*/!* ); +} +``` + +Эта функция ещё не знает, каким будет `this`. Это выяснится при выполнении программы. + +**Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный `this`:** + +```js +//+ run +var user = { firstName: "Вася" }; +var admin = { firstName: "Админ" }; + +function func() { + alert( this.firstName ); +} + +user.f = func; +admin.g = func; + +*!* +// this равен объекту перед точкой: +user.f(); // Вася +admin.g(); // Админ +admin['g'](); // Админ (не важно, доступ к объекту через точку или квадратные скобки) +*/!* +``` + +**Значение `this` не зависит от того, как функция была создана, оно определяется исключительно в момент вызова.** + +## Значение this при вызове без контекста + +Если функция использует `this` -- это подразумевает работу с объектом. Но и прямой вызов `func()` технически возможен. + +Как правило, такая ситуация возникает при ошибке в разработке. + +При этом `this` получает значение `window`, глобального объекта: + +```js +//+ run +function func() { + alert(this); // выведет [object Window] или [object global] +} + +func(); +``` + +В современном стандарте языка это поведение изменено, вместо глобального объекта `this` будет `undefined`. + +```js +//+ run +function func() { + "use strict"; + alert(this); // выведет undefined (кроме IE<10) +} + +func(); +``` + +Это стоит иметь в виду для общего развития, но обычно если в функции используется `this`, то она, всё же, проектируется для вызова в контексте объекта. + +[warn header="`this` теряется при операциях с методом"] +Ещё раз обратим внимание: контекст `this` никак не привязан к функции, даже если она создана в объявлении объекта. + +Чтобы `this` передался правильно, нужно вызвать функцию именно через точку (или квадратные скобки). Любой более хитрый вызов приведёт к потере контекста, например: + +```js +//+ run +var user = { + name: "Вася", + hi: function() { alert(this.name); }, + bye: function() { alert("Пока"); } +}; + +user.hi(); // Вася (простой вызов работает) +*!* +// а теперь вызовем user.hi или user.bye в зависимости от имени +(user.name == "Вася" ? user.hi : user.bye)(); // undefined +*/!* +``` + +В последней строке примера метод получен в результате выполнения тернарного оператора и тут же вызван. При этом `this` теряется. + +Иначе говоря, такой вызов эквивалентен двум строкам: + +```js +var method = (user.name == "Вася" ? user.hi : user.bye); +method(); // без this +``` + +[/warn] + +## Задачи + + + + diff --git a/1-js/6-objects-more/2-constructor-new/1-two-functions-one-object/solution.md b/1-js/6-objects-more/2-constructor-new/1-two-functions-one-object/solution.md new file mode 100644 index 00000000..2dcc843b --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/1-two-functions-one-object/solution.md @@ -0,0 +1,19 @@ +Да, возможны. + +Они должны возвращать одинаковый объект. При этом если функция возвращает объект, то `this` не используется. + +Например, они могут вернуть один и тот же объект `obj`, определённый снаружи: + +```js +//+ run +var obj = {}; + +function A() { return obj; } +function B() { return obj; } + +var a = new A; +var b = new B; + +alert( a == b ); // true +``` + diff --git a/1-js/6-objects-more/2-constructor-new/1-two-functions-one-object/task.md b/1-js/6-objects-more/2-constructor-new/1-two-functions-one-object/task.md new file mode 100644 index 00000000..a70741db --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/1-two-functions-one-object/task.md @@ -0,0 +1,17 @@ +# Две функции один объект + +[importance 2] + +Возможны ли такие функции `A` и `B` в примере ниже, что соответствующие объекты `a,b` равны (см. код ниже)? + +```js +function A() { ... } +function B() { ... } + +var a = new A; +var b = new B; + +alert( a == b ); // true +``` + +Если да -- приведите пример кода с такими функциями. \ No newline at end of file diff --git a/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/_js.view/solution.js b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/_js.view/solution.js new file mode 100644 index 00000000..6e40b871 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/_js.view/solution.js @@ -0,0 +1,15 @@ +function Calculator() { + + this.read = function() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + }; + + this.sum = function() { + return this.a + this.b; + }; + + this.mul = function() { + return this.a * this.b; + }; +} \ No newline at end of file diff --git a/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/_js.view/test.js b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/_js.view/test.js new file mode 100644 index 00000000..74f1186e --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/_js.view/test.js @@ -0,0 +1,25 @@ +sinon.stub(window, "prompt") + +prompt.onCall(0).returns("2"); +prompt.onCall(1).returns("3"); + +describe("calculator", function() { + var calculator; + before(function() { + calculator = new Calculator(); + calculator.read(); + }); + + it("при вводе 2 и 3 сумма равна 5", function() { + assert.equal( calculator.sum(), 5 ); + }); + + it("при вводе 2 и 3 произведение равно 6", function() { + assert.equal( calculator.mul(), 6 ); + }); + +}); + +after(function() { + prompt.restore(); +}); \ No newline at end of file diff --git a/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/solution.md b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/solution.md new file mode 100644 index 00000000..41051702 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/solution.md @@ -0,0 +1,27 @@ + + +```js +//+ run demo +function Calculator() { + + this.read = function() { + this.a = +prompt('a?', 0); + this.b = +prompt('b?', 0); + }; + + this.sum = function() { + return this.a + this.b; + }; + + this.mul = function() { + return this.a * this.b; + }; +} + +var calculator = new Calculator(); +calculator.read(); + +alert( "Сумма=" + calculator.sum() ); +alert( "Произведение=" + calculator.mul() ); +``` + diff --git a/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/task.md b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/task.md new file mode 100644 index 00000000..bc7933a0 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/2-calculator-constructor/task.md @@ -0,0 +1,22 @@ +# Создать Calculator при помощи конструктора + +[importance 5] + +Напишите *функцию-конструктор* `Calculator`, которая создает объект с двумя методами: +
      +
    • Метод `read()` запрашивает два значения при помощи `prompt` и запоминает их в свойствах объекта.
    • +
    • Метод `sum()` возвращает сумму запомненных свойств.
    • +
    • Метод `mul()` возвращает произведение запомненных свойств.
    • +
    + +Пример использования: + +```js +var calculator = new Calculator(); +calculator.read(); + +alert( "Сумма=" + calculator.sum() ); +alert( "Произведение=" + calculator.mul() ); +``` + +[demo /] diff --git a/1-js/6-objects-more/2-constructor-new/3-accumulator/_js.view/solution.js b/1-js/6-objects-more/2-constructor-new/3-accumulator/_js.view/solution.js new file mode 100644 index 00000000..425f7bdd --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/3-accumulator/_js.view/solution.js @@ -0,0 +1,8 @@ +function Accumulator(startingValue) { + this.value = startingValue; + + this.read = function() { + this.value += +prompt('Сколько добавлять будем?', 0); + }; + +} diff --git a/1-js/6-objects-more/2-constructor-new/3-accumulator/_js.view/test.js b/1-js/6-objects-more/2-constructor-new/3-accumulator/_js.view/test.js new file mode 100644 index 00000000..1ab40633 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/3-accumulator/_js.view/test.js @@ -0,0 +1,37 @@ +describe("Accumulator(1)", function() { + var accumulator; + before(function() { + accumulator = new Accumulator(1); + }); + + beforeEach(function() { + sinon.stub(window, "prompt") + }); + + afterEach(function() { + prompt.restore(); + }); + + it("начальное значение 1", function() { + assert.equal( accumulator.value, 1 ); + }); + + it("после ввода 0 значение 1", function() { + prompt.returns("0"); + accumulator.read(); + assert.equal( accumulator.value, 1 ); + }); + + it("после ввода 1 значение 2", function() { + prompt.returns("1"); + accumulator.read(); + assert.equal( accumulator.value, 2 ); + }); + + it("после ввода 2 значение 4", function() { + prompt.returns("2"); + accumulator.read(); + assert.equal( accumulator.value, 4 ); + }); + +}); \ No newline at end of file diff --git a/1-js/6-objects-more/2-constructor-new/3-accumulator/solution.md b/1-js/6-objects-more/2-constructor-new/3-accumulator/solution.md new file mode 100644 index 00000000..974473fd --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/3-accumulator/solution.md @@ -0,0 +1,19 @@ + + +```js +//+ run +function Accumulator(startingValue) { + this.value = startingValue; + + this.read = function() { + this.value += +prompt('Сколько добавлять будем?', 0); + }; + +} + +var accumulator = new Accumulator(1); +accumulator.read(); +accumulator.read(); +alert( accumulator.value ); +``` + diff --git a/1-js/6-objects-more/2-constructor-new/3-accumulator/task.md b/1-js/6-objects-more/2-constructor-new/3-accumulator/task.md new file mode 100644 index 00000000..7852e561 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/3-accumulator/task.md @@ -0,0 +1,24 @@ +# Создать Accumulator при помощи конструктора + +[importance 5] + +Напишите *функцию-конструктор* `Accumulator(startingValue)`. +Объекты, которые она создает, должны хранить текущую сумму и прибавлять к ней то, что вводит посетитель. + +Более формально, объект должен: +
      +
    • Хранить текущее значение в своём свойстве `value`. Начальное значение свойства `value` ставится конструктором равным `startingValue`.
    • +
    • Метод `read()` вызывает `prompt`, принимает число и прибавляет его к свойству `value`.
    • +
    +Таким образом, свойство `value` является текущей суммой всего, что ввел посетитель при вызовах метода `read()`, с учетом начального значения `startingValue`. + +Ниже вы можете посмотреть работу кода: + +```js +var accumulator = new Accumulator(1); // начальное значение 1 +accumulator.read(); // прибавит ввод prompt к текущему значению +accumulator.read(); // прибавит ввод prompt к текущему значению +alert( accumulator.value ); // выведет текущее значение +``` + +[demo /] diff --git a/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/_js.view/solution.js b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/_js.view/solution.js new file mode 100644 index 00000000..175c5704 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/_js.view/solution.js @@ -0,0 +1,29 @@ +function Calculator() { + + var methods = { + "-": function(a, b) { + return a - b; + }, + "+": function(a, b) { + return a + b; + } + }; + + this.calculate = function(str) { + + var split = str.split(' '), + a = +split[0], + op = split[1], + b = +split[2] + + if(!methods[op] || isNaN(a) || isNaN(b)) { + return NaN; + } + + return methods[op](+a, +b); + } + + this.addMethod = function(name, func) { + methods[name] = func; + }; +} diff --git a/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/_js.view/test.js b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/_js.view/test.js new file mode 100644 index 00000000..50a30d5c --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/_js.view/test.js @@ -0,0 +1,26 @@ +var calculator; +before(function() { + calculator = new Calculator; +}); + +it("calculate(12 + 34) = 46", function() { + assert.equal( calculator.calculate("12 + 34"), 46 ); +}); + +it("calculate(34 - 12) = 22", function() { + assert.equal( calculator.calculate("34 - 12"), 22 ); +}); + +it("добавили умножение: calculate(2 * 3) = 6", function() { + calculator.addMethod("*", function(a, b) { + return a * b; + }); + assert.equal( calculator.calculate("2 * 3"), 6 ); +}); + +it("добавили возведение в степень: calculate(2 ** 3) = 8", function() { + calculator.addMethod("**", function(a, b) { + return Math.pow(a, b); + }); + assert.equal( calculator.calculate("2 ** 3"), 8 ); +}); diff --git a/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/solution.md b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/solution.md new file mode 100644 index 00000000..80b6a8a0 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/solution.md @@ -0,0 +1,55 @@ + + +```js +//+ run +function Calculator() { + + var methods = { + "-": function(a, b) { + return a - b; + }, + "+": function(a, b) { + return a + b; + } + }; + + this.calculate = function(str) { + + var split = str.split(' '), + a = +split[0], + op = split[1], + b = +split[2] + + if(!methods[op] || isNaN(a) || isNaN(b)) { + return NaN; + } + + return methods[op](+a, +b); + } + + this.addMethod = function(name, func) { + methods[name] = func; + }; +} + +var calc = new Calculator; + +calc.addMethod("*", function(a, b) { + return a * b; +}); +calc.addMethod("/", function(a, b) { + return a / b; +}); +calc.addMethod("**", function(a, b) { + return Math.pow(a, b); +}); + +var result = calc.calculate("2 ** 3"); +alert(result); // 8 +``` + +
      +
    • Обратите внимание на хранение методов. Они просто добавляются к внутреннему объекту.
    • +
    • Все проверки и преобразование к числу производятся в методе `calculate`. В дальнейшем он может быть расширен для поддержки более сложных выражений.
    • +
    + diff --git a/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/task.md b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/task.md new file mode 100644 index 00000000..89e61cc9 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/4-calculator-extendable/task.md @@ -0,0 +1,41 @@ +# Создайте калькулятор + +[importance 5] + +Напишите конструктор `Calculator`, который создаёт расширяемые объекты-калькуляторы. + +Эта задача состоит из двух частей, которые можно решать одна за другой. +
      +
    1. Первый шаг задачи: вызов `calculate(str)` принимает строку, например "1 + 2", с жёстко заданным форматом "ЧИСЛО операция ЧИСЛО" (по одному пробелу вокруг операции), и возвращает результат. Понимает плюс `+` и минус `-`. + +Пример использования: + +```js +var calc = new Calculator; + +alert(calc.calculate("3 + 7")); // 10 +``` + +
    2. +
    3. Второй шаг -- добавить калькулятору метод `addMethod(name, func)`, который учит калькулятор новой операции. Он получает имя операции `name` и функцию от двух аргументов `func(a,b)`, которая должна её реализовывать. + +Например, добавим операции умножить `*`, поделить `/` и возвести в степень `**`: + +```js +var powerCalc = new Calculator; +powerCalc.addMethod("*", function(a, b) { return a * b; }); +powerCalc.addMethod("/", function(a, b) { return a / b; }); +powerCalc.addMethod("**", function(a, b) { return Math.pow(a, b); }); + +var result = powerCalc.calculate("2 ** 3"); +alert(result); // 8 +``` + +
    4. +
    + +
      +
    • Поддержка скобок и сложных математических выражений в этой задаче не требуется.
    • +
    • Числа и операции могут состоять из нескольких символов. Между ними ровно один пробел.
    • +
    • Предусмотрите обработку ошибок. Какая она должна быть - решите сами.
    • +
    \ No newline at end of file diff --git a/1-js/6-objects-more/2-constructor-new/article.md b/1-js/6-objects-more/2-constructor-new/article.md new file mode 100644 index 00000000..ab1922d4 --- /dev/null +++ b/1-js/6-objects-more/2-constructor-new/article.md @@ -0,0 +1,185 @@ +# Создание объектов через "new" + +Обычный синтаксис `{...}` позволяет создать один объект. Но зачастую нужно создать много однотипных объектов. + +Для этого используют функции, запуская их при помощи специального оператора `new`. +[cut] +## Конструктор + +Конструктором становится любая функция, вызванная через `new`. + +Например: + +```js +function Animal(name) { + this.name = name; + this.canWalk = true; +} + +*!* +var animal = new Animal("ёжик"); +*/!* +``` + +Технически, никаких ограничений нет -- любую функцию можно вызвать при помощи `new`. Но при этом она работает несколько иным образом, чем обычно, поэтому функции, предназначенные к вызову через `new`, называют с большой буквы. + +**Алгоритм работы оператора `new`:** + +
      +
    1. Автоматически создается новый пустой объект.
    2. +
    3. Ключевое слово `this` получает ссылку на этот объект.
    4. +
    5. Функция выполняется. Как правило, она модифицирует `this`, добавляет методы, свойства.
    6. +
    7. Возвращается `this`.
    8. +
    + + +В результате вызова `new Animal("ёжик");` получаем объект: + +```js +animal = { + name: "ёжик", + canWalk: true +} +``` + +Иными словами, при вызове `new Animal` происходит что-то в таком духе (комментарии -- это то, что делает интерпретатор): + +```js +function Animal(name) { +*!* + // this = {} +*/!* + this.name = name; + this.canWalk = true; +*!* + // return this +*/!* +} +``` + +## Правила обработки return + +Как правило, конструкторы ничего не возвращают. Их задача -- записать всё, что нужно, в `this`, который автоматически станет результатом. + +Но если явный вызов `return` всё же есть, то применяется простое правило: +
      +
    • При вызове `return` с объектом, будет возвращён он, а не `this`.
    • +
    • При вызове `return` с примитивным значением, оно будет отброшено.
    • +
    + +Иными словами, вызов `return` с объектом вернёт объект, а с чем угодно, кроме объекта -- прекратит выполнение функции и возвратит `this`. + +Например, возврат объекта: + +```js +//+ run +function BigAnimal() { + + this.name = "Мышь"; + + return { name: "Годзилла" }; // <-- возвратим объект +} + +alert( new BigAnimal().name ); // Годзилла, получили объект вместо this +``` + +А вот пример с возвратом строки: + +```js +//+ run +function BigAnimal() { + + this.name = "Мышь"; + + return "Годзилла"; // <-- возвратим примитив +} + +alert( new BigAnimal().name ); // Мышь, получили this (а Годзилла пропал) +``` + +Эта особенность работы `new` прописана в стандарте, знать о ней полезно, но используется она весьма редко. + +[smart] +Кстати, при вызове `new` без аргументов скобки можно не ставить: + +```js +var animal = new BigAnimal; // <-- без скобок +// то же самое что +var animal = new BigAnimal(); +``` + +[/smart] + +## Создание методов в конструкторе + +Использование функций для создания объекта дает большую гибкость. Можно передавать конструктору параметры, определяющие как его создавать. + +Например, `new User(name)` создает объект с заданным значением свойства `name` и методом `sayHi`: + +```js +//+ run +function User(name) { + this.name = name; + + this.sayHi = function() { + alert("Моё имя: " + this.name); + }; +} + +*!* +var ivan = new User("Иван"); + +ivan.sayHi(); // Моё имя: Иван +*/!* + +/* +ivan = { + name: "Иван", + sayHi: функция +} +*/ +``` + +## Локальные переменные + +В функции-конструкторе бывает удобно объявить вспомогательные локальные переменные и вложенные функции, которые будут видны только внутри: + +```js +//+ run +function User(firstName, lastName) { +*!* + // вспомогательная переменная + var phrase = "Привет"; + + // вспомогательная вложенная функция + function getFullName() { + return firstName + " " + lastName; + } +*/!* + + this.sayHi = function() { + alert(phrase + ", " + getFullName()); // использование + }; +} + +var vasya = new User("Вася", "Петров"); +vasya.sayHi(); // Вася Петров +``` + +Мы уже говорили об этом подходе ранее, в главе [](/closures-usage). + +Те функции и данные, которые должны быть доступны для внешнего кода, мы пишем в `this` -- и к ним можно будет обращаться, как например `vasya.sayHi()`, а вспомогательные, которые нужны только внутри самого объекта, сохраняем в локальной области видимости. + +## Итого + +Объекты могут быть созданы при помощи функций-конструкторов: + +
      +
    • Любая функция может быть вызвана с `new`, при этом она получает новый пустой объект в качестве `this`, в который она добавляет свойства. Если функция не решит возвратить свой объект, то её результатом будет `this`.
    • +
    • Функции, которые предназначены для создания объектов, называются *конструкторами*. Их названия пишут с большой буквы, чтобы отличать от обычных.
    • +
    + + + + + diff --git a/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/_js.view/solution.js b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/_js.view/solution.js new file mode 100644 index 00000000..f92594e2 --- /dev/null +++ b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/_js.view/solution.js @@ -0,0 +1,11 @@ +function Article() { + this.created = new Date; + + Article.count++; // увеличиваем счетчик при каждом вызове + Article.last = this.created; // и запоминаем дату +} +Article.count = 0; // начальное значение + +Article.showStats = function() { + alert('Всего: ' + this.count + ', Последняя: ' + this.last); +}; diff --git a/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/_js.view/test.js b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/_js.view/test.js new file mode 100644 index 00000000..04f7d30b --- /dev/null +++ b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/_js.view/test.js @@ -0,0 +1,28 @@ +describe("Article.showStats", function() { + before(function() { + sinon.stub(window, "alert"); + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + window.alert.restore(); + this.clock.restore(); + }); + + it("Выводит число статей и дату создания последней", function() { + new Article(); + this.clock.tick(100); + new Article(); + Article.showStats(); + + assert( alert.calledWith('Всего: 2, Последняя: ' + new Date() ) ); + }); + + it("и ещё одна статья...", function() { + this.clock.tick(100); + new Article(); + Article.showStats(); + + assert( alert.calledWith('Всего: 3, Последняя: ' + new Date() ) ); + }); +}); \ No newline at end of file diff --git a/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/solution.md b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/solution.md new file mode 100644 index 00000000..ad088c0b --- /dev/null +++ b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/solution.md @@ -0,0 +1,29 @@ +Решение (как вариант): + +```js +//+ run +function Article() { + this.created = new Date; + +*!* + Article.count++; // увеличиваем счетчик при каждом вызове + Article.last = this.created; // и запоминаем дату +*/!* +} +Article.count = 0; // начальное значение +// (нельзя оставить undefined, т.к. Article.count++ будет NaN) + +Article.showStats = function() { + alert('Всего: ' + this.count + ', Последняя: ' + this.last); +}; + +new Article(); +new Article(); + +Article.showStats(); // Всего: 2, Последняя: (дата) + +new Article(); + +Article.showStats(); // Всего: 3, Последняя: (дата) +``` + diff --git a/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/task.md b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/task.md new file mode 100644 index 00000000..513ed072 --- /dev/null +++ b/1-js/6-objects-more/3-static-properties-and-methods/1-objects-counter/task.md @@ -0,0 +1,33 @@ +# Счетчик объектов + +[importance 5] + +Добавить в конструктор `Article`: +
      +
    • Подсчёт общего количества созданных объектов.
    • +
    • Запоминание даты последнего созданного объекта.
    • +
    +Используйте для этого статические свойства. + +Пусть вызов `Article.showStats()` выводит то и другое. + +Использование: + +```js +function Article() { + this.created = new Date(); +*!* + // ... ваш код ... +*/!* +} + +new Article(); +new Article(); + +Article.showStats(); // Всего: 2, Последняя: (дата) + +new Article(); + +Article.showStats(); // Всего: 3, Последняя: (дата) +``` + diff --git a/1-js/6-objects-more/3-static-properties-and-methods/article.md b/1-js/6-objects-more/3-static-properties-and-methods/article.md new file mode 100644 index 00000000..decd48c6 --- /dev/null +++ b/1-js/6-objects-more/3-static-properties-and-methods/article.md @@ -0,0 +1,245 @@ +# Статические и фабричные методы + +Методы и свойства, которые не привязаны к конкретному экземпляру объекта, называют "статическими". Их записывают прямо в саму функцию-конструктор. + +[cut] + +## Статические свойства + +В коде ниже используются статические свойства `Article.count` и `Article.DEFAULT_FORMAT`: + +```js +function Article() { + Article.count++; +} + +Article.count = 0; // статическое свойство-переменная +Article.DEFAULT_FORMAT = "html"; // статическое свойство-константа +``` + +Они хранят данные, специфичные не для одного объекта, а для всех статей целиком. + +Как правило, это чаще константы, такие как формат "по умолчанию" `Article.DEFAULT_FORMAT`. + +## Статические методы + +С примерами статических методов мы уже знакомы: это встроенные методы [String.fromCharCode](http://javascript.ru/String.fromCharCode), [Date.parse](http://javascript.ru/Date.parse). + +Создадим для `Article` статический метод `Article.showCount()`: + +```js +//+ run +function Article() { + Article.count++; + + //... +} +Article.count = 0; + +Article.showCount = function() { +*!* + alert(this.count); // (1) +*/!* +} + +// использование +new Article(); +new Article(); +Article.showCount(); // (2) +``` + +Здесь `Article.count` -- статическое свойство, а `Article.showCount` -- статический метод. + +**Обратите внимание на контекст `this`. Несмотря на то, что переменная и метод -- статические, он всё ещё полезен. В строке `(1)` он равен `Article`!** + +## Пример: сравнение объектов + +Ещё один хороший способ применения -- сравнение объектов. + +Например, у нас есть объект `Journal` для журналов. Журналы можно сравнивать -- по толщине, по весу, по другим параметрам. + +Объявим "стандартную" функцию сравнения, которая будет сравнивать по дате издания. Эта функция сравнения, естественно, не привязана к конкретному журналу, но относится к журналам вообще. + +Поэтому зададим её как статический метод `Journal.compare`: + +```js +function Journal(date) { + this.date = date; + // ... +} + +// возвращает значение, большее 0, если A больше B, иначе меньшее 0 +Journal.compare = function(journalA, journalB) { + return journalA.date - journalB.date; +}; +``` + +В примере ниже эта функция используется для поиска самого раннего журнала из массива: + +```js +//+ run +function Journal(date) { + this.date = date; + + this.formatDate = function(date) { + return date.getDate() + '.' + (date.getMonth()+1) + '.' + date.getFullYear(); + }; + + this.getTitle = function() { + return "Выпуск от " + this.formatDate(this.date); + }; + +} + +*!* +Journal.compare = function(journalA, journalB) { + return journalA.date - journalB.date; +}; +*/!* + +// использование: +var journals = [ + new Journal(new Date(2012,1,1)), + new Journal(new Date(2012,0,1)), + new Journal(new Date(2011,11,1)) +]; + +function findMin(journals) { + var min = 0; + for(var i=0; i 0 ) min = i; +*/!* + } + return journals[min]; +} + +alert( findMin(journals).getTitle() ); +``` + +**Статический метод также можно использовать для функций, которые вообще не требуют наличия объекта.** + +Например, метод `formatDate(date)` можно сделать статическим. Он будет форматировать дату "как это принято в журналах", при этом его можно использовать в любом месте кода, не обязательно создавать журнал. + +Например: + +```js +//+ run +function Journal() { /*...*/ } + +Journal.formatDate = function(date) { + return date.getDate() + '.' + (date.getMonth()+1) + '.' + date.getFullYear(); +} + +// ни одного объекта Journal нет, просто форматируем дату +alert( *!*Journal.formatDate(new Date)*/!* ); +``` + +## Фабричные методы + +Рассмотрим ситуацию, когда объект нужно создавать различными способами. Например, это реализовано во встроенном объекте [Date](/datetime). Он по-разному обрабатывает аргументы разных типов: + +
      +
    • `new Date()` -- создаёт объект с текущей датой,
    • +
    • `new Date(milliseconds)` -- создаёт дату по количеству миллисекунд `milliseconds`,
    • +
    • `new Date(year, month, day ...)` -- создаёт дату по компонентам год, месяц, день...
    • +
    • `new Date(datestring)` -- читает дату из строки `datestring`
    • +
    + +**"Фабричный статический метод" -- удобная альтернатива такому конструктору. Так называется статический метод, который служит для создания новых объектов (поэтому и называется "фабричным").** + +Пример встроенного фабричного метода -- [String.fromCharCode(code)](http://javascript.ru/String.fromCharCode). Этот метод создает строку из кода символа: + +```js +//+ run +var str = String.fromCharCode(65); +alert(str); // 'A' +``` + +Но строки -- слишком простой пример, посмотрим что-нибудь посложнее. + +Допустим, нам нужно создавать объекты `User`: анонимные `new User()` и с данными `new User({name: 'Вася', age: 25})`. + +Можно, конечно, создать полиморфную функцию-конструктор `User`: + +```js +//+ run +function User(userData) { + if (userData) { // если указаны данные -- одна ветка if + this.name = userData.name; + this.age = userData.age; + } else { // если не указаны -- другая + this.name = 'Аноним'; + } + + this.sayHi = function() { alert(this.name) }; + // ... +} + +// Использование + +var guest = new User(); +guest.sayHi(); // Аноним + +var knownUser = new User({name: 'Вася', age: 25}); +knownUser.sayHi(); // Вася +``` + +Подход с использованием фабричных методов был бы другим. Вместо разбора параметров в конструкторе -- делаем два метода: `User.createAnonymous` и `User.createFromData`. + +Код: + +```js +//+ run +function User() { + this.sayHi = function() { alert(this.name) }; +} + +User.createAnonymous = function() { + var user = new User; + user.name = 'Аноним'; + return user; +} + +User.createFromData = function(userData) { + var user = new User; + user.name = userData.name; + user.age = userData.age; + return user; +} + +// Использование + +*!* +var guest = User.createAnonymous(); +guest.sayHi(); // Аноним + +var knownUser = User.createFromData({name: 'Вася', age: 25}); +knownUser.sayHi(); // Вася +*/!* +``` + +Преимущества использования фабричных методов: + +[compare] ++Лучшая читаемость кода. Как конструктора -- вместо одной большой функции несколько маленьких, так и вызывающего кода -- явно видно, что именно создаётся. ++Лучший контроль ошибок, т.к. если в `createFromData` ничего не передали, то будет ошибка, а полиморфный конструктор создал бы анонимного посетителя. ++Удобная расширяемость. Например, нужно добавить создание администратора, без аргументов. Фабричный метод сделать легко: `User.createAdmin = function() { ... }`. А для полиморфного конструктора вызов без аргумента создаст анонима, так что нужно добавить параметр -- "тип посетителя" и усложнить этим код. +[/compare] + +**Поэтому полиморфные конструкторы лучше использовать там, где нужна именно полиморфность**, т.е. когда непонятно, какого типа аргумент передадут, и хочется в одном конструкторе охватить все варианты. + +А в остальных случаях отличная альтернатива -- фабричные методы. + +## Итого + +Статические свойства и методы объекта удобно применять в следующих случаях: + +
      +
    • Общие действия и подсчёты, имеющие отношения ко всем объектам данного типа. В примерах выше это подсчёт количества.
    • +
    • Методы, не привязанные к конкретному объекту, например сравнение.
    • +
    • Вспомогательные методы, которые полезны вне объекта, например для форматирования даты.
    • +
    • Фабричные методы.
    • +
    + diff --git a/1-js/6-objects-more/4-call-apply/1-rewrite-sum-arguments/solution.md b/1-js/6-objects-more/4-call-apply/1-rewrite-sum-arguments/solution.md new file mode 100644 index 00000000..ba57d3fa --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/1-rewrite-sum-arguments/solution.md @@ -0,0 +1,31 @@ +# Первый вариант + +```js +//+ run +function sumArgs() { + // скопируем reduce из массива + arguments.reduce = [].reduce; + return arguments.reduce(function(a, b) { + return a + b; + }); +} + +alert( sumArgs(4,5,6) ); // 15 +``` + +# Второй вариант + +Метод `call` здесь вполне подойдёт, так как требуется вызвать `reduce` в контексте `arguments` с одним аргументом. + +```js +//+ run +function sumArgs() { + // запустим reduce из массива напрямую + return [].reduce.call(arguments, function(a, b) { + return a + b; + }); +} + +alert( sumArgs(4,5,6) ); // 15 +``` + diff --git a/1-js/6-objects-more/4-call-apply/1-rewrite-sum-arguments/task.md b/1-js/6-objects-more/4-call-apply/1-rewrite-sum-arguments/task.md new file mode 100644 index 00000000..412f68c9 --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/1-rewrite-sum-arguments/task.md @@ -0,0 +1,28 @@ +# Перепишите суммирование аргументов + +[importance 5] + +Есть функция `sum`, которая суммирует все элементы массива: + +```js +//+ run +function sum(arr) { + return arr.reduce(function(a, b) { return a + b; }); +} + +alert( sum([1,2,3]) ); // 6 (=1+2+3) +``` + +Создайте аналогичную функцию `sumArgs()`, которая будет суммировать все свои аргументы: + +```js +function sumArgs() { + /* ваш код */ +} + +alert( sumArgs(1,2,3) ); // 6, аргументы переданы через запятую, без массива +``` + +Для решения примените метод `reduce` к `arguments`, используя `call`, `apply` или одалживание метода. + +P.S. Функция `sum` вам не понадобится, она приведена в качестве примера использования `reduce` для похожей задачи. \ No newline at end of file diff --git a/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/_js.view/solution.js b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/_js.view/solution.js new file mode 100644 index 00000000..41cf927e --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/_js.view/solution.js @@ -0,0 +1,3 @@ +function applyAll(func) { + return func.apply(this, [].slice.call(arguments, 1) ); +} \ No newline at end of file diff --git a/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/_js.view/test.js b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/_js.view/test.js new file mode 100644 index 00000000..7de18f29 --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/_js.view/test.js @@ -0,0 +1,15 @@ +describe("applyAll", function() { + + it("применяет функцию ко всем аргументам, начиная со 2го", function() { + var min = applyAll(Math.min, 1, 2, 3); + assert.equal( min, 1 ); + }); + + it("при отсутствии аргументов просто вызывает функцию", function() { + var spy = sinon.spy(); + applyAll(spy); + assert( spy.calledOnce ); + assert.equal( spy.firstCall.args.length, 0 ); + }); + +}); \ No newline at end of file diff --git a/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/solution.md b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/solution.md new file mode 100644 index 00000000..32d6e1c7 --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/solution.md @@ -0,0 +1,23 @@ + + +```js +//+ run +function sum() { + return [].reduce.call(arguments, function(a, b) { return a + b; }); +} +function mul() { + return [].reduce.call(arguments, function(a, b) { return a * b; }); +} + +*!* +function applyAll(func) { + return func.apply(this, [].slice.call(arguments, 1) ); +} +*/!* + +alert( applyAll(sum, 1, 2, 3) ); // 6 +alert( applyAll(mul, 2, 3, 4) ); // 24 +alert( applyAll(Math.max, 2, -2, 3) ); // 3 +alert( applyAll(Math.min, 2, -2, 3) ); // -2 +``` + diff --git a/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/task.md b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/task.md new file mode 100644 index 00000000..d7f13e9a --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/2-apply-function-skip-first-argument/task.md @@ -0,0 +1,35 @@ +# Примените функцию к аргументам + +[importance 5] + +Напишите функцию `applyAll(func, arg1, arg2...)`, которая получает функцию `func` и произвольное количество аргументов. + +Она должна вызвать `func(arg1, arg2...)`, то есть передать в `func` все аргументы, начиная со второго, и возвратить результат. + +Например: + +```js +// Применить Math.max к аргументам 2, -2, 3 +alert( applyAll(Math.max, 2, -2, 3) ); // 3 + +// Применить Math.min к аргументам 2, -2, 3 +alert( applyAll(Math.min, 2, -2, 3) ); // -2 +``` + +Область применения `applyAll`, конечно, шире, можно вызывать её и со своими функциями: + +```js +//+ run +function sum() { // суммирует аргументы: sum(1,2,3) = 6 + return [].reduce.call(arguments, function(a, b) { return a + b; }); +} +function mul() { // перемножает аргументы: mul(2,3,4) = 24 + return [].reduce.call(arguments, function(a, b) { return a * b; }); +} + +*!* +alert( applyAll(sum, 1, 2, 3) ); // -> sum(1, 2, 3) = 6 +alert( applyAll(mul, 2, 3, 4) ); // -> mul(2, 3, 4) = 24 +*/!* +``` + diff --git a/1-js/6-objects-more/4-call-apply/article.md b/1-js/6-objects-more/4-call-apply/article.md new file mode 100644 index 00000000..8308ec8d --- /dev/null +++ b/1-js/6-objects-more/4-call-apply/article.md @@ -0,0 +1,312 @@ +# Явное указание this: "call", "apply" + +Итак, мы знаем, что в `this` -- это текущий объект при вызове "через точку" и новый объект при конструировании через `new`. + +В этой главе наша цель получить окончательное и полное понимание `this` в JavaScript. Для этого не хватает всего одного элемента: способа явно указать `this` при помощи методов `call` и `apply`. + +[cut] + +## Метод call + +Синтаксис метода `call`: + +```js +func.call(context, arg1, arg2,...) +``` + +При этом вызывается функция `func`, первый аргумент `call` становится её `this`, а остальные передаются "как есть". + +**Вызов `func.call(context, a, b...)` -- то же, что обычный вызов `func(a, b...)`, но с явно указанным `this(=context)`.** + +Например, у нас есть функция `showFullName`, которая работает с `this`: + +```js +function showFullName() { + alert( this.firstName + " " + this.lastName ); +} +``` + +**Обратите внимание, JavaScript позволяет использовать `this` везде. Любая функция может в своём коде упомянуть `this`, каким будет это значение -- выяснится в момент запуска.** + +Вызов `showFullName.call(user)` запустит функцию, установив `this = user`, вот так: + +```js +//+ run +function showFullName() { + alert( this.firstName + " " + this.lastName ); +} + +var user = { + firstName: "Василий", + lastName: "Петров" +}; + +*!* +// функция вызовется с this=user +showFullName.call(user) // "Василий Петров" +*/!* +``` + +После контекста в `call` можно передать аргументы для функции. Вот пример с более сложным вариантом `showFullName`, который конструирует ответ из указанных свойств объекта: + +```js +//+ run +var user = { + firstName: "Василий", + surname: "Петров", + patronym: "Иванович" +}; + +function showFullName(firstPart, lastPart) { + alert( this[firstPart] + " " + this[lastPart] ); +} + +*!* +// f.call(контекст, аргумент1, аргумент2, ...) +showFullName.call(user, 'firstName', 'surname') // "Василий Петров" +showFullName.call(user, 'firstName', 'patronym') // "Василий Иванович" +*/!* +``` + +## "Одалживание метода" + +При помощи `call` можно легко взять метод одного объекта, в том числе встроенного, и вызвать в контексте другого. + +Это называется "одалживание метода" (на англ. *method borrowing*). + +**Используем эту технику для упрощения манипуляций с `arguments`.** + +Как мы знаем, `arguments` не массив, а обычный объект, поэтому таких полезных методов как `push`, `pop`, `join` и других у него нет. Но иногда так хочется, чтобы были... + +Нет ничего проще! Давайте скопируем метод `join` из обычного массива: + +```js +//+ run +function printArgs() { + arguments.join = [].join; // одолжили метод (1) + + var argStr = arguments.join(':'); // (2) + + alert(argStr); // сработает и выведет 1:2:3 +} + +printArgs(1, 2, 3); +``` + +
      +
    1. В строке `(1)` объявлен пустой массив `[]` и скопирован его метод `[].join`. Обратим внимание, мы не вызываем его, а просто копируем. Функция, в том числе встроенная -- обычное значение, мы можем скопировать любое свойство любого объекта, и `[].join` здесь не исключение.
    2. +
    3. В строке `(2)` запустили `join` в контексте `arguments`, как будто он всегда там был.
    4. + +[smart header="Почему вызов сработает?"] + +Здесь метод join массива скопирован и вызван в контексте `arguments`. Не произойдёт ли что-то плохое от того, что `arguments` -- не массив? Почему он, вообще, сработал? + +Ответ на эти вопросы простой. В соответствии [со спецификацией](http://es5.github.com/x15.4.html#x15.4.4.5), внутри `join` реализован примерно так: + +```js +function join(separator) { + if (!this.length) return ''; + + var str = this[0]; + + for (var i = 1; islice. + +По стандарту вызов `arr.slice(start, end)` создаёт новый массив и копирует в него элементы массива `arr` от `start` до `end`. А если `start` и `end` не указаны, то копирует весь массив. + +Вызовем его в контексте `arguments`: + +```js +//+ run +function printArgs() { + // вызов arr.slice() скопирует все элементы из this в новый массив +*!* + var args = [].slice.call(arguments); +*/!* + alert( args.join(', ') ); // args - полноценный массив из аргументов +} + +printArgs('Привет', 'мой', 'мир'); // Привет, мой, мир +``` + +Как и в случае с `join`, такой вызов технически возможен потому, что `slice` для работы требует только нумерованные свойства и `length`. Всё это в `arguments` есть. + +## Метод apply + +Если нам неизвестно, с каким количеством аргументов понадобится вызвать функцию, можно использовать более мощный метод: `apply`. + +**Вызов функции при помощи `func.apply` работает аналогично `func.call`, но принимает массив аргументов вместо списка.** + +```js +func.call(context, arg1, arg2) +// идентичен вызову +func.apply(context, [arg1, arg2]); +``` + +В частности, эти две строчки cработают одинаково: + +```js +showFullName.call(user, 'firstName', 'surname'); + +showFullName.apply(user, ['firstName', 'surname']); +``` + +Преимущество `apply` перед `call` отчётливо видно в следующем примере, когда мы формируем массив аргументов динамически: + +```js +//+ run +var arr = []; +arr.push(1); +arr.push(5); +arr.push(2); + +// получить максимум из элементов arr +alert( Math.max.apply(null, arr) ); // 5 +``` + +Обратим внимание, в примере выше вызывается метод `Math.max`. Его стандартное применение -- это выбор максимального аргумента из переданных, которых может быть сколько угодно: + +```js +//+ run +alert( Math.max(1, 5, 2) ); // 5 +alert( Math.max(1, 5, 2, 8) ); // 8 +``` + +В примере выше мы передали аргументы через массив -- второй параметр `apply`... Но вы, наверное, заметили небольшую странность? В качестве контекста `this` был передан `null`. + +Строго говоря, полным эквивалентом вызову `Math.max(1,2,3)` был бы вызов `Math.max.apply(Math, [1,2,3])`. В обоих этих вызовах контекстом будет объект `Math`. + +Но в данном случае в качестве контекста можно передавать что угодно, поскольку в своей внутренней реализации метод `Math.max`не использует `this`. Действительно, зачем `this`, если нужно всего лишь выбрать максимальный из аргументов? + +**Вот так, при помощи `apply` мы получили короткий и элегантный способ вычислить максимальное значение в массиве!** + +[smart header="Вызов `call/apply` с `null` или `undefined`"] + +В старом стандарте при указании первого аргумента `null` или `undefined` в `call/apply`, функция получала `this = window`, например: + +```js +//+ run +function f() { + alert(this); +} + +f.call(null); // window +``` + +Это поведение исправлено в современном стандарте ([15.3](http://es5.github.com/x15.3.html#x15.3.4.3)). + +Если функция работает в строгом режиме, то `this` передаётся "как есть": + +```js +//+ run +function f() { + "use strict"; +*!* + alert(this); // null, а не window +*/!* +} + +f.call(null); +``` + +[/smart] + +## Итого про this + + +Значение `this` устанавливается в зависимости от того, как вызвана функция: +
      +
      При вызове функции как метода
      +
      + +```js +obj.func(...) // this = obj +obj["func"](...) +``` + +
      +
      При обычном вызове
      +
      + +```js +func(...) // this = window (ES3) /undefined (ES5) +``` + +
      +
      В `new`
      +
      + +```js +new func() // this = {} (новый объект) +``` + +
      +
      Явное указание
      +
      + +```js +func.apply(context, args) // this = context (явная передача) +func.call(context, arg1, arg2, ...) +``` + +
      +
      + + diff --git a/1-js/6-objects-more/5-bind/1-cross-browser-bind/solution.md b/1-js/6-objects-more/5-bind/1-cross-browser-bind/solution.md new file mode 100644 index 00000000..28b52dcb --- /dev/null +++ b/1-js/6-objects-more/5-bind/1-cross-browser-bind/solution.md @@ -0,0 +1,8 @@ + +Страшновато выглядит, да? Работает так (по строкам): +
        +
      1. Вызов `bind` сохраняет дополнительные аргументы `args` (они идут со 2го номера) в массив `bindArgs`.
      2. +
      3. ... и возвращает обертку `wrapper`.
      4. +
      5. Эта обёртка делает из `arguments` массив `args` и затем, используя метод [concat](http://javascript.ru/Array/concat), прибавляет их к аргументам `bindArgs` (карринг).
      6. +
      7. Затем передаёт вызов `func` с контекстом и общим массивом аргументов.
      8. +
      diff --git a/1-js/6-objects-more/5-bind/1-cross-browser-bind/task.md b/1-js/6-objects-more/5-bind/1-cross-browser-bind/task.md new file mode 100644 index 00000000..af2df791 --- /dev/null +++ b/1-js/6-objects-more/5-bind/1-cross-browser-bind/task.md @@ -0,0 +1,22 @@ +# Кросс-браузерная эмуляция bind + +[importance 3] + +Если вы вдруг захотите копнуть поглубже -- аналог `bind` для IE8- и старых версий других браузеров будет выглядеть следующим образом: + +```js +function bind(func, context /*, args*/) { + var bindArgs = [].slice.call(arguments, 2); // (1) + function wrapper() { // (2) + var args = [].slice.call(arguments); + var unshiftArgs = bindArgs.concat(args); // (3) + return func.apply(context, unshiftArgs); // (4) + } + return wrapper; +} +``` + +Использование -- вместо `mul.bind(null, 2)` вызывать `bind(mul, null, 2)`. + +Не факт, что он вам понадобится, но в качестве упражнение попробуйте разобраться, как это работает. + diff --git a/1-js/6-objects-more/5-bind/2-write-to-object-after-bind/solution.md b/1-js/6-objects-more/5-bind/2-write-to-object-after-bind/solution.md new file mode 100644 index 00000000..c43c8731 --- /dev/null +++ b/1-js/6-objects-more/5-bind/2-write-to-object-after-bind/solution.md @@ -0,0 +1,22 @@ +Ответ: `Hello`. + +```js +//+ run +function f() { + alert( this ); +} + +var user = { + g: bind(f, "Hello") +} + +user.g(); +``` + +Так как вызов идёт в контексте объекта `user.g()`, то внутри функции `g` контекст `this = user`. + +Однако, функции `g` совершенно без разницы, какой `this` она получила. + +Её единственное предназначение -- это передать вызов в `f` вместе с аргументами и ранее указанным контекстом `"Hello"`, что она и делает. + +Эта задача демонстрирует, что изменить однажды привязанный контекст уже нельзя. \ No newline at end of file diff --git a/1-js/6-objects-more/5-bind/2-write-to-object-after-bind/task.md b/1-js/6-objects-more/5-bind/2-write-to-object-after-bind/task.md new file mode 100644 index 00000000..75b35b6d --- /dev/null +++ b/1-js/6-objects-more/5-bind/2-write-to-object-after-bind/task.md @@ -0,0 +1,18 @@ +# Запись в объект после bind + +[importance 5] + +Что выведет функция? + +```js +function f() { + alert( this ); +} + +var user = { + g: bind(f, "Hello") +} + +user.g(); +``` + diff --git a/1-js/6-objects-more/5-bind/3-second-bind/solution.md b/1-js/6-objects-more/5-bind/3-second-bind/solution.md new file mode 100644 index 00000000..952a8df4 --- /dev/null +++ b/1-js/6-objects-more/5-bind/3-second-bind/solution.md @@ -0,0 +1,55 @@ +Ответ: `"Вася"`. + +```js +//+ run +function f() { + alert(this.name); +} + +f = f.bind( {name: "Вася"} ).bind( {name: "Петя"} ); + +f(); // Вася +``` + +Первый вызов `f.bind(..Вася..)` возвращает "обёртку", которая устанавливает контекст для `f` и передаёт вызов `f`. + +Следующий вызов `bind` будет устанавливать контекст уже для этой обёртки, это ни на что не влияет. + +Чтобы это проще понять, используем наш собственный вариант `bind` вместо встроенного: + +```js +function bind(func, context) { + return function() { + return func.apply(context, arguments); + }; +} +``` + +Код станет таким: + +```js +function f() { + alert(this.name); +} + +f = bind(f, {name: "Вася"} ); // (1) +f = bind(f, {name: "Петя"} ); // (2) + +f(); // Вася +``` + +Здесь видно, что первый вызов `bind`, в строке `(1)`, возвращает обёртку вокруг `f`, которая выглядит так (выделена): + +```js +function bind(func, context) { +*!* + return function() { + return func.apply(context, arguments); + }; +*/!* +} +``` + +В этой обёртке нигде не используется `this`, только `func` и `context`. Посмотрите на код, там нигде нет `this`. + +Поэтому следующий `bind` в строке `(2)`, который выполняется уже над обёрткой и фиксирует в ней `this`, ни на что не влияет. Какая разница, что будет в качестве `this` в функции, которая этот `this` не использует? diff --git a/1-js/6-objects-more/5-bind/3-second-bind/task.md b/1-js/6-objects-more/5-bind/3-second-bind/task.md new file mode 100644 index 00000000..5808b590 --- /dev/null +++ b/1-js/6-objects-more/5-bind/3-second-bind/task.md @@ -0,0 +1,16 @@ +# Повторный bind + +[importance 5] + +Что выведет этот код? + +```js +function f() { + alert(this.name); +} + +f = f.bind( {name: "Вася"} ).bind( {name: "Петя" } ); + +f(); +``` + diff --git a/1-js/6-objects-more/5-bind/4-function-property-after-bind/solution.md b/1-js/6-objects-more/5-bind/4-function-property-after-bind/solution.md new file mode 100644 index 00000000..a882a35e --- /dev/null +++ b/1-js/6-objects-more/5-bind/4-function-property-after-bind/solution.md @@ -0,0 +1,4 @@ +Ответ: `undefined`. + +Результатом работы `bind` является функция-обёртка над `sayHi`. Эта функция -- самостоятельный объект, у неё уже нет свойства `test`. + diff --git a/1-js/6-objects-more/5-bind/4-function-property-after-bind/task.md b/1-js/6-objects-more/5-bind/4-function-property-after-bind/task.md new file mode 100644 index 00000000..2ab42fc4 --- /dev/null +++ b/1-js/6-objects-more/5-bind/4-function-property-after-bind/task.md @@ -0,0 +1,20 @@ +# Свойство функции после bind + +[importance 5] + +В свойство функции записано значение. Изменится ли оно после применения `bind`? Обоснуйте ответ. + +```js +function sayHi() { + alert(this.name); +} +sayHi.test = 5; +alert(sayHi.test); // 5 + +*!* +var bound = sayHi.bind({ name: "Вася" }); + +alert(bound.test); // что выведет? почему? +*/!* +``` + diff --git a/1-js/6-objects-more/5-bind/5-question-use-bind/solution.md b/1-js/6-objects-more/5-bind/5-question-use-bind/solution.md new file mode 100644 index 00000000..d022a510 --- /dev/null +++ b/1-js/6-objects-more/5-bind/5-question-use-bind/solution.md @@ -0,0 +1,105 @@ +# Решение с bind + +Ошибка происходит потому, что `ask` получает только функцию, без объекта-контекста. + +Используем `bind`, чтобы передать в `ask` функцию с уже привязанным контекстом: + +```js +//+ run +"use strict"; + +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} + +var user = { + login: 'Василий', + password: '12345', + + loginOk: function() { + alert(this.login + ' вошёл в сайт'); + }, + + loginFail: function() { + alert(this.login + ': ошибка входа'); + }, + + checkPassword: function() { +*!* + ask("Ваш пароль?", this.password, this.loginOk.bind(this), this.loginFail.bind(this)); +*/!* + } +}; + +var vasya = user; +user = null; +vasya.checkPassword(); +``` + +# Решение через замыкание + +Альтернативное решение -- сделать функции-обёртки над `user.loginOk/loginFail`: + +```js +var user = { + ... + checkPassword: function() { +*!* + ask("Ваш пароль?", this.password, + function() { user.loginOk(); }, function() { user.loginFail(); }); +*/!* + } +} +``` + +...Но такой код использует переменную `user`, так что если объект переместить из неё, к примеру, так, то работать он не будет: + +```js +var vasya = user; // переместим user в vasya +user = null; +vasya.checkPassword(); // упс будет ошибка, ведь в коде объекта остался user +``` + +Для того, чтобы избежать проблем, можно использовать `this`. Внутри `checkPassword` он всегда будет равен текущему объекту, так что скопируем его в переменную, которую назовём `self`: + +```js +//+ run +"use strict"; + +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} + +var user = { + login: 'Василий', + password: '12345', + + loginOk: function() { + alert(this.login + ' вошёл в сайт'); + }, + + loginFail: function() { + alert(this.login + ': ошибка входа'); + }, + + checkPassword: function() { +*!* + var self = this; + ask("Ваш пароль?", this.password, + function() { self.loginOk(); }, + function() { self.loginFail(); } + ); +*/!* + } +}; + +var vasya = user; +user = null; +vasya.checkPassword(); +``` + +Теперь всё работает. Анонимные функции достают правильный контекст из замыкания, где он сохранён в переменной `self`. \ No newline at end of file diff --git a/1-js/6-objects-more/5-bind/5-question-use-bind/task.md b/1-js/6-objects-more/5-bind/5-question-use-bind/task.md new file mode 100644 index 00000000..be46ffc1 --- /dev/null +++ b/1-js/6-objects-more/5-bind/5-question-use-bind/task.md @@ -0,0 +1,50 @@ +# Использование функции вопросов + +[importance 5] + +Вызов `user.checkPassword()` в коде ниже должен, при помощи `ask`, спрашивать пароль и вызывать `loginOk/loginFail` в зависимости от правильности ответа. + +Однако, его вызов приводит к ошибке. Почему? + +Исправьте выделенную строку, чтобы всё работало (других строк изменять не надо). + +```js +//+ run +"use strict"; + +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} + +var user = { + login: 'Василий', + password: '12345', + + loginOk: function() { + alert(this.login + ' вошёл в сайт'); + }, + + loginFail: function() { + alert(this.login + ': ошибка входа'); + }, + + checkPassword: function() { +*!* + ask("Ваш пароль?", this.password, this.loginOk, this.loginFail); +*/!* + } +}; + +user.checkPassword(); +``` + +P.S. Ваше решение должно также срабатывать, если переменная `user` будет перезаписана, например вместо `user.checkPassword()` в конце будут строки: + +```js +var vasya = user; +user = null; +vasya.checkPassword(); +``` + diff --git a/1-js/6-objects-more/5-bind/6-ask-currying/solution.md b/1-js/6-objects-more/5-bind/6-ask-currying/solution.md new file mode 100644 index 00000000..3a1173f0 --- /dev/null +++ b/1-js/6-objects-more/5-bind/6-ask-currying/solution.md @@ -0,0 +1,69 @@ +# Решение с bind + +Первое решение -- передать в `ask` функции с привязанным контекстом и аргументами. + +```js +//+ run +"use strict"; + +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} + +var user = { + login: 'Василий', + password: '12345', + + loginDone: function(result) { + alert(this.login + (result ? ' вошёл в сайт' : ' ошибка входа')); + }, + + checkPassword: function() { +*!* + ask("Ваш пароль?", this.password, this.loginDone.bind(this, true), this.loginDone.bind(this, false)); +*/!* + } +}; + +user.checkPassword(); +``` + +# Решение с локальной переменной + +Второе решение -- это скопировать `this` в локальную переменную (чтобы внешняя перезапись не повлияла): + +```js +//+ run +"use strict"; + +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} + +var user = { + login: 'Василий', + password: '12345', + + loginDone: function(result) { + alert(this.login + (result ? ' вошёл в сайт' : ' ошибка входа')); + }, + + checkPassword: function() { + var self = this; +*!* + ask("Ваш пароль?", this.password, + function() { self.loginDone(true); }, + function() { self.loginDone(false); } + ); +*/!* + } +}; + +user.checkPassword(); +``` + +Оба решения хороши, вариант с `bind` короче. \ No newline at end of file diff --git a/1-js/6-objects-more/5-bind/6-ask-currying/task.md b/1-js/6-objects-more/5-bind/6-ask-currying/task.md new file mode 100644 index 00000000..245d91d8 --- /dev/null +++ b/1-js/6-objects-more/5-bind/6-ask-currying/task.md @@ -0,0 +1,53 @@ +# Использование функции вопросов с каррингом + +[importance 5] + +Эта задача -- усложнённый вариант задачи [](/task/question-use-bind). В ней объект `user` изменён. + +Теперь вместо двух функций `user.loginOk()` и `user.loginFail()` теперь один метод: `user.loginDone(true/false)`, который нужно вызвать с `true` при верном ответе и `fail` -- при неверном. + +Код ниже делает это, соответствующий фрагмент выделен. + +**Сейчас он обладает важным недостатком: при записи в `user` другого значения объект перестанет корректно работать, вы увидите это, запустив пример ниже (будет ошибка).** + +Как бы вы написали правильно? + +**Исправьте выделенный фрагмент, чтобы код заработал.** + +```js +//+ run +"use strict"; + +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} + +var user = { + login: 'Василий', + password: '12345', + + // метод для вызова из ask + loginDone: function(result) { + alert(this.login + (result ? ' вошёл в сайт' : ' ошибка входа')); + }, + + checkPassword: function() { +*!* + ask("Ваш пароль?", this.password, + function() { user.loginDone(true); }, + function() { user.loginDone(false); } + ); +*/!* + } +}; + +var vasya = user; +user = null; +vasya.checkPassword(); +``` + +Изменения должны касаться только выделенного фрагмента. + +Если возможно, предложите два решения, одно -- с использованием `bind`, другое -- без него. Какое решение лучше? diff --git a/1-js/6-objects-more/5-bind/article.md b/1-js/6-objects-more/5-bind/article.md new file mode 100644 index 00000000..29a23d13 --- /dev/null +++ b/1-js/6-objects-more/5-bind/article.md @@ -0,0 +1,308 @@ +# Привязка контекста и карринг: "bind" + +Функции в JavaScript никак не привязаны к своему контексту `this`, с одной стороны, здорово -- это позволяет быть максимально гибкими, одалживать методы и так далее. + +Но с другой стороны -- в некоторых случаях контекст может быть потерян. То есть мы вроде как вызываем метод объекта, а на самом деле он получает `this = undefined`. + +Такая ситуация является типичной для начинающих разработчиков, но бывает и у "зубров" то же. Конечно, зубры при этом знают, что с ней делать. + +[cut] + +## Пример потери контекста + +В браузере есть встроенная функция `setTimeout(func, ms)`, которая вызывает выполение функции `func` через `ms` миллисекунд (=1/1000 секунды). + +Мы подробно остановимся на ней и её тонкостях позже, в главе [](/setTimeout-setInterval), а пока просто посмотрим пример. + +Этот код выведет "Привет" через 1000мс, то есть 1 секунду: + +```js +//+ run +setTimeout(function() { + alert("Привет"); +}, 1000); +``` + +Попробуем сделать то же самое с методом объекта, следующий код должен выводить имя пользователя через 1 секунду: + +```js +//+ run +var user = { + firstName: "Вася", + sayHi: function() { + alert(this.firstName); + } +}; + +*!* +setTimeout( user.sayHi, 1000); // undefined (не Вася!) +*/!* +``` + +**При запуске кода выше через секунду выводится вовсе не `"Вася"`, а `undefined`!** + +Это произошло потому, что в примере выше `setTimeout` получил функцию `user.sayHi`, но не её контекст. То есть, последняя строчка аналогична двум таким: + +```js +var f = user.sayHi; +setTimeout(f, 1000); // контекст user потеряли +``` + +**Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно?** + +Есть несколько способов решения, среди которых мы, в зависимости от ситуации, можем выбирать. + +## Решение 1: сделать обёртку + +Самый простой вариант решения -- это обернуть вызов в анонимную функцию: + +```js +//+ run +var user = { + firstName: "Вася", + sayHi: function() { + alert(this.firstName); + } +}; + +*!* +setTimeout(function() { + user.sayHi(); // Вася +}, 1000); +*/!* +``` + +Теперь код работает, так как `user` достаётся из замыкания. + +Но тут же появляется и уязвимое место в структуре кода! + +**А что, если до срабатывания `setTimeout` в переменную `user` будет записано другое значение? К примеру, какой-то другой пользователь... В этом случае вызов неожиданно будет совсем не тот!** + +Хорошо бы гарантировать правильность контекста. + +## Решение 2: bind для привязки контекста + +Напишем вспомогательную функцию `bind(func, context)`, которая будет жёстко фиксировать контекст для `func`: + +```js +function bind(func, context) { + return function() { + return func.apply(context, arguments); + }; +} +``` + +Параметры: +
      +
      `func`
      +
      Произвольная функция
      +
      `context`
      +
      Произвольный объект
      + +Результатом вызова `bind(func, context)` будет, как видно из кода, функция-обёртка, которая передаёт все вызовы `func`, указывая при этом правильный контекст `context`. + +Чтобы понять, как она работает, нужно вспомнить тему "замыкания" -- здесь `bind` возвращает анонимную функцию, которая при вызове получает контекст `context` из внешней области видимости и передаёт вызов в `func` вместе с этим контекстом `context` и аргументами `arguments`. + +**В результате вызова `bind` мы получаем как бы "ту же функцию, но с фиксированным контекстом".** + +Пример с `bind`: + +```js +//+ run +function bind(func, context) { + return function() { + return func.apply(context, arguments); + }; +} + +var user = { + firstName: "Вася", + sayHi: function() { + alert(this.firstName); + } +}; + +*!* +setTimeout( bind(user.sayHi, user), 1000 ); +*/!* +``` + +Теперь всё в порядке! В `setTimeout` пошла обёртка, фиксирующая контекст. + + +## Решение 3: встроенный метод bind [#bind] + +В современном JavaScript (или при подключении библиотеки [es5-shim](https://github.com/kriskowal/es5-shim) для IE8-) у функций уже есть встроенный метод [bind](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind), который мы можем использовать. + +Он позволяет получить обёртку, которая привязывает функцию не только к нужному контексту но, если нужно, то и к аргументам. + +Синтаксис `bind`: + +```js +var wrapper = func.bind(context[, arg1, arg2...]) +``` + +
      +
      `func`
      +
      Произвольная функция
      +
      `context`
      +
      Обертка `wrapper` будет вызывать функцию с контекстом `this = context`.
      +
      `arg1`, `arg2`, ...
      +
      Если указаны аргументы `arg1, arg2...` -- они будут прибавлены к каждому вызову новой функции, причем встанут *перед* теми, которые указаны при вызове.
      +
      + +Результат вызова: `func.bind(context)` аналогичен вызову `bind(func, context)`, описанному выше. То есть, `wrapper` -- это обёртка, фиксирующая контекст и передающая вызовы в `func`. Также можно указать аргументы, но это делается редко, мы поговорим о них позже. + +Пример со встроенным методом `bind`: + +```js +//+ run +var user = { + firstName: "Вася", + sayHi: function() { + alert(this.firstName); + } +}; + +*!* +// setTimeout( bind(user.sayHi, user), 1000 ); + +setTimeout( user.sayHi.bind(user), 1000 ); // аналог через встроенный метод +*/!* +``` + +Получили простой и надёжный способ привязать контекст, причём даже встроенный в JavaScript. + +[smart header="Привязать всё: `bindAll`"] +Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле: + +```js +for(var prop in user) { + if (typeof user[prop] == 'function') { + user[prop] = user[prop].bind(user); + } +} +``` + +В некоторых JS-фреймворках есть даже встроенные функции для этого, например [_.bindAll(obj)](http://lodash.com/docs#bindAll). +[/smart] + +## Карринг + +До этого мы говорили о привязке контекста. Теперь пойдём на шаг дальше. Привязывать можно не только контекст, но и аргументы. Используется это реже, но бывает полезно. + +[Карринг](http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) (currying) или *каррирование* -- термин [функционального программирования](http://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), который означает создание новой функции путём фиксирования аргументов существующей. + +Как было сказано выше, метод `func.bind(context, ...)` может создавать обёртку, которая фиксирует не только контекст, но и ряд аргументов функции. + +Например, есть функция умножения двух чисел `mul(a, b)`: + +```js +function mul(a, b) { + return a * b; +}; +``` + +При помощи `bind` создадим функцию `double`, удваивающую значения. Это будет вариант функции `mul` с фиксированным первым аргументом: + +```js +//+ run +*!* +// double умножает только на два +var double = mul.bind(null, 2); // контекст фиксируем null, он не используется +*/!* + +alert( double(3) ); // = mul(2, 3) = 6 +alert( double(4) ); // = mul(2, 4) = 8 +alert( double(5) ); // = mul(2, 5) = 10 +``` + +При вызове `double` будет передавать свои аргументы исходной функции `mul` после тех, которые указаны в `bind`, то есть в данном случае после зафиксированного первого аргумента `2`. + +**Говорят, что `double` является "частичной функцией" (partial function) от `mul`.** + +Другая частичная функция `triple` утраивает значения: + +```js +//+ run +*!* +var triple = mul.bind(null, 3); // контекст фиксируем null, он не используется +*/!* + +alert( triple(3) ); // = mul(3, 3) = 9 +alert( triple(4) ); // = mul(3, 4) = 12 +alert( triple(5) ); // = mul(3, 5) = 15 +``` + +**При помощи `bind` мы можем получить из функции её "частный вариант" как самостоятельную функцию и дальше передать в `setTimeout` или сделать с ней что-то ещё.** + + +## Задачи + + +Рассмотрим для дальнейших задач "функцию для вопросов" `ask`: + +```js +function ask(question, answer, ok, fail) { + var result = prompt(question, ''); + if (result.toLowerCase() == answer.toLowerCase()) ok(); + else fail(); +} +``` + +Пока в этой функции ничего особого нет. Её назначение -- задать вопрос `question` и, если ответ совпадёт с `answer`, то запустить функцию `ok()`, а иначе -- функцию `fail()`. + +Однако, тем не менее, эта функция взята из реального проекта. Просто обычно она сложнее, вместо `alert/prompt` -- вывод красивого JavaScript-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно. + +Пример использования: + +```js +//+ run +*!* +ask("Выпустить птичку?", "да", fly, die); +*/!* + +function fly() { + alert('улетела :)'); +} + +function die() { + alert('птичку жалко :('); +} +``` + +## Итого + +Функции и контекст к JavaScript -- как шнурки и кроссовки. Если мы куда-то отправляем шнурки (например в `setTimeout`), то кроссовки сами за ними не побегут. + +Нужно либо передать их дополнительно, либо привязать одно к другому вызовом `bind`, либо завернуть в замыкание. + +[head] + +[/head] \ No newline at end of file diff --git a/1-js/6-objects-more/6-decorators/1-logging-decorator/_js.view/solution.js b/1-js/6-objects-more/6-decorators/1-logging-decorator/_js.view/solution.js new file mode 100644 index 00000000..22c190ed --- /dev/null +++ b/1-js/6-objects-more/6-decorators/1-logging-decorator/_js.view/solution.js @@ -0,0 +1,9 @@ +function makeLogging(f, log) { + + function wrapper(a) { + log.push(a); + return f.call(this, a); + } + + return wrapper; +} diff --git a/1-js/6-objects-more/6-decorators/1-logging-decorator/_js.view/test.js b/1-js/6-objects-more/6-decorators/1-logging-decorator/_js.view/test.js new file mode 100644 index 00000000..67480fdc --- /dev/null +++ b/1-js/6-objects-more/6-decorators/1-logging-decorator/_js.view/test.js @@ -0,0 +1,48 @@ +describe("makeLogging", function() { + it("записывает вызовы в массив log", function() { + var work = sinon.spy(); + + var log = []; + work = makeLogging(work, log); + assert.deepEqual( log, []); + + work(1); + assert.deepEqual( log, [1]); + + work(2); + assert.deepEqual( log, [1, 2]); + }); + + it("передаёт вызов функции, возвращает её результат", function() { + var log = []; + + function work(x) { + return x*2; + } + + work = sinon.spy(work); + var spy = work; + work = makeLogging(work, log); + + assert.equal( work(1), 2 ); + assert(spy.calledWith(1)); + }); + + + it("сохраняет контекст вызова для методов объекта", function() { + var log = []; + + var calculator = { + double: function(x) { return x*2; } + } + + calculator.double = sinon.spy(calculator.double); + var spy = calculator.double; + calculator.double = makeLogging(calculator.double, log); + + assert.equal( calculator.double(1), 2 ); + assert(spy.calledWith(1)); + assert(spy.calledOn(calculator)); + }); + +}); diff --git a/1-js/6-objects-more/6-decorators/1-logging-decorator/solution.md b/1-js/6-objects-more/6-decorators/1-logging-decorator/solution.md new file mode 100644 index 00000000..ac92d137 --- /dev/null +++ b/1-js/6-objects-more/6-decorators/1-logging-decorator/solution.md @@ -0,0 +1,44 @@ +Возвратим декоратор `wrapper` который будет записывать аргумент в `log` и передавать вызов в `f`: + +```js +//+ run +function work(a) { + /*...*/ // work - произвольная функция, один аргумент +} + +function makeLogging(f, log) { + +*!* + function wrapper(a) { + log.push(a); + return f.call(this, a); + } +*/!* + + return wrapper; +} + +var log = []; +work = makeLogging(work, log); + +work(1); // 1 +work(5); // 5 + +for(var i=0; i другое значение +``` + +Обратите внимание: проверка на наличие уже подсчитанного значения выглядит так: `if (x in cache)`. Менее универсально можно проверить так: `if (cache[x])`, это если мы точно знаем, что `cache[x]` никогда не будет `false`, `0` и т.п. + diff --git a/1-js/6-objects-more/6-decorators/3-caching-decorator/task.md b/1-js/6-objects-more/6-decorators/3-caching-decorator/task.md new file mode 100644 index 00000000..2f51f84c --- /dev/null +++ b/1-js/6-objects-more/6-decorators/3-caching-decorator/task.md @@ -0,0 +1,34 @@ +# Кеширующий декоратор + +[importance 5] + +Создайте декоратор `makeCaching(f)`, который берет функцию `f` и возвращает обертку, которая кеширует её результаты. + +**В этой задаче функция `f` имеет только один аргумент, и он является числом.** + +
        +
      1. При первом вызове обертки с определенным аргументом -- она вызывает `f` и запоминает значение.
      2. +
      3. При втором и последующих вызовах с тем же аргументом возвращается запомненное значение.
      4. +
      + +Должно работать так: + +```js +function f(x) { + return Math.random()*x; // random для удобства тестирования +} + +function makeCaching(f) { /* ваш код */ } + +f = makeCaching(f); + +var a, b; + +a = f(1); +b = f(1); +alert( a == b ); // true (значение закешировано) + +b = f(2); +alert( a == b ); // false, другой аргумент => другое значение +``` + diff --git a/1-js/6-objects-more/6-decorators/article.md b/1-js/6-objects-more/6-decorators/article.md new file mode 100644 index 00000000..746ac446 --- /dev/null +++ b/1-js/6-objects-more/6-decorators/article.md @@ -0,0 +1,205 @@ +# Функции-обёртки, декораторы + +JavaScript предоставляет удивительно гибкие возможности по работе с функциями: их можно передавать, в них можно записывать данные как в объекты, у них есть свои встроенные методы... + +Конечно, этим нужно уметь пользоваться. В этой главе, чтобы более глубоко понимать работу с функциями, мы рассмотрим создание функций-обёрток или, иначе говоря, "декораторов". +[cut] + +## Примеры декораторов + +Декоратор -- приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение. + +***Декоратор* получает функцию и возвращает обертку, которая делает что-то своё "вокруг" вызова основной функции.** + +### bind -- привязка контекста + +Один простой декоратор вы уже видели ранее -- это функция [bind](/bind): + +```js +function bind(func, context) { + return function() { + return func.apply(context, arguments); + }; +} +``` + +Вызов `bind(func, context)` возвращает обёртку, которая ставит `this` и передаёт основную работу функции `func`. + +### Декоратор -- измеритель времени + +Посмотрим немного более сложный декоратор, замеряющий время выполнения функции: + +**При помощи декоратора `timingDecorator` мы можем взять произвольную функцию и одним движением руки прикрутить к ней измеритель времени:** + +```js +//+ run +var timers = {} + +// прибавит время выполнения f к таймеру timers[timer] +function timingDecorator(f, timer) { + return function() { + var start = performance.now(); + + var result = f.apply(this, arguments); + + if (!timers[timer]) timers[timer] = 0; + timers[timer] += performance.now() - start; + + return result; + } +} + +// функция может быть произвольной, например такой: +function fibonacci(n) { + return (n > 2) ? fibonacci(n-1) + fibonacci(n-2) : 1; +} + +*!* +// использование: завернём fibonacci в декоратор +fibonacci = timingDecorator(fibonacci, "fibo"); +*/!* + +// неоднократные вызовы... +alert( fibonacci(10) ); // 55 +alert( fibonacci(20) ); // 6765 +// ... + +*!* +// в любой момент можно получить общее количество времени на вызовы +alert( timers.fibo + 'мс' ); +*/!* +``` + +Обратим внимание на ключевую строку декоратора`var result = f.apply(this, arguments)`. + +**Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию, так что изнутри `f` всё выглядит так, как будто это была вызвана она, а не декоратор.** + +### Декоратор для проверки типа + +В JavaScript, как правило, пренебрегают проверками типа. В функцию, которая должна получать число, может быть передана строка, булево значение или даже объект. + +Например: + +```js +function sum(a, b) { + return a + b; +} + +alert( sum(true, { name: "Вася", age: 35 }) ); // true[Object object] +``` + +Функция "как-то" отработала, но в реальной жизни такой вызов, скорее всего, будет следствием программной ошибки. Всё-таки `sum` предназначена для суммирования чисел, а не объектов. + +Многие языки программирования позволяют прямо в объявлении функции указать, какие типы данных имеют параметры. И это удобно, поскольку повышает надёжность кода. + +В JavaScript же проверку типов приходится делать дополнительным кодом в начале функции, который во-первых обычно лень писать, а во-вторых он увеличивает общий объем текста, тем самым ухудшая читаемость. + +**Декораторы способны упростить рутинные, повторяющиеся задачи, вынести их из кода функции.** + +Например, создадим декоратор, который принимает функцию и массив, который описывает для какого аргумента какую проверку типа применять: + +```js +//+ run +// вспомогательная функция для проверки на число +function checkNumber(value) { + return typeof value == 'number'; +} + +// декоратор, проверяющий типы для f +// второй аргумент checks - массив с функциями для проверки +function typeCheck(f, checks) { + return function() { + for(var i=0; i +function timingDecorator(f) { + return function() { + var d = new Date(); + var result = f.apply(this, arguments); + console.log("Функция заняла: " + (new Date - d) + "мс"); + return result; + } +} +function bind(func, context) { + return function() { + return func.apply(context, arguments); + }; +} + +[/head] \ No newline at end of file diff --git a/1-js/6-objects-more/index.md b/1-js/6-objects-more/index.md new file mode 100644 index 00000000..e021da19 --- /dev/null +++ b/1-js/6-objects-more/index.md @@ -0,0 +1,3 @@ +# Методы объектов и контекст вызова + +Начинаем изучать объектно-ориентированную разработку -- как работают объекты и функции, что такое контекст вызова и почему его значение нельзя предсказать. Как, всё же, гарантировать правильный контекст и многие другие, не самые простые, темы. \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/1-array-equals-string/solution.md b/1-js/7-js-misc/1-object-conversion/1-array-equals-string/solution.md new file mode 100644 index 00000000..a0c488b5 --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/1-array-equals-string/solution.md @@ -0,0 +1,15 @@ +Если с одной стороны -- объект, а с другой -- нет, то сначала приводится объект. + +В данном случае сравнение означает численное приведение. У массивов нет `valueOf`, поэтому вызывается `toString`, который возвращает список элементов через запятую. + +В данном случае, элемент только один - он и возвращается. Так что `['x']` становится `'x'`. Получилось `'x' == 'x'`, верно. + +P.S. +По той же причине верны равенства: + +```js +//+ run +alert( ['x','y'] == 'x,y' ); // true +alert( [] == '' ); // true +``` + diff --git a/1-js/7-js-misc/1-object-conversion/1-array-equals-string/task.md b/1-js/7-js-misc/1-object-conversion/1-array-equals-string/task.md new file mode 100644 index 00000000..190bbeb4 --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/1-array-equals-string/task.md @@ -0,0 +1,11 @@ +# ['x'] == 'x' + +[importance 5] + +Почему результат `true` ? + +```js +//+ run +alert( ['x'] == 'x' ); +``` + diff --git a/1-js/7-js-misc/1-object-conversion/2-tostring-valueof/solution.md b/1-js/7-js-misc/1-object-conversion/2-tostring-valueof/solution.md new file mode 100644 index 00000000..9b2108d0 --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/2-tostring-valueof/solution.md @@ -0,0 +1,10 @@ +# Первый alert(foo) + +Возвращает строковое представление объекта, используя `toString`, т.е. `"foo"`. + +# Второй alert(foo + 1) +Оператор `'+'` преобразует объект к примитиву, используя `valueOf`, так что результат: `3`. + +# Третий alert(foo + '3') + +То же самое, что и предыдущий случай, объект превращается в примитив `2`. Затем происходит сложение `2 + '3'`. Оператор `'+'` при сложении чего-либо со строкой приводит и второй операнд к строке, а затем применяет конкатенацию, так что результат -- строка `"23"`. \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/2-tostring-valueof/task.md b/1-js/7-js-misc/1-object-conversion/2-tostring-valueof/task.md new file mode 100644 index 00000000..2785680a --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/2-tostring-valueof/task.md @@ -0,0 +1,24 @@ +# Преобразование + +[importance 5] + +Объявлен объект с `toString` и `valueOf`. + +Какими будут результаты `alert`? + +```js +var foo = { + toString: function () { + return 'foo'; + }, + valueOf: function () { + return 2; + } +}; + +alert(foo); +alert(foo + 1); +alert(foo + "3"); +``` + +Подумайте, прежде чем ответить. \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/3-compare-empty-arrays/solution.md b/1-js/7-js-misc/1-object-conversion/3-compare-empty-arrays/solution.md new file mode 100644 index 00000000..ef69d31c --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/3-compare-empty-arrays/solution.md @@ -0,0 +1,34 @@ +# Ответ по первому равенству + +Два объекта равны только тогда, когда это один и тот же объект. + +В первом равенстве создаются два массива, это разные объекты, так что они неравны. + +# Ответ по второму равенству + +
        +
      1. Первым делом, обе части сравнения вычисляются. Справа находится `![]`. Логическое НЕ `'!'` преобразует аргумент к логическому типу. Массив является объектом, так что это `true`. Значит, правая часть становится `![] = !true = false`. Так что получили: + +```js +alert( [] == false ); +``` + +
      2. +
      3. Проверка равенства между объектом и примитивом вызывает численное преобразование объекта. + +У массива нет `valueOf`, сработает `toString` и преобразует массив в список элементов, то есть - в пустую строку: + +```js +alert( '' == false ); +``` + +
      4. +
      5. Сравнение различных типов вызывает численное преобразование слева и справа: + +```js +alert( 0 == 0 ); +``` + +Теперь результат очевиден. +
      6. +
      \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/3-compare-empty-arrays/task.md b/1-js/7-js-misc/1-object-conversion/3-compare-empty-arrays/task.md new file mode 100644 index 00000000..3883d1ef --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/3-compare-empty-arrays/task.md @@ -0,0 +1,13 @@ +# Почему [] == [] неверно, а [ ] == ![ ] верно? + +[importance 5] + +Почему первое равенство -- неверно, а второе -- верно? + +```js +//+ run +alert( [] == [] ); // false +alert( [] == ![] ); // true +``` + +Какие преобразования происходят при вычислении? \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/4-object-types-conversion-questions/solution.md b/1-js/7-js-misc/1-object-conversion/4-object-types-conversion-questions/solution.md new file mode 100644 index 00000000..863963ce --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/4-object-types-conversion-questions/solution.md @@ -0,0 +1,33 @@ + + +```js +new Date(0) - 0 = 0 // (1) +new Array(1)[0] + "" = "undefined" // (2) +({})[0]
 = undefined // (3) +[1] + 1 = "11" // (4) +[1,2] + [3,4] = "1,23,4" // (5) +[] + null + 1 = "null1" // (6) +[[0]][0][0] = 0 // (7) +({} + {}) = "[object Object][object Object]" // (8) +``` + +
        +
      1. `new Date(0)` -- дата, созданная по миллисекундам и соответствующая 0мс от 1 января 1970 года 00:00:00 UTC. Оператор минус `-` преобразует дату обратно в число миллисекунд, то есть в `0`.
      2. +
      3. `new Array(num)` при вызове с единственным аргументом-числом создаёт массив данной длины, без элементов. Поэтому его нулевой элемент равен `undefined`, при сложении со строкой получается строка `"undefined"`.
      4. +
      5. Фигурные скобки -- это создание пустого объекта, у него нет свойства `'0'`. Так что значением будет `undefined`. +Обратите внимание на внешние, круглые скобки. Если их убрать и запустить `{}[0]` в отладочной консоли браузера -- будет `0`, т.к. скобки `{}` будут восприняты как пустой блок кода, после которого идёт массив.
      6. +
      7. Массив преобразуется в строку `"1"`. Оператор `"+"` при сложении со строкой приводит второй аргумент к строке -- значит будет `"1" + "1" = "11"`.
      8. +
      9. Массивы приводятся к строке и складываются.
      10. +
      11. Массив преобразуется в пустую строку `"" + null + 1`, оператор `"+"` видит, что слева строка и преобразует `null` к строке, получается `"null" + 1`, и в итоге `"null1"`.
      12. +
      13. `[[0]]` -- это вложенный массив `[0]` внутри внешнего `[ ]`. Затем мы берём от него нулевой элемент, и потом еще раз. + +Если это непонятно, то посмотрите на такой пример: + +```js +alert( [1,[0],2][1] ); +``` + +Квадратные скобки после массива/объекта обозначают не другой массив, а взятие элемента. +
      14. +
      15. Каждый объект преобразуется к примитиву. У встроенных объектов `Object` нет подходящего `valueOf`, поэтому используется `toString`, так что складываются в итоге строковые представления объектов.
      16. +
      \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/4-object-types-conversion-questions/task.md b/1-js/7-js-misc/1-object-conversion/4-object-types-conversion-questions/task.md new file mode 100644 index 00000000..e85a68bc --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/4-object-types-conversion-questions/task.md @@ -0,0 +1,17 @@ +# Вопросник по преобразованиям, для объектов + +[importance 5] + +Подумайте, какой результат будет у выражений ниже. Когда закончите -- сверьтесь с решением. + +```js +new Date(0) - 0 +new Array(1)[0] + "" +({})[0]
 +[1] + 1 +[1,2] + [3,4] +[] + null + 1 +[[0]][0][0] +({} + {}) +``` + diff --git a/1-js/7-js-misc/1-object-conversion/5-sum-many-brackets/solution.md b/1-js/7-js-misc/1-object-conversion/5-sum-many-brackets/solution.md new file mode 100644 index 00000000..c670bd4d --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/5-sum-many-brackets/solution.md @@ -0,0 +1,59 @@ +# Подсказка + +Чтобы `sum(1)`, а также `sum(1)(2)` можно было вызвать новыми скобками -- результатом `sum` должна быть функция. + +Но эта функция также должна уметь превращаться в число. Для этого нужно дать ей соответствующий `valueOf`. А если мы хотим, чтобы и в строковом контексте она вела себя так же -- то `toString`. + +# Решение + +Функция, которая возвращается `sum`, должна накапливать значение при каждом вызове. + +Удобнее всего хранить его в замыкании, в переменной `currentSum`. Каждый вызов прибавляет к ней очередное значение: + +```js +//+ run +function sum(a) { + + var currentSum = a; + + function f(b) { + currentSum += b; + return f; + } + + f.toString = function() { return currentSum; }; + + return f; +} + +alert( sum(1)(2) ); // 3 +alert( sum(5)(-1)(2) ); // 6 +alert( sum(6)(-1)(-2)(-3) ); // 0 +alert( sum(0)(1)(2)(3)(4)(5) ); // 15 +``` + +При внимательном взгляде на решение легко заметить, что функция `sum` срабатывает только один раз. Она возвращает функцию `f`. + +Затем, при каждом запуске функция `f` добавляет параметр к сумме `currentSum`, хранящейся в замыкании, и возвращает сама себя. + +**В последней строчке `f` нет рекурсивного вызова.** + +Вот так была бы рекурсия: + +```js +function f(b) { + currentSum += b; + return f(); // <-- подвызов +} +``` + +А в нашем случае, мы просто возвращаем саму функцию, ничего не вызывая. + +```js +function f(b) { + currentSum += b; + return f; // <-- не вызывает сама себя, а возвращает ссылку на себя +} +``` + +Эта `f` используется при следующем вызове, опять возвратит себя, и так сколько нужно раз. Затем, при использовании в строчном или численном контексте -- сработает `toString`, который вернет текущую сумму `currentSum`. \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/5-sum-many-brackets/task.md b/1-js/7-js-misc/1-object-conversion/5-sum-many-brackets/task.md new file mode 100644 index 00000000..b220c704 --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/5-sum-many-brackets/task.md @@ -0,0 +1,17 @@ +# Сумма произвольного количества скобок + +[importance 2] + +Напишите функцию `sum`, которая будет работать так: + +```js +sum(1)(2) == 3; // 1 + 2 +sum(1)(2)(3) == 6; // 1 + 2 + 3 +sum(5)(-1)(2) == 6 +sum(6)(-1)(-2)(-3) == 0 +sum(0)(1)(2)(3)(4)(5) == 15 +``` + +Количество скобок может быть любым. + +Пример такой функции для двух аргументов -- есть в решении задачи [](/task/closure-sum). \ No newline at end of file diff --git a/1-js/7-js-misc/1-object-conversion/article.md b/1-js/7-js-misc/1-object-conversion/article.md new file mode 100644 index 00000000..b8c1e213 --- /dev/null +++ b/1-js/7-js-misc/1-object-conversion/article.md @@ -0,0 +1,239 @@ +# Преобразование объектов: toString и valueOf + +Ранее, в главе [](/types-conversion) мы рассматривали преобразование типов для примитивов. Теперь добавим в нашу картину мира объекты. + +Бывают операции, при которых объект должен быть преобразован в примитив. +[cut] +Например: + +
        +
      • Строковое преобразование -- если объект выводится через `alert(obj)`.
      • +
      • Численное преобразование -- при арифметических операциях, сравнении с примитивом.
      • +
      • Логическое преобразование -- при `if(obj)` и других логических операциях.
      • +
      + +Рассмотрим эти преобразования по очереди. + +## Логическое преобразование + +Проще всего -- с логическим преобразованием. + +**Любой объект в логическом контексте -- `true`, даже если это пустой массив `[]` или объект `{}`.** + +```js +//+ run +if ( {} && [] ) { + alert("Все объекты - true!"); // alert сработает +} +``` + +## Строковое преобразование + +Строковое преобразование проще всего увидеть, если вывести объект при помощи `alert`: + +```js +//+ run +var user = { + firstName: 'Василий' +}; + +alert(user); // [object Object] +``` + +**Как видно, содержимое объекта не вывелось. Это потому, что стандартным строковым представлением пользовательского объекта является строка `"[object Object]"`**. + +Такой вывод объекта не содержит интересной информации. Поэтому имеет смысл его поменять на что-то более полезное. + +**Если в объекте присутствует метод `toString`, который возвращает примитив, то он используется для преобразования.** + +```js +//+ run +var user = { + + firstName: 'Василий', + + *!*toString:*/!* function() { + return 'Пользователь ' + this.firstName; + } +}; + +alert( user ); // Пользователь Василий +``` + +[smart header="Результатом `toString` может быть любой примитив"] +Метод `toString` не обязан возвращать именно строку. + +Его результат может быть любого примитивного типа. Например, это может быть число, как в примере ниже: + +```js +//+ run +var obj = { + toString: function() { return 123; } +}; + +alert(obj); // 123 +``` + +Поэтому мы и называем его здесь *"строковое преобразование"*, а не "преобразование к строке". +[/smart] + +Все объекты, включая встроенные, имеют свои реализации метода `toString`, например: + +```js +//+ run +alert( [1,2] ); // toString для массивов выводит список элементов "1,2" +alert( new Date ); // toString для дат выводит дату в виде строки +alert( function() { } ); // toString для функции выводит её код +``` + +## Численное преобразование + +Для численного преобразования объекта используется метод `valueOf`, а если его нет -- то `toString`: + +```js +//+ run +var room = { + number: 777, + + valueOf: function() { return this.number; }, + toString: function() { return this.number; } +}; + +alert( +room ); // 777, *!*вызвался valueOf*/!* + +delete room.valueOf; // *!*valueOf удалён*/!* + +alert( +room ); // 777, *!*вызвался toString*/!* +``` + +Метод `valueOf` обязан возвращать примитивное значение, иначе его результат будет проигнорирован. При этом -- не обязательно числовое. + +[smart header="У большинства объектов нет `valueOf`"] +**У большинства встроенных объектов такого `valueOf` нет, поэтому численное и строковое преобразования для них работают одинаково.** + +Исключением является объект `Date`, который поддерживает оба типа преобразований: + +```js +//+ run +alert( new Date() ); // toString: Дата в виде читаемой строки +alert( +new Date() ); // valueOf: кол-во миллисекунд, прошедших с 01.01.1970 +``` + +[/smart] + +[smart header="Детали спецификации"] +Если посмотреть в стандарт, то в пункте [15.2.4.4](http://es5.github.com/x15.2.html#x15.2.4.4) говорится о том, что `valueOf` есть у любых объектов. Но он ничего не делает, просто возвращает сам объект (не-примитивное значение!), а потому игнорируется. +[/smart] + +## Две стадии преобразования + +Если необходимо, полученный из объекта примитив может быть преобразован дальше. + +Например, рассмотрим применение к объекту операции `==`: + +```js +//+ run +var obj = { + valueOf: function() { return 1; } +}; + +alert(obj == true); // true +``` + +Объект `obj` был сначала преобразован в примитив, используя численное преобразование, получилось `1 == true`. + +Далее, так как значения всё ещё разных типов, применяются правила преобразования примитивов, результат: `true`. + +То же самое -- при сложении с объектом при помощи `+`: + +```js +//+ run +var obj = { + valueOf: function() { return 1; } +}; + +alert(obj + "test"); // 1test +``` + +Или вот, для разности объектов: + +```js +//+ run +var a = { + valueOf: function() { return "1"; } +}; +var b = { + valueOf: function() { return "2"; } +}; + +alert(a - b); // "1" - "2" = -1 +``` + +[warn header="Исключение: `Date`"] +Обычно арифметические операторы и сравнения используют численное преобразование, но есть исключение: объект `Date`. + +Он преобразуется в примитив, используя строковое преобразование. С этим можно столкнуться в операторе `"+"`: + +```js +//+ run +// бинарный вариант, преобразование к примитиву +alert( new Date + "" ); // "строка даты" + +// унарный вариант, наравне с - * / и другими приводит к числу +alert( +new Date ); // число миллисекунд +``` + +Это исключение явно прописано в стандарте и является единственным в своём роде. +[/warn] + +[warn header="Как испугать Java-разработчика"] +В языке Java (это не JavaScript, другой язык, здесь приведён для примера) логические значения можно создавать, используя синтаксис `new Boolean(true/false)`, например `new Boolean(true)`. + +В JavaScript тоже есть подобная возможность, которая возвращает "объектную обёртку" для логического значения. + +Эта возможность давно существует лишь для совместимости, она и не используется на практике, поскольку приводит к странным результатам. Некоторые из них могут сильно удивить человека, не привыкшего к JavaScript, например: + +```js +//+ run +var value = new Boolean(false); +if ( value ) { + alert(true); // сработает! +} +``` + +Почему запустился `alert`? Ведь в `if` находится `false`... Проверим: + +```js +//+ run +var value = new Boolean(false); + +*!* +alert(value); // выводит false, все ок.. +*/!* + +if ( value ) { + alert(true); // ..но тогда почему выполняется alert в if ?!? +} +``` + +Дело в том, что `new Boolean` -- это не примитивное значение, а объект. Поэтому в логическом контексте он преобразуется к`true`, в результате работает первый пример. + +А второй пример вызывает `alert`, который преобразует объект к строке, и он становится `"false"`. + +**В JavaScript вызовы `new Boolean/String/Number` не используются, а используются простые вызовы соответствующих функций, они преобразуют значение в примитив нужного типа, например `Boolean(val) === !!val`.** +[/warn] + +## Итого + +
        +
      • В логическом контексте объект -- всегда `true`.
      • +
      • При строковом преобразовании объекта используется его метод `toString`. Он должен возвращать примитивное значение, причём не обязательно именно строку. +
      • +
      • Для численного преобразования используется метод `valueOf`, который также может возвратить любое примитивное значение. У большинства объектов `valueOf` не работает (возвращает сам объект и потому игнорируется), при этом для численного преобразования используется `toString`.
      • +
      + +Полный алгоритм преобразований есть в спецификации EcmaScript, смотрите пункты [11.8.5](http://es5.github.com/x11.html#x11.8.5), [11.9.3](http://es5.github.com/x11.html#x11.9.3), а также [9.1](http://es5.github.com/x9.html#x9.1) и [9.3](http://es5.github.com/x9.html#x9.3). + + + + diff --git a/1-js/7-js-misc/2-class-property/article.md b/1-js/7-js-misc/2-class-property/article.md new file mode 100644 index 00000000..fca95e60 --- /dev/null +++ b/1-js/7-js-misc/2-class-property/article.md @@ -0,0 +1,103 @@ +# Секретное свойство [[Class]] + +Для встроенных объектов есть одна "секретная" возможность узнать их тип, которая связана с методом `toString`. + +**Во всех встроенных объектах есть специальное свойство `[[Class]]`, в котором хранится информация о его типе или конструкторе.** + +Оно взято в квадратные скобки, так как это свойство -- внутреннее. Явно получить его нельзя, но можно прочитать его "в обход", воспользовавшись методом `toString` из `Object`. + +[cut] + +## Получение [[Class]] + +Вернёмся к примеру, который видели раньше: + +```js +//+ run +var obj = {}; +alert( obj ); // [object Object] +``` + +**В выводе стандартного `toString` для объектов внутри `[object ...]` указано как раз значение `[[Class]]`.** + +Для обычного объекта это как раз и есть `"Object"`, но если бы такой `toString` запустить для даты, то будет `[object Date]`, для массивов -- `[object Array]` и т.п. + +К сожалению или к счастью, но большинство встроенных объектов в JavaScript имеют свой собственный метод `toString`: для массивов он выводит элементы через запятую, для дат -- строчное представление и так далее. + +То есть, просто вызов `[1,2,3].toString()` вернёт нам `1,2,3` и никакой информации про `[[Class]]`. + +Поэтому для получения `[[Class]]` мы одолжим функцию `toString` у стандартного объекта и запустим её в контексте тех значений, для которых нужно получить тип. В этом нам поможет метод `call`: + +```js +//+ run +var toClass = {}.toString; // (1) + +var arr = [1,2]; +alert( toClass.call(arr) ); // (2) [object Array] + +var date = new Date; +alert( toClass.call(date) ); // [object Date] + +var type = toClass.call(date).slice(8, -1); // (3) +alert(type); // Date +``` + +Разберем происходящее более подробно. + +
        +
      1. Можно переписать эту строку в две: + +```js +var obj = {}; +var toClass = obj.toString; +``` + +Иначе говоря, мы создаём пустой объект `{}` и копируем ссылку на его метод `toString` в переменную `toClass`. + +**Для получения `[[Class]]` нужна именно внутренняя реализация `toString` стандартного объекта `Object`, другая не подойдёт.**
      2. +
      3. Вызываем скопированный метод в контексте нужного объекта `obj`. + +Мы могли бы поступить проще -- одолжить метод под другим названием: + +```js +//+ run +var arr = [1,2]; +arr.toClass = {}.toString; + +alert( arr.toClass() ); // [object Array] +``` + +...Но зачем копировать лишнее свойство в объект? Синтаксис `toClass.call(arr)` делает то же самое, поэтому используем его. +
      4. +
      5. Всё, класс получен. При желании можно убрать обёртку `[object ...]`, взяв подстроку вызовом `slice(8,-1)`.
      6. +
      + +Метод также можно использовать с примитивами: + +```js +//+ run +alert( {}.toString.call(123) ); // [object Number] +alert( {}.toString.call("строка") ); // [object String] +``` + +[warn header="Вызов `{}.toString` в консоли может выдать ошибку"] +При тестировании кода в консоли вы можете обнаружить, что если ввести в командную строку `{}.toString.call(...)` -- будет ошибка. С другой стороны, вызов `alert( {}.toString... )` -- работает. + +Эта ошибка возникает потому, что фигурные скобки `{ }` в основном потоке кода интерпретируются как блок. Интерпретатор читает `{}.toString.call(...)` так: + +```js +{ } // пустой блок кода +.toString.call(...) // а что это за точка в начале? не понимаю, ошибка! +``` + +Фигурные скобки считаются объектом, только если они находятся в контексте выражения. В частности, оборачивание в скобки `( {}.toString... )` тоже сработает нормально. +[/warn] + +## Итого + +
        +
      • Свойство `[[Class]]` позволяет получить тип для встроенных объектов. Далее мы будем рассматривать создание своих объектов через функцию-конструктор, с ними `[[Class]]` не работает.
      • +
      • Для доступа к `[[Class]]` используется `{}.toString.call(obj).slice(8, -1)`.
      • +
      + +Обычно в JavaScript используется утиная типизация. Свойство `[[Class]]` -- самое надёжное средство проверки типа встроенных объектов, но обычно утиной типизации вполне хватает. \ No newline at end of file diff --git a/1-js/7-js-misc/3-json/1-serialize-object/solution.md b/1-js/7-js-misc/3-json/1-serialize-object/solution.md new file mode 100644 index 00000000..ee40c197 --- /dev/null +++ b/1-js/7-js-misc/3-json/1-serialize-object/solution.md @@ -0,0 +1,12 @@ + + +```js +var leader = { + name: "Василий Иванович", + age: 35 +}; + +var leaderStr = JSON.stringify(leader); +leader = JSON.parse(leaderStr); +``` + diff --git a/1-js/7-js-misc/3-json/1-serialize-object/task.md b/1-js/7-js-misc/3-json/1-serialize-object/task.md new file mode 100644 index 00000000..b5b7ba1b --- /dev/null +++ b/1-js/7-js-misc/3-json/1-serialize-object/task.md @@ -0,0 +1,14 @@ +# Превратите объект в JSON + +[importance 3] + +Превратите объект `leader` из примера ниже в JSON: + +```js +var leader = { + name: "Василий Иванович", + age: 35 +}; +``` + +После этого прочитайте получившуюся строку обратно в объект. diff --git a/1-js/7-js-misc/3-json/2-serialize-object-circular/solution.md b/1-js/7-js-misc/3-json/2-serialize-object-circular/solution.md new file mode 100644 index 00000000..a5738a55 --- /dev/null +++ b/1-js/7-js-misc/3-json/2-serialize-object-circular/solution.md @@ -0,0 +1,63 @@ +# Ответ на первый вопрос + +Обычный вызов `JSON.stringify(team)` выдаст ошибку, так как объекты `leader` и `soldier` внутри структуры ссылаются друг на друга. + +Формат JSON не предусматривает средств для хранения ссылок. + +# Варианты решения + +Чтобы превращать такие структуры в JSON, обычно используются два подхода: + +
        +
      1. Добавить в `team` свой код `toJSON`: + +```js +team.toJSON = function() { + /* свой код, который может создавать копию объекта без круговых ссылок и передавать управление JSON.stringify */ +} +``` + +При этом, конечно, понадобится и своя функция чтения из JSON, которая будет восстанавливать объект, а затем дополнять его круговыми ссылками. +
      2. +
      3. Можно учесть возможную проблему в самой структуре, используя вместо ссылок `id`. Как правило, это несложно, ведь на сервере у данных тоже есть идентификаторы. + +Изменённая структура может выглядеть так: + +```js +var leader = { + id: 12, + name: "Василий Иванович" +}; + +var soldier = { + id: 51, + name: "Петька" +}; + +*!* +// поменяли прямую ссылку на ID +leader.soldierId = 51; +soldier.leaderId = 12; +*/!* + +var team = { + 12: leader, + 51: soldier +}; +``` + +..Но действительно ли это решение будет оптимальным? Использовать структуру стало сложнее, и вряд ли это изменение стоит делать лишь из-за JSON. Вот если есть другие преимущества, тогда можно подумать. +
      4. +
      + +Универсальный вариант подхода, описанного выше -- это использование особой реализации JSON, которая не входит в стандарт и поддерживает расширенный формат для поддержки ссылок. + +Она, к примеру, есть во фреймворке Dojo. + +При вызове `dojox.json.ref.toJson(team)` будет создано следующее строковое представление: + +```js +[{"name":"Василий Иванович","soldier":{"name":"Петька","leader":{"$ref":"#0"}}},{"$ref":"#0.soldier"}] +``` + +Метод разбора такой строки -- также свой: `dojox.json.ref.fromJson`. \ No newline at end of file diff --git a/1-js/7-js-misc/3-json/2-serialize-object-circular/task.md b/1-js/7-js-misc/3-json/2-serialize-object-circular/task.md new file mode 100644 index 00000000..8f470c47 --- /dev/null +++ b/1-js/7-js-misc/3-json/2-serialize-object-circular/task.md @@ -0,0 +1,26 @@ +# Превратите объекты со ссылками в JSON + +[importance 3] + +Превратите объект `team` из примера ниже в JSON: + +```js +var leader = { + name: "Василий Иванович" +}; + +var soldier = { + name: "Петька" +}; + +// эти объекты ссылаются друг на друга! +leader.soldier = soldier; +soldier.leader = leader; + +var team = [ leader, soldier ]; +``` + +
        +
      1. Может ли это сделать прямой вызов `JSON.stringify(team)`? Если нет, то почему?
      2. +
      3. Какой подход вы бы предложили для чтения и восстановления таких объектов?
      4. +
      \ No newline at end of file diff --git a/1-js/7-js-misc/3-json/article.md b/1-js/7-js-misc/3-json/article.md new file mode 100644 index 00000000..2b416cc1 --- /dev/null +++ b/1-js/7-js-misc/3-json/article.md @@ -0,0 +1,366 @@ +# Формат JSON, метод toJSON + +В этой главе мы рассмотрим работу с форматом [JSON](http://ru.wikipedia.org/wiki/JSON), который используется для представления объектов в виде строки. + +Это один из наиболее удобных форматов данных при взаимодействии с JavaScript. Если нужно с сервера взять объект с данными и передать на клиенте, то в качестве промежуточного формата -- для передачи по сети, почти всегда используют именно его. + +В современных браузерах есть замечательные методы, знание тонкостей которых делает операции с JSON простыми и комфортными. + +[cut] + +## Формат JSON + +Данные в формате JSON ([RFC 4627](http://tools.ietf.org/html/rfc4627)) представляют собой: +
        +
      • JavaScript-объекты `{ ... }` или
      • +
      • Массивы `[ ... ]` или
      • +
      • Значения одного из типов: +
          +
        • строки в двойных кавычках,
        • +
        • число,
        • +
        • логическое значение `true`/`false`,
        • +
        • `null`.
        • +
        +
      • +
      + +Почти все языки программирования имеют библиотеки для преобразования объектов в формат JSON. + +Основные методы для работы с JSON в JavaScript -- это: +
        +
      • `JSON.parse` -- читает объекты из строки в формате JSON.
      • +
      • `JSON.stringify` -- превращает объекты в строку в формате JSON, используется, когда нужно из JavaScript передать данные по сети.
      • +
      + +Далее мы разберём их подробнее. + +## Метод JSON.parse + +Вызов `JSON.parse(str)` превратит строку с данными в формате JSON в JavaScript-объект/массив/значение. + +Например: + +```js +//+ run +var numbers = "[0, 1, 2, 3]"; + +numbers = JSON.parse(numbers); + +alert( numbers[1] ); // 1 +``` + +Или так: + +```js +//+ run +var user = '{ "name": "Вася", "age": 35, "isAdmin": false, friends: [0,1,2,3] }'; + +user = JSON.parse(user); + +alert( user.friends[1] ); // 1 +``` + +Данные могут быть сколь угодно сложными, объекты и массивы могут включать в себя другие объекты и массивы. Главное чтобы они соответствовали формату. + +[warn header="JSON-объекты ≠ JavaScript-объекты"] +**Объекты в формате JSON похожи на обычные JavaScript-объекты, но отличаются от них более строгими требованиями к строкам -- они должны быть именно в двойных кавычках.** + +В частности, первые два свойства объекта ниже -- некорректны: + +```js +{ + *!*name*/!*: "Вася", // ошибка: ключ name без кавычек! + "surname": *!*'Петров'*/!*,// ошибка: одинарные кавычки у значения 'Петров'! + "age": 35 // .. а тут всё в порядке. + "isAdmin": false // и тут тоже всё ок +} +``` + +Кроме того, в формате JSON не поддерживаются комментарии. Он предназначен только для передачи данных. +[/warn] + +## Умный разбор: JSON.parse(str, reviver) + +Метод `JSON.parse` поддерживает и более сложные алгоритмы разбора. + +Например, мы получили с сервера объект с данными события `event`. + +Он выглядит так: + +```js +// title: название собятия, date: дата события +var str = '{"title":"Конференция","date":"2012-11-30T12:00:00.000Z"}'; +``` + +...И теперь нужно *восстановить* его, то есть превратить в JavaScript-объект. + +Попробуем вызвать для этого `JSON.parse`: + +```js +//+ run +var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}'; + +var event = JSON.parse(str); + +*!* +alert( event.date.getDate() ); // ошибка! +*/!* +``` + +...Увы, ошибка! + +Дело в том, что значением `event.date` является строка, а отнюдь не объект `Date`. Откуда методу `JSON.parse` знать, что нужно превратить строку именно в дату? + +**Для интеллектуального восстановления из строки у `JSON.parse(str, reviver)` есть второй параметр `reviver`, который является функцией `function (key, value)`.** + +Если она указана, то в процессе чтения объекта из строки `JSON.parse` передаёт ей по очереди все создаваемые пары ключ-значение и может возвратить либо преобразованное значение, либо `undefined`, если его нужно пропустить. + +В данном случае мы можем создать правило, что ключ `date` всегда означает дату: + +```js +//+ run +// дата в строке - в формате UTC +var str = '{"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}'; + +*!* +var event = JSON.parse(str, function(key, value) { + if (key == 'date') return new Date(value); + return value; +}); +*/!* + +alert( event.date.getDate() ); // теперь сработает! +``` + +Кстати, эта возможность работает и для вложенных объектов тоже: + +```js +//+ run +var schedule = '{ \ + "events": [ \ + {"title":"Конференция","date":"2014-11-30T12:00:00.000Z"}, \ + {"title":"День рождения","date":"2015-04-18T12:00:00.000Z"} \ + ]\ +}'; + +schedule = JSON.parse(schedule, function(key, value) { + if (key == 'date') return new Date(value); + return value; +}); + +*!* +alert( schedule.events[1].date.getDate() ); // сработает! +*/!* +``` + +## Сериализация, метод JSON.stringify + +Метод `JSON.stringify(value, replacer, space)` преобразует (*"сериализует"*) значение в JSON-строку. + +Он поддерживается во всех браузерах, включая IE8+. Для более IE7- рекомендуется библиотека [JSON-js](https://github.com/douglascrockford/JSON-js), которая добавляет аналогичную функциональность. + +Пример использования: + +```js +//+ run +var event = { + title: "Конференция", + date: "сегодня" +}; + +var str = JSON.stringify(event); +alert( str ); // {"title":"Конференция","date":"сегодня"} + +// Обратное преобразование. +event = JSON.parse(str); +``` + +**При сериализации объекта вызывается его метод `toJSON`.** + +Если такого метода нет -- перечисляются его свойства, кроме функций. + +Посмотрим это в примере посложнее: + +```js +//+ run +var room = { + number: 23, + occupy: function() { + alert(this.number); + } +}; + +event = { + title: "Конференция", + date: new Date(Date.UTC(2014, 0, 1)), + room: room +}; + +alert( JSON.stringify(event) ); +/* + { + "title":"Конференция", + "date":"2014-01-01T00:00:00.000Z", // (1) + "room": {"number":23} // (2) + } +*/ +``` + +Обратим внимание на два момента: +
        +
      1. Дата превратилась в строку. Это не случайно: у всех дат есть встроенный метод `toJSON`. Его результат в данном случае -- строка в таймзоне UTC.
      2. +
      3. У объекта `room` нет метода `toJSON`. Поэтому он сериализуется перечислением свойств. + +Мы, конечно, могли бы добавить такой метод, тогда в итог попал бы его результат: + +```js +//+ run +var room = { + number: 23, +*!* + toJSON: function() { + return this.number; + } +*/!* +}; + +alert( JSON.stringify(room) ); // 23 +``` + +
      4. +
      + +### Исключение свойств + +Попытаемся преобразовать в JSON объект, содержащий ссылку на DOM. + +Например: + +```js +//+ run +var user = { + name: "Вася", + age: 25, + window: window +}; + +*!* +alert( JSON.stringify(user) ); // ошибка! +// TypeError: Converting circular structure to JSON (текст из Chrome) +*/!* +``` + +Произошла ошибка! В чём же дело, неужели некоторые объекты запрещены? Как видно из текста ошибки -- дело совсем в другом. Глобальный объект `window` -- сложная структура с кучей встроенных свойств и круговыми ссылками, поэтому его преобразовать невозможно. Да и нужно ли? + +**Во втором параметре `JSON.stringify(value, replacer)` можно указать массив свойств, которые подлежат сериализации.** + +Например: + +```js +//+ run +var user = { + name: "Вася", + age: 25, + window: window +}; + +*!* +alert( JSON.stringify(user, ["name", "age"]) ); +// {"name":"Вася","age":25} +*/!* +``` + +Для более сложных ситуаций вторым параметром можно передать функцию `function(key, value)`, которая возвращает сериализованное `value` либо `undefined`, если его не нужно включать в результат: + +```js +//+ run +var user = { + name: "Вася", + age: 25, + window: window +}; + +*!* +var str = JSON.stringify(user, function(key, value) { + if (key == 'elem') return undefined; + return value; +} ); +*/!* + +alert(str); // {"name":"Вася","age":25} +``` + +В примере выше функция пропустит свойство с названием `elem`. Для остальных она просто возвращает значение, передавая его стандартному алгоритму. А могла бы и как-то обработать. + +**Функция `replacer` работает рекурсивно.** + +То есть, если объект содержит вложенные объекты, массивы и т.п., то все они пройдут через `replacer`. + +### Красивое форматирование + +В методе `JSON.stringify(value, replacer, space)` есть ещё третий параметр `space`. + +Если он является числом -- то уровни вложенности в JSON оформляются указанным количеством пробелов, если строкой -- вставляется эта строка. + +Например: + +```js +//+ run +var user = { + name: "Вася", + age: 25, + roles: {isAdmin: false, isEditor: true} +}; + +*!* +var str = JSON.stringify(user, "", 4); +*/!* + +alert(str); +/* Результат -- красиво сериализованный объект: +{ + "name": "Вася", + "age": 25, + "roles": { + "isAdmin": false, + "isEditor": true + } +} +*/ +``` + +## Итого + +
        +
      • JSON -- формат для представления объектов (и не только) в виде строки.
      • +
      • Методы [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) и [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) позволяют интеллектуально преобразовать объект в строку и обратно.
      • +
      • Для IE7- можно подключить библиотеку [json2](https://github.com/douglascrockford/JSON-js/blob/master/json2.js).
      • +
      + + + +[head] + +[/head] \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/1-output-numbers-100ms/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/1-output-numbers-100ms/solution.md new file mode 100644 index 00000000..f6e7fbcd --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/1-output-numbers-100ms/solution.md @@ -0,0 +1,17 @@ + + +```js +//+ run +function printNumbersInterval() { + var i = 1; + var timerId = setInterval(function() { + console.log(i); + if (i == 20) clearInterval(timerId); + i++; + }, 100); +} + +// вызов +printNumbersInterval(); +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/1-output-numbers-100ms/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/1-output-numbers-100ms/task.md new file mode 100644 index 00000000..dc2364f4 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/1-output-numbers-100ms/task.md @@ -0,0 +1,21 @@ +# Вывод чисел каждые 100мс + +[importance 5] + +Напишите функцию `printNumbersInterval()`, которая последовательно выводит в консоль числа от 1 до 20, с интервалом между числами 100мс. То есть, весь вывод должен занимать 2000мс, в течение которых каждые 100мс в консоли появляется очередное число. + +Нажмите на кнопку, открыв консоль, для демонстрации: + + + + +P.S. Функция должна использовать `setInterval`. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/2-output-numbers-100ms-settimeout/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/2-output-numbers-100ms-settimeout/solution.md new file mode 100644 index 00000000..bc23dd04 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/2-output-numbers-100ms-settimeout/solution.md @@ -0,0 +1,17 @@ + + +```js +//+ run +function printNumbersTimeout20_100() { + var i = 1; + var timerId = setTimeout(function go() { + console.log(i); + if (i < 20) setTimeout(go, 100); + i++; + }, 100); +} + +// вызов +printNumbersTimeout20_100(); +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/2-output-numbers-100ms-settimeout/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/2-output-numbers-100ms-settimeout/task.md new file mode 100644 index 00000000..8334753f --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/2-output-numbers-100ms-settimeout/task.md @@ -0,0 +1,5 @@ +# Вывод чисел каждые 100мс, через setTimeout + +[importance 5] + +Сделайте то же самое, что в задаче [](/task/output-numbers-100ms), но с использованием рекурсивного `setTimeout` вместо `setInterval`. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/3-highlight-tactics/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/3-highlight-tactics/solution.md new file mode 100644 index 00000000..13c70041 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/3-highlight-tactics/solution.md @@ -0,0 +1,5 @@ +**Нужно выбрать вариант 2, который гарантирует браузеру свободное время между выполнениями `highlight`.** + +Первый вариант может загрузить процессор на 100%, если `highlight` занимает время, близкое к 10мс или, тем более, большее чем 10мс, т.к. таймер не учитывает время выполнения функции. + +Что интересно, в обоих случаях браузер не будет выводить предупреждение о том, что скрипт занимает много времени. Но от 100% загрузки процессора возможны притормаживания других операций. В общем, это совсем не то, что мы хотим, поэтому вариант 2. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/3-highlight-tactics/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/3-highlight-tactics/task.md new file mode 100644 index 00000000..afe1d6fd --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/3-highlight-tactics/task.md @@ -0,0 +1,34 @@ +# Для подсветки setInterval или setTimeout? + +[importance 5] + +Стоит задача: реализовать подсветку синтаксиса в длинном коде при помощи JavaScript, для онлайн-редактора кода. Это требует сложных вычислений, особенно загружает процессор генерация дополнительных элементов страницы, визуально осуществляющих подсветку. + +Поэтому решаем обрабатывать не весь код сразу, что привело бы к зависанию скрипта, а разбить работу на части: подсвечивать по 20 строк раз в 10мс. + +Как мы знаем, есть два варианта реализации такой подсветки: + +
        +
      1. Через `setInterval`, с остановкой по окончании работы: + +```js +timer = setInterval(function() { + if (есть еще что подсветить) highlight(); + else clearInterval(timer); +}, 10); +``` + +
      2. +
      3. Через рекурсивный `setTimeout`: + +```js +setTimeout(function go() { + highlight(); + if (есть еще что подсветить) setTimeout(go, 10); +}, 10); +``` + +
      4. +
      + +Какой из них стоит использовать? Почему? \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/4-settimeout-result/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/4-settimeout-result/solution.md new file mode 100644 index 00000000..ea95474b --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/4-settimeout-result/solution.md @@ -0,0 +1,8 @@ +Ответы: +
        +
      • `alert` выведет `100000000`.
      • +
      • **3**, срабатывание будет после окончания работы `hardWork`.
      • +
      + + +Так будет потому, что вызов планируется на `100мс` от времени вызова `setTimeout`, но функция выполняется больше, чем `100мс`, поэтому к моменту ее окончания время уже подошло и отложенный вызов выполняется тут же. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/4-settimeout-result/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/4-settimeout-result/task.md new file mode 100644 index 00000000..cc6bf22e --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/4-settimeout-result/task.md @@ -0,0 +1,31 @@ +# Что выведет setTimeout? + +[importance 5] + +В коде ниже запланирован запуск `setTimeout`, а затем запущена тяжёлая функция `f`, выполнение которой занимает более долгое время, чем интервал до срабатывания таймера. + +Когда сработает `setTimeout`? Выберите нужный вариант: +
        +
      1. До выполнения `f`.
      2. +
      3. Во время выполнения `f`.
      4. +
      5. Сразу же по окончании `f`.
      6. +
      7. Через 100мс после окончания `f`.
      8. +
      + +Что выведет `alert` в коде ниже? + +```js +setTimeout(function() { + alert(i); +}, 100); + +var i; + +function hardWork() { + // время выполнения этого кода >100мс, сам код неважен + for(i=0; i<1e8; i++) hardWork[i%2] = i; +} + +hardWork(); +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/5-setinterval-result/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/5-setinterval-result/solution.md new file mode 100644 index 00000000..d45ba64f --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/5-setinterval-result/solution.md @@ -0,0 +1,37 @@ +Вызов `alert(i)` в `setTimeout` введет `100000001`. + +Можете проверить это запуском: + +```js +//+ run +var timer = setInterval(function() { + i++; +}, 10); + +setTimeout(function() { + clearInterval(timer); +*!* + alert(i); // (*) +*/!* +}, 50); + +var i; + +function f() { + // точное время выполнения не играет роли + // здесь оно заведомо больше 100мс + for(i=0; i<1e8; i++) f[i%2] = i; +} + +f(); +``` + +Правильный вариант срабатывания: **3** (сразу же по окончании `f` один раз). + +Планирование `setInterval` будет вызывать функцию каждые `10мс` после текущего времени. Но так как интерпретатор занят долгой функцией, то до конца ее работы никакого вызова не происходит. + +За время выполнения `f` может пройти время, на которое запланированы несколько вызовов `setInterval`, но в этом случае остается только один, т.е. накопления вызовов не происходит. Такова логика работы `setInterval`. + +После окончания текущего скрипта интерпретатор обращается к очереди запланированных вызовов, видит в ней `setInterval` и выполняет. А затем тут же выполняется `setTimeout`, очередь которого тут же подошла. + +Итого, как раз и видим, что `setInterval` выполнился ровно 1 раз по окончании работы функции. Такое поведение кросс-браузерно. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/5-setinterval-result/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/5-setinterval-result/task.md new file mode 100644 index 00000000..504dde53 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/5-setinterval-result/task.md @@ -0,0 +1,44 @@ +# Что выведет после setInterval? + +[importance 5] + +В коде ниже запускается `setInterval` каждые 10мс, и через 50мс запланирована его отмена. + +После этого запущена тяжёлая функция `f`, выполнение которой (мы точно знаем) потребует более 100мс. + +Сработает ли `setInterval`, как и когда? + +Варианты: +
        +
      1. Да, несколько раз, *в процессе* выполнения `f`.
      2. +
      3. Да, несколько раз, *сразу после* выполнения `f`.
      4. +
      5. Да, один раз, *сразу после* выполнения `f`.
      6. +
      7. Нет, не сработает.
      8. +
      9. Может быть по-разному, как повезёт.
      10. +
      + +Что выведет `alert` в строке `(*)`? + +```js +var i; +var timer = setInterval(function() { // планируем setInterval каждые 10мс + i++; +}, 10); + +setTimeout(function() { // через 50мс - отмена setInterval + clearInterval(timer); +*!* + alert(i); // (*) +*/!* +}, 50); + +// и запускаем тяжёлую функцию +function f() { + // точное время выполнения не играет роли + // здесь оно заведомо больше 100мс + for(i=0; i<1e8; i++) f[i%2] = i; +} + +f(); +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/6-who-runs-faster/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/6-who-runs-faster/solution.md new file mode 100644 index 00000000..7fe4a8fe --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/6-who-runs-faster/solution.md @@ -0,0 +1,55 @@ +Задача -- с небольшим "нюансом". + +Есть браузеры, в которых на время работы JavaScript таймер "застывает", например таков IE. В них количество шагов будет почти одинаковым, +-1. + +В других браузерах (Chrome) первый бегун будет быстрее. + +Создадим реальные объекты `Runner` и запустим их для проверки: + +```js +//+ run +function Runner() { + this.steps = 0; + + this.step = function() { + this.doSomethingHeavy(); + this.steps++; + }; + + function fib(n) { + return n <= 1 ? n : fib(n-1) + fib(n-2); + } + + this.doSomethingHeavy = function() { + for(var i=0; i<25; i++) { + this[i] = fib(i); + } + }; + +} + +var runner1 = new Runner(); +var runner2 = new Runner(); + +// запускаем бегунов +var t1 = setInterval(function() { + runner1.step(); +}, 15); + +var t2 = setTimeout(function go() { + runner2.step(); + t2 = setTimeout(go, 15); +}, 15); + +// кто сделает больше шагов? +setTimeout(function() { + clearInterval(t1); + clearTimeout(t2); + alert(runner1.steps); + alert(runner2.steps); +}, 5000); +``` + +Если бы в шаге `step()` не было вызова `doSomethingHeavy()`, то есть он бы не требовал времени, то количество шагов было бы почти равным. + +Но так как у нас шаг, всё же, что-то делает, и функция `doSomethingHeavy()` специально написана таким образом, что она требует (небольшого) времени, то первый бегун успеет сделать больше шагов. Ведь в `setTimeout` пауза `15` мс будет *между* шагами, а `setInterval` шагает равномерно, каждые `15` мс. Получается чаще. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/6-who-runs-faster/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/6-who-runs-faster/task.md new file mode 100644 index 00000000..f65075d8 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/6-who-runs-faster/task.md @@ -0,0 +1,35 @@ +# Кто быстрее? + +[importance 5] + +Есть два бегуна: + +```js +var runner1 = new Runner(); +var runner2 = new Runner(); +``` + +У каждого есть метод `step()`, который делает шаг, увеличивая свойство `steps`. + +Конкретный код метода `step()` не имеет значения, важно лишь что шаг делается не мгновенно, он требует небольшого времени. + +Если запустить первого бегуна через `setInterval`, а второго -- через вложенный `setTimeout` -- какой сделает больше шагов за 5 секунд? + +```js +// первый? +setInterval(function() { + runner1.step(); +}, 15); + +// или второй? +setTimeout(function go() { + runner2.step(); + setTimeout(go, 15); +}, 15); + +setTimeout(function() { + alert(runner1.steps); + alert(runner2.steps); +}, 5000); +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/_js.view/solution.js b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/_js.view/solution.js new file mode 100644 index 00000000..62271941 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/_js.view/solution.js @@ -0,0 +1,12 @@ +function delay(f, ms) { + + return function() { + var savedThis = this; + var savedArgs = arguments; + + setTimeout(function() { + f.apply(savedThis, savedArgs); + }, ms); + }; + +} \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/_js.view/test.js b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/_js.view/test.js new file mode 100644 index 00000000..457ccf7f --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/_js.view/test.js @@ -0,0 +1,45 @@ +describe("delay", function() { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + this.clock.restore(); + }); + + it("вызывает функцию через указанный таймаут", function() { + var start = Date.now(); + function f(x) { + assert.equal(Date.now() - start, 1000); + } + f = sinon.spy(f); + + var f1000 = delay(f, 1000); + f1000("test"); + this.clock.tick(2000); + assert(f.calledOnce, 'calledOnce check fails'); + }); + + it("передаёт аргументы и контекст", function() { + var start = Date.now(); + var user = { + sayHi: function(phrase, who) { + assert.equal(this, user); + assert.equal(phrase, "Привет"); + assert.equal(who, "Вася"); + assert.equal(Date.now() - start, 1500); + } + }; + + user.sayHi = sinon.spy(user.sayHi); + + var spy = user.sayHi; + user.sayHi = delay(user.sayHi, 1500); + + user.sayHi("Привет", "Вася"); + + this.clock.tick(2000); + + assert(spy.calledOnce, 'проверка calledOnce не сработала'); + }); +}); \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/solution.md new file mode 100644 index 00000000..d578ae3b --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/solution.md @@ -0,0 +1,46 @@ + + +```js +//+ run +function delay(f, ms) { + +*!* + return function() { + var savedThis = this; + var savedArgs = arguments; + + setTimeout(function() { + f.apply(savedThis, savedArgs); + }, ms); + }; +*/!* + +} + +function f(x) { + alert(x); +} + +var f1000 = delay(f, 1000); +var f1500 = delay(f, 1500); + +f1000("тест"); // выведет "тест" через 1000 миллисекунд +f1500("тест2"); // выведет "тест2" через 1500 миллисекунд +``` + +Обратим внимание на то, как работает обёртка: + +```js +return function() { + var savedThis = this; + var savedArgs = arguments; + + setTimeout(function() { + f.apply(savedThis , savedArgs); + }, ms); +}; +``` + +Именно обёртка возвращается декоратором `delay` и будет вызвана. Чтобы передать аргумент и контекст функции, вызываемой через `ms` миллисекунд, они копируются в локальные переменные `savedThis` и `savedArgs`. + +Это один из самых простых, и в то же время удобных способов передать что-либо в функцию, вызываемую через `setTimeout`. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/task.md new file mode 100644 index 00000000..f65ddb06 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/7-delay/task.md @@ -0,0 +1,23 @@ +# Функция-задержка + +[importance 5] + +**Напишите функцию `delay(f, ms)`, которая возвращает обёртку вокруг `f`, задерживающую вызов на `ms` миллисекунд.** + +Например: + +```js +function f(x) { + alert(x); +} + +var f1000 = delay(f, 1000); +var f1500 = delay(f, 1500); + +f1000("тест"); // выведет "тест" через 1000 миллисекунд +f1500("тест2"); // выведет "тест2" через 1500 миллисекунд +``` + +Иначе говоря, `f1000` -- это "задержанный на 1000мс" вызов `f`. + +В примере выше у функции только один аргумент, но `delay` должна быть универсальной: передавать любое количество аргументов и контекст `this`. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/_js.view/solution.js b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/_js.view/solution.js new file mode 100644 index 00000000..1993b2e4 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/_js.view/solution.js @@ -0,0 +1,17 @@ +function debounce(f, ms) { + + var state = null; + + var COOLDOWN = 1; + + return function() { + if (state) return; + + f.apply(this, arguments); + + state = COOLDOWN; + + setTimeout(function() { state = null }, ms); + } + +} diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/_js.view/test.js b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/_js.view/test.js new file mode 100644 index 00000000..857f4dca --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/_js.view/test.js @@ -0,0 +1,38 @@ +describe("debounce", function() { + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + this.clock.restore(); + }); + + it("вызывает функцию не чаще чем раз в ms миллисекунд", function() { + var log = ''; + function f(a) { log += a; } + + f = debounce(f, 1000); + + f(1); // выполнится сразу же + f(2); // игнор + + setTimeout(function() { f(3) }, 100); // игнор (рановато) + setTimeout(function() { f(4) }, 1100); // выполнится (таймаут прошёл) + setTimeout(function() { f(5) }, 1500); // игнор + + this.clock.tick(5000); + assert.equal(log, "14"); + }); + + it("сохраняет контекст вызова", function() { + var obj = { + f: function() { + assert.equal(this, obj); + } + }; + + obj.f = debounce(obj.f, 1000); + obj.f("test"); + }); + +}); \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/solution.md new file mode 100644 index 00000000..6c8ea156 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/solution.md @@ -0,0 +1,38 @@ + + +```js +//+ run +function debounce(f, ms) { + + var state = null; + + var COOLDOWN = 1; + + return function() { + if (state) return; + + f.apply(this, arguments); + + state = COOLDOWN; + + setTimeout(function() { state = null }, ms); + } + +} + +function f(x) { alert(x) } +var f = debounce(f, 1000); + +f(1); // 1, выполнится сразу же +f(2); // игнор + +setTimeout( function() { f(3) }, 100); // игнор (прошло только 100мс) +setTimeout( function() { f(4) }, 1100); // 4, выполнится +setTimeout( function() { f(5) }, 1500); // игнор +``` + +Вызов `debounce` возвращает функцию-обёртку. Все необходимые данные для неё хранятся в замыкании. + +При вызове ставится таймер и состояние `state` меняется на константу `COOLDOWN` ("в процессе охлаждения"). + +Последующие вызовы игнорируются, пока таймер не обнулит состояние. \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/task.md new file mode 100644 index 00000000..343ccecf --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/8-debounce/task.md @@ -0,0 +1,23 @@ +# Вызов не чаще чем в N миллисекунд + +[importance 5] + +**Напишите функцию `debounce(f, ms)`, которая возвращает обёртку, которая передаёт вызов `f` не чаще, чем раз в `ms` миллисекунд.** + +"Лишние" вызовы игнорируются. Все аргументы и контекст -- передаются. + +Например: + +```js +function f() { ... } + +var f = debounce(f, 1000); + +f(1); // выполнится сразу же +f(2); // игнор + +setTimeout( function() { f(3) }, 100); // игнор (прошло только 100мс) +setTimeout( function() { f(4) }, 1100); // выполнится +setTimeout( function() { f(5) }, 1500); // игнор +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/_js.view/solution.js b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/_js.view/solution.js new file mode 100644 index 00000000..7d01975d --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/_js.view/solution.js @@ -0,0 +1,29 @@ +function throttle(func, ms) { + + var isThrottled = false, + savedArgs, + savedThis; + + function wrapper() { + + if (isThrottled) { + savedArgs = arguments; + savedThis = this; + return; + } + + func.apply(this, arguments); + + isThrottled = true; + + setTimeout(function() { + isThrottled = false; + if (savedArgs) { + wrapper.apply(savedThis, savedArgs); + savedArgs = savedThis = null; + } + }, ms); + } + + return wrapper; +} diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/_js.view/test.js b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/_js.view/test.js new file mode 100644 index 00000000..c11a0f74 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/_js.view/test.js @@ -0,0 +1,44 @@ +describe("throttle(f, 1000)", function() { + var f1000; + var log = ""; + function f(a) { log += a; } + + before(function() { + f1000 = throttle(f, 1000); + this.clock = sinon.useFakeTimers(); + }); + + it("первый вызов срабатывает тут же", function() { + f1000(1); // такой вызов должен сработать тут же + assert.equal(log, "1"); + }); + + it("тормозит второе срабатывание до 1000мс", function() { + f1000(2); // (тормозим, не прошло 1000мс) + f1000(3); // (тормозим, не прошло 1000мс) + // через 1000 мс запланирован вызов с последним аргументом + + assert.equal(log, "1"); // пока что сработал только первый вызов + + this.clock.tick(1000); // прошло 1000мс времени + assert.equal(log, "13"); // log==13, т.к. сработал вызов f1000(3) + }); + + it("тормозит третье срабатывание до 1000мс после второго", function() { + this.clock.tick(100); + f1000(4); // (тормозим, с последнего вызова прошло 100мс - менее 1000мс) + this.clock.tick(100); + f1000(5); // (тормозим, с последнего вызова прошло 200мс - менее 1000мс) + this.clock.tick(700); + f1000(6); // (тормозим, с последнего вызова прошло 900мс - менее 1000мс) + + this.clock.tick(100); // сработал вызов с 6 + + assert.equal(log, "136"); + }); + + after(function() { + this.clock.restore(); + }); + +}); \ No newline at end of file diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/solution.md b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/solution.md new file mode 100644 index 00000000..6c078926 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/solution.md @@ -0,0 +1,42 @@ + + +```js +function throttle(func, ms) { + + var isThrottled = false, + savedArgs, + savedThis; + + function wrapper() { + + if (isThrottled) { // (2) + savedArgs = arguments; + savedThis = this; + return; + } + + func.apply(this, arguments); // (1) + + isThrottled = true; + + setTimeout(function() { + isThrottled = false; // (3) + if (savedArgs) { + wrapper.apply(savedThis, savedArgs); + savedArgs = savedThis = null; + } + }, ms); + } + + return wrapper; +} +``` + +Шаги работы этой функции: +
        +
      1. Декоратор `throttle` возвращает функцию-обёртку `wrapper`, которая при первом вызове запускает `func` и переходит в состояние "паузы" (`isThrottled = true`).
      2. +
      3. В этом состоянии все новые вызовы запоминаются в замыкании через `savedArgs/savedThis`. Обратим внимание, что и контекст вызова и аргументы для нас одинаково важны и запоминаются одновременно. Только зная и то и другое, можно воспроизвести вызов правильно.
      4. +
      5. Далее, когда пройдёт таймаут `ms` миллисекунд -- пауза будет снята, а `wrapper` -- запущен с последними аргументами и контекстом (если во время паузы были вызовы).
      6. +
      + +Шаг `(3)` запускает именно не саму функцию, а снова `wrapper`, так как необходимо не только выполнить `func`, но и снова поставить выполнение на паузу. Получается последовательность "вызов - пауза.. вызов - пауза .. вызов - пауза ...", каждое выполнение в обязательном порядке сопровождается паузой после него. Это удобно описывается рекурсией. diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/task.md b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/task.md new file mode 100644 index 00000000..6c762461 --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/9-throttle/task.md @@ -0,0 +1,50 @@ +# Тормозилка + +[importance 5] + +Напишите функцию `throttle(f, ms)` -- "тормозилку", которая возвращает обёртку, передающую вызов `f` не чаще, чем раз в `ms` миллисекунд. + +**У этой функции должно быть важное существенное отличие от `debounce`:** если игнорируемый вызов оказался последним, т.е. после него до окончания задержки ничего нет -- то он выполнится. + +Чтобы лучше понять, откуда взялось это требование, и как `throttle` должна работать -- разберём реальное применение, на которое и ориентирована эта задача. + +**Например, нужно обрабатывать передвижения мыши.** + +В JavaScript это делается функцией, которая будет запускаться при каждом микро-передвижении мыши и получать координаты курсора. По мере того, как мышь двигается, эта функция может запускаться очень часто, может быть 100 раз в секунду (каждые 10мс). + +**Функция обработки передвижения должна обновлять некую информацию на странице.** + +При этом обновление -- слишком "тяжёлый" процесс, чтобы делать его при каждом микро-передвижении. Имеет смысл делать его раз в 100мс, не чаще. + +Пусть функция, которая осуществляет это обновление по передвижению, называется `onmousemove`. + +Вызов `throttle(onmousemove, 100)`, по сути, предназначен для того, чтобы "притормаживать" обработку `onmousemove`. Технически, он должен возвращать обёртку, которая передаёт все вызовы `onmousemove`, но не чаще чем раз в 100мс. + +**При этом промежуточные движения можно игнорировать, но мышь в конце концов где-то остановится. И это последнее, итоговое положение мыши обязательно нужно обработать!** + +Визуально это даст следующую картину обработки перемещений мыши: +
        +
      1. Первое обновление произойдёт сразу (это важно, посетитель тут же видит реакцию на своё действие).
      2. +
      3. Дальше может быть много вызовов (микро-передвижений) с разными координатами, но пока не пройдёт 100мс -- ничего не будет.
      4. +
      5. По истечении 100мс -- опять обновление, с последними координатами. Промежуточные микро-передвижения игнорированы.
      6. +
      7. В конце концов мышь где-то остановится, обновление по окончании очередной паузы 100мс (иначе мы не знаем, последнее оно или нет) сработает с последними координатами.
      8. +
      + +Ещё раз заметим -- задача из реальной жизни, и в ней принципиально важно, что *последнее* передвижение обрабатывается. Пользователь должен увидеть, где остановил мышь. + +Пример использования: + +```js +var f = function(a) { console.log(a) }; + +// затормозить функцию до одного раза в 1000 мс +var f1000 = throttle(f, 1000); + +f1000(1); // выведет 1 +f1000(2); // (тормозим, не прошло 1000мс) +f1000(3); // (тормозим, не прошло 1000мс) + +// когда пройдёт 1000мс... +// выведет 3, промежуточное значение 2 игнорируется +``` + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/article.md b/1-js/7-js-misc/4-setTimeout-setInterval/article.md new file mode 100644 index 00000000..8e7ea42f --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/article.md @@ -0,0 +1,322 @@ +# setTimeout и setInterval + +Почти все реализации JavaScript имеют внутренний таймер-планировщик, который позволяет задавать вызов функции через заданный период времени. + +В частности, эта возможность поддерживается в браузерах и в сервере Node.JS. + +[cut] +## setTimeout + +Синтаксис: + +```js +var timerId = setTimeout(func/code, delay[, arg1, arg2...]) +``` + +Параметры: + +
      +
      `func/code`
      +
      Функция или строка кода для исполнения. +Строка поддерживается для совместимости, использовать её не рекомендуется.
      +
      `delay`
      +
      Задержка в милисекундах, 1000 милисекунд равны 1 секунде.
      +
      `arg1`, `arg2`...
      +
      Аргументы, которые нужно передать функции. Не поддерживаются в IE9-.
      +
      + +Исполнение функции произойдёт спустя время, указанное в параметре `delay`. + +Например, следующий код вызовет `func()` через одну секунду: + +```js +//+ run +function func() { + alert('Привет'); +} + +*!* +setTimeout(func, 1000); +*/!* +``` + +С передачей аргументов (не сработает в IE9-): + +```js +//+ run +function func(phrase, who) { + alert(phrase + ', ' + who); +} + +*!* +setTimeout(func, 1000, "Привет", "Вася"); // Привет, Вася +*/!* +``` + +**Если первый аргумент является строкой, то интерпретатор создаёт анонимную функцию из этой строки.** + +То есть такая запись тоже сработает: + +```js +//+ run +setTimeout("alert('Привет')", 1000); +``` + +**Однако, использование строк не рекомендуется, так как они могут вызвать проблемы при минимизации кода, и, вообще, сама возможность использовать строку сохраняется лишь для совместимости.** + +Вместо них используйте анонимные функции, вот так: + +```js +//+ run +setTimeout(function() { alert('Привет') }, 1000); +``` + +### Отмена исполнения clearTimeout + +Функция `setTimeout` возвращает числовой идентификатор таймера `timerId`, который можно использовать для отмены действия. + +Синтаксис: + +```js +var timerId = setTimeout(...); +clearTimeout(timerId); +``` + +В следующем примере мы ставим таймаут, а затем удаляем (передумали). В результате ничего не происходит. + +```js +//+ run +var timerId = setTimeout(function() { alert(1) }, 1000); +alert(timerId); // число - идентификатор таймера + +clearTimeout(timerId); +alert(timerId); // всё ещё число, оно не обнуляется после отмены +``` + +Как видно из `alert`, в браузере идентификатор таймера является обычным числом. Другие JavaScript-окружения, например Node.JS, могут возвращать объект таймера, с дополнительными методами. + +**Такие разночтения вполне соответствуют стандарту просто потому, что в спецификации JavaScript про таймеры нет ни слова.** + +Таймеры -- это надстройка над JavaScript, которая описана в [секции Timers](http://www.w3.org/TR/html5/webappapis.html#timers) стандарта HTML5 для браузеров и в [документации к Node.JS](http://nodejs.org/docs/latest/api/timers.html) -- для сервера. + +## setInterval + +Метод `setInterval` имеет синтаксис, аналогичный `setTimeout`. + +```js +var timerId = setInterval(func/code, delay[, arg1, arg2...]) +``` + +Смысл аргументов -- тот же самый. Но, в отличие от `setTimeout`, он запускает выполнение функции не один раз, а регулярно повторяет её через указанный интервал времени. Остановить исполнение можно вызовом `clearInterval(timerId)`. + +Следующий пример при запуске станет выводить сообщение каждые две секунды, пока не пройдёт 5 секунд: + +```js +//+ run +// начать повторы с интервалом 2 сек +var timerId = setInterval(function() { + alert("тик"); +}, 2000); + +// через 5 сек остановить повторы +setTimeout(function() { + clearInterval(timerId); + alert('стоп'); +}, 5000); +``` + +[smart header="Модальные окна замораживают время в Chrome/Opera/Safari"] +Что будет, если долго не жать `OK` на появившемся `alert`? Это зависит от браузера. + +В браузерах Chrome, Opera и Safari внутренний таймер "заморожен" во время показа `alert/confirm/prompt`. А вот в IE и Firefox внутренний таймер продолжит идти. + +Поэтому, если закрыть `alert` после небольшой паузы, то в Firefox/IE следующий `alert` будет показан сразу же (время подошло), а в Chrome/Opera/Safari -- только через 2 секунды после закрытия. +[/smart] + + +### Рекурсивный setTimeout + +Важная альтернатива `setInterval` -- рекурсивный `setTimeout`: + +```js +/** вместо: +var timerId = setInterval(function() { + alert("тик"); +}, 2000); +*/ + +var timerId = setTimeout(function tick() { + alert("тик"); +*!* + timerId = setTimeout(tick, 2000); +*/!* +}, 2000); +``` + +В коде выше следующее выполнение планируется сразу после окончания предыдущего. + +**Рекурсивный `setTimeout` -- более гибкий метод тайминга, чем `setInterval`, так как время до следующего выполнения можно запланировать по-разному, в зависимости от результатов текущего.** + +Например, у нас есть сервис, который в 5 секунд опрашивает сервер на предмет новых данных. В случае, если сервер перегружен, можно увеличивать интервал опроса до 10, 20, 60 секунд... А потом вернуть обратно, когда всё нормализуется. + +Если у нас регулярно проходят грузящие процессор задачи, то мы можем оценивать время, потраченное на их выполнение, и планировать следующий запуск раньше или позже. + +**Рекурсивный `setTimeout` гарантирует паузу между вызовами, `setInterval` -- нет.** + +Давайте сравним два кода. Первый использует `setInterval`: + +```js +var i = 1; +setInterval(function() { + func(i); +}, 100); +``` + +Второй использует рекурсивный `setTimeout`: + +```js +var i = 1; +setTimeout(function run() { + func(i); + setTimeout(run, 100); +}, 100); +``` + +При `setInterval` внутренний таймер будет срабатывать чётко каждые `100` мс и вызывать `func(i)`: + + + +Вы обратили внимание?... + +**Реальная пауза между вызовами `func` при `setInterval` меньше, чем указана в коде!** + +Это естественно, ведь время работы функции никак не учитывается, оно "съедает" часть интервала. + +Возможно и такое что `func` оказалась сложнее, чем мы рассчитывали и выполнялась дольше, чем 100мс. + +В этом случае интерпретатор будет ждать, пока функция завершится, затем проверит таймер и, если время вызова `setInterval` уже подошло (или прошло), то следующий вызов произойдёт *сразу же*. + +**Если функция и выполняется дольше, чем пауза `setInterval`, то вызовы будут происходить вообще без перерыва.** + +Исключением является IE, в котором таймер "застывает" во время выполнения JavaScript. + +А так будет выглядить картинка с рекурсивным `setTimeout`: + + + +**При рекурсивном `setTimeout` задержка всегда фиксирована и равна 100мс.** + +Это происходит потому, что каждый новый запуск планируется только после окончания текущего. + +[smart header="Управление памятью"] +Сборщик мусора в JavaScript не чистит функции, назначенные в таймерах, пока таймеры актуальны. + +При передаче функции в `setInterval/setTimeout` создаётся внутренняя ссылка на неё, через которую браузер её будет запускать, и которая препятствует удалению из памяти, даже если функция анонимна. + +```js +// Функция будет жить в памяти, пока не сработал (или не был очищен) таймер +setTimeout(function() {}, 100); +``` + +
        +
      • Для `setTimeout` -- внутренняя ссылка исчезнет после исполнения функции.
      • +
      • Для `setInterval` -- ссылка исчезнет при очистке таймера.
      • +
      + +**Так как функция также тянет за собой всё замыкание, то ставшие неактуальными, но не отменённые `setInterval` могут приводить к излишним тратам памяти.** +[/smart] + + +## Минимальная задержка таймера + +У браузерного таймера есть минимальная возможная задержка. Она меняется от примерно нуля до 4мс в современных браузерах. В более старых она может быть больше и достигать 15мс. + +По [стандарту](http://www.w3.org/TR/html5/webappapis.html#timers), минимальная задержка составляет 4мс. Так что нет разницы между `setTimeout(..,1)` и `setTimeout(..,4)`. + +Посмотреть минимальное разрешение "вживую" можно на следующем примере. + +**В примере ниже каждая полоска удлиняется вызовом `setInterval` с указанной на ней задержкой -- от 0мс (сверху) до 20мс (внизу).** + +Позапускайте его в различных браузерах. Вы заметите, что несколько первых полосок анимируются с одинаковой скоростью. Это как раз потому, что слишком маленькие задержки таймер не различает. + +[iframe border="1" src="setInterval-anim" link edit] + +[warn] +В Internet Explorer, нулевая задержка `setInterval(.., 0)` не сработает. Это касается именно `setInterval`, т.е. `setTimeout(.., 0)` работает нормально. +[/warn] + +## Реальная частота срабатывания + +В ряде ситуаций таймер будет срабатывать реже, чем обычно. Задержка между вызовами `setInterval(..., 4)` может быть не 4мс, а 30мс или даже 1000мс. + +
        +
      • Большинство браузеров (десктопных в первую очередь) продолжают выполнять `setTimeout/setInterval`, даже если вкладка неактивна. + +При этом ряд из них (Chrome, FF, IE10) снижают минимальную частоту таймера, до 1 раза в секунду. Получается, что в "фоновой" вкладке будет срабатывать таймер, но редко.
      • +
      • При работе от батареи, в ноутбуке -- браузеры тоже могут снижать частоту, чтобы реже выполнять код и экономить заряд батареи. Особенно этим известен IE. Снижение может достигать нескольких раз, в зависимости от настроек.
      • +
      • При слишком большой загрузке процессора JavaScript может не успевать обрабатывать таймеры вовремя. При этом некоторые запуски `setInterval` будут пропущены.
      • +
      + +**Вывод: на частоту 4мс стоит ориентироваться, но не стоит рассчитывать.** + +Посмотрим снижении частоты в действии на небольшом примере. + +При клике на кнопку ниже запускается `setInterval(..., 90)`, который выводит список интервалов времени между 25 последними срабатываниями таймера. Запустите его. Перейдите на другую вкладку и вернитесь. + +
      + + + + + + +Если ваш браузер увеличивает таймаут при фоновом выполнении вкладки, то вы увидите увеличенные интервалы, помеченные красным. + +Кроме того, вы заметите, что таймер не является идеально точным ;) + + +## Разбивка долгих скриптов + +Нулевой или небольшой таймаут также используют, чтобы разорвать поток выполнения "тяжелых" скриптов. + +Например, скрипт для подсветки синтаксиса должен проанализировать код, создать много цветных элементов для подсветки и добавить их в документ -- на большом файле это займёт много времени, браузер может даже подвиснуть, что неприемлемо. + +Для того, чтобы этого избежать, сложная задача разбивается на части, выполнение каждой части запускается через мини-интервал после предыдущей, чтобы дать браузеру время. + +Например, осуществляется анализ и подсветка первых 100 строк, затем через 20 мс -- следующие 100 строк и так далее. При этом можно подстраиваться под CPU посетителя: замерять время на анализ 100 строк и, если процессор хороший, то в следующий раз обработать 200 строк, а если плохой -- то 50. В итоге подсветка будет работать с адекватной быстротой и без тормозов на любых текстах и компьютерах. + +## Итого + +
        +
      • Методы `setInterval(func, delay)` и `setTimeout(func, delay)` позволяют запускать `func` регулярно/один раз через `delay` миллисекунд.
      • +
      • Оба метода возвращают идентификатор таймера. Его используют для остановки выполнения вызовом `clearInterval/clearTimeout`.
      • +
      • В случаях, когда нужно гарантировать задержку между регулярными вызовами или гибко её менять, вместо `setInterval` используют рекурсивный `setTimeout`.
      • +
      • Минимальная задержка по стандарту составляет `4мс`. Браузеры соблюдают этот стандарт, но некоторые другие среды для выполнения JS, например Node.JS, могут предоставить и меньше задержки.
      • +
      • В реальности срабатывания таймера могут быть гораздо реже, чем назначено, например если процессор перегружен, вкладка находится в фоновом режиме, ноутбук работает от батареи или по какой-то иной причине.
      • + +Браузерных особенностей почти нет, разве что вызов `setInterval(..., 0)` с нулевой задержкой в IE недопустим, нужно указывать `setInterval(..., 1)`. + + + + + +## Полезные декораторы + +Эти декораторы часто востребованы в реальных проектах. Постарайтесь сделать их без подсказок. + + + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/interval1.png b/1-js/7-js-misc/4-setTimeout-setInterval/interval1.png new file mode 100755 index 00000000..2c60a630 Binary files /dev/null and b/1-js/7-js-misc/4-setTimeout-setInterval/interval1.png differ diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/setInterval-anim.view/index.html b/1-js/7-js-misc/4-setTimeout-setInterval/setInterval-anim.view/index.html new file mode 100755 index 00000000..8506c73d --- /dev/null +++ b/1-js/7-js-misc/4-setTimeout-setInterval/setInterval-anim.view/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/1-js/7-js-misc/4-setTimeout-setInterval/timeout.png b/1-js/7-js-misc/4-setTimeout-setInterval/timeout.png new file mode 100755 index 00000000..2ab19038 Binary files /dev/null and b/1-js/7-js-misc/4-setTimeout-setInterval/timeout.png differ diff --git a/1-js/7-js-misc/5-eval/1-eval-calculator/solution.md b/1-js/7-js-misc/5-eval/1-eval-calculator/solution.md new file mode 100644 index 00000000..aed1c355 --- /dev/null +++ b/1-js/7-js-misc/5-eval/1-eval-calculator/solution.md @@ -0,0 +1,12 @@ +Вычислить любое выражение нам поможет `eval`: + +```js +//+ demo run +var expr = prompt("Введите выражение?", '2*3+2'); + +alert(eval(expr)); +``` + +При этом посетитель потенциально может делать все, что угодно. + +Чтобы ограничить выражения только математикой, вводимую строку нужно проверять при помощи [регулярных выражений](/regular-expressions-javascript) на наличие любых символов, кроме букв, пробелов и знаков пунктуации. diff --git a/1-js/7-js-misc/5-eval/1-eval-calculator/task.md b/1-js/7-js-misc/5-eval/1-eval-calculator/task.md new file mode 100644 index 00000000..771b005f --- /dev/null +++ b/1-js/7-js-misc/5-eval/1-eval-calculator/task.md @@ -0,0 +1,9 @@ +# Eval-калькулятор + +[importance 4] + +Напишите интерфейс, который принимает математическое выражение (`prompt`) и возвращает его результат. + +Проверять выражение на корректность не требуется. + +[demo /] diff --git a/1-js/7-js-misc/5-eval/article.md b/1-js/7-js-misc/5-eval/article.md new file mode 100644 index 00000000..d2c961fa --- /dev/null +++ b/1-js/7-js-misc/5-eval/article.md @@ -0,0 +1,265 @@ +# Запуск кода из строки: eval + +Функция `eval(code)` позволяет выполнить код, переданный ей в виде строки. + +Этот код будет выполнен в *текущей области видимости*. +[cut] + +## Использование eval + +В простейшем случае `eval` всего лишь выполняет код, например: + +```js +//+ run +var a = 1; + +(function() { + + var a = 2; + +*!* + eval(' alert(a) '); // 2 +*/!* + +})() +``` + +Но он может не только выполнить код, но и вернуть результат. + +**Вызов `eval` возвращает последнее вычисленное выражение**: + +Например: + +```js +//+ run +alert( eval('1+1') ); // 2 +``` + +**При вызове `eval` имеет полный доступ к локальным переменным.** + +Это означает, что текущие переменные могут быть изменены или дополнены: + +```js +//+ untrusted refresh run +var x = 5; +eval(" alert(x); x = 10"); // 5, доступ к старому значению +alert(x); // 10, значение изменено внутри eval +``` + +[smart header="В строгом режиме `eval` имеет свою область видимости "] +В строгом режиме функционал `eval` чуть-чуть меняется. + +При `use strict` код внутри `eval` по-прежнему сможет читать и менять внешние переменные, однако переменные и функции, объявленные внутри `eval`, не попадут наружу. + +```js +//+ untrusted refresh run +"use strict"; + +*!* +eval("var a = 5; function f() { }"); +*/!* +alert(a); // ошибка, переменная не определена +// функция f тоже не видна снаружи +``` + +Иными словами, в новом стандарте `eval` имеет свою область видимости, а к внешним переменным обращается через замыкание, аналогично тому, как работают обычные функции. +[/smart] + +## Грамотное использование eval + +Начнём с того, что `eval` применяется очень редко. Действительно редко. Есть даже такое выражение "eval is evil" (eval -- зло). + +Причина проста: когда-то JavaScript был гораздо более слабым языком, чем сейчас и некоторые вещи без `eval` было сделать невозможно. Но те времена давно прошли. И теперь найти тот случай, когда действительно надо выполнить код из строки -- это надо постараться. + +Но если вы действительно знаете, что это именно тот случай и вам необходим `eval` -- есть ряд вещей, которые нужно иметь в виду. + +**Доступ к локальным переменным -- самое страшное зло `eval`.** + +Дело в том, что локальные переменные могут быть легко переименованы: + +```js +function sayHi() { + var phrase = "Привет"; + eval(str); +} +``` + +Переменная `phrase` может быть переименована в `hello`, и если строка `str` обращается к ней -- будет ошибка. + +**Современные средства сжатия JavaScript переименовывают локальные переменные автоматически.** + +Это считается безопасным, так как локальная переменная видна лишь внутри функции и если в ней везде поменять `phrase` на `p`, то никто этого не заметит. + +До сжатия: + +```js +function sayHi() { + var phrase = "Привет"; + alert(phrase); +} +``` + +После сжатия: + +```js +function sayHi() { + var a = "Привет"; + alert(a); +} +``` + +На самом деле всё ещё проще -- в данном случае утилита сжатия автоматически уберёт переменную `a` и код станет таким: + +```js +function sayHi() { + alert("Привет"); +} +``` + +**Теперь очевидно, что если где-то в функции есть `eval`, то любое его взаимодействие с локальными переменными будет нарушено с непредсказуемыми побочными эффектами.** + +Некоторые инструменты сжатия предупреждают, когда видят `eval` или стараются вообще не сжимать такой код вместе с его внешними функциями, но всё это борьба с последствиями кривого кода. + +### Запуск скрипта в глобальной области + +Ок, взаимодействовать с локальными переменными нельзя. + +Но допустим мы загрузили с сервера или вручную сгенерировали скрипт, который нужно выполнить в глобальной области, вне любых функций. + +**Есть два трюка для выполнения кода в глобальной области.** + +
          +
        1. Везде, кроме IE8-, достаточно вызвать `eval` не напрямую, а через `window.eval`. + +Вот так: + +```js +//+ run +var a = 1; + +(function() { + + var a = 2; +*!* + window.eval(' alert(a) '); // 1, выполнено глобально везде, кроме IE8- +*/!* +})(); +``` + +
        2. +
        3. В IE8- можно применить нестандартную фунцию [execScript](http://msdn.microsoft.com/en-us/library/ie/ms536420%28v=vs.85%29.aspx). Она, как и `eval`, выполняет код, но всегда в глобальной области видимости и не возвращает значение.
        4. +
        + +Оба способа можно объединить в единой функции `globalEval(code)`, выполняющей код без доступа к локальным переменным: + +```js +//+ run +*!* +function globalEval(code) { // объединим два способа в одну функцию + window.execScript ? execScript(code) : window.eval(code); +} +*/!* + +var a = 1; + +(function() { + + var a = 2; + + globalEval(' alert(a) '); // 1, во всех браузерах + +})(); +``` + +### Взаимодействие с внешним кодом, new Function + +Бывает, что в код, выполняемый при помощи `eval`, всё же нужно передать какие-то значения. + + Считать их из локальных переменных нельзя: как мы видели, это подвержено ошибкам при переименовании переменных и сразу ломается при сжатии JavaScript. + +**К счастью, существует отличная альтернатива `eval`, которая позволяет корректно взаимодействовать c внешним кодом: `new Function`.** + +Вызов `new Function('a,b', '..тело..')` создает функцию с указанными аргументами `a,b` и телом. Как мы помним, доступа к текущему замыканию у такой функции не будет, но можно передать параметры и получить результат. + +Например: + +```js +//+ run +var a = 2, b = 3; + +*!* +// вместо обращения к a,b через eval +// будем принимать их как аргументы динамически созданной функции +var mul = new Function('a, b', ' return a * b;'); +*/!* + +alert( mul(a, b) ); // 6 +``` + +## JSON и eval + +В браузерах IE7- не было методов `JSON.stringify` и `JSON.parse`, поэтому работа с JSON происходила через `eval`. + +Вызов `eval(code)` выполняет код и, если это выражение, то возвращает его значение, поэтому можно в качестве кода передать JSON. + +Например: + +```js +//+ run +var str= '{ \ + "name": "Вася", \ + "age": 25 \ +}'; + +*!* +var user = eval('(' + str + ')'); +*/!* + +alert(user.name); // Вася +``` + +Зачем здесь нужны скобки `eval( '(' + str + ')' )`, почему не просто `eval(str)`? + +...Всё дело в том, что в JavaScript с фигурной скобки `{` начинаются не только объекты, а в том числе и "блоки кода". Что имеется в виду в данном случае -- интерпретатор определяет по контексту. Если в основном потоке кода -- то блок, если в контексте выражения, то объект. + +Поэтому если передать в `eval` объект напрямую, то интерпретатор подумает, что это на самом деле блок кода, а там внутри какие-то двоеточия... + +Вот, для примера, `eval` без скобок, он выдаст ошибку: + +```js +//+ run +var user = eval( '{ "name": "Вася", "age": 25 }' ); +``` + +А если `eval` получает выражение в скобках `( ... )`, то интерпретатор точно знает, что это не блок кода, а объект: + +```js +//+ run +var user = eval( '( { "name": "Вася", "age": 25 } )' ); +alert(user.age); // 25 +``` + +[warn header="Осторожно, злой JSON!"] +Если мы получаем JSON из недоверенного источника, например с чужого сервера, то разбор через `eval` может быть опасен. + +Например, чужой сервер может быть взломан (за свой-то код мы отвечаем, а за чужой -- нет) и вместо JSON вставлен злонамеренный JavaScript-код. + +**Поэтому рекомендуется, всё же, использовать `JSON.parse`.** + +При разборе через `JSON.parse` некорректный JSON просто приведёт к ошибке, а вот при разборе через `eval` этот код реально выполнится, он может вывести что-то на странице, перенаправить посетителя куда-то и т.п. + +Если вам важна поддержка IE7, в котором `JSON.parse нет`, то от злого кода можно защититься проверкой текста при помощи регулярного выражения из [стандарта RFC 4627, секция 6](https://www.ietf.org/rfc/rfc4627.txt) или подключить библиотеку [json2](https://github.com/douglascrockford/JSON-js/blob/master/json2.js). +[/warn] + + +## Итого + +
          +
        • Функция `eval(str)` выполняет код и возвращает последнее вычисленное выражение. В современном JavaScript она используется редко.
        • +
        • Вызов `eval` может читать и менять локальные переменные. Это -- зло, которого нужно избегать.
        • +
        • Для выполнения скрипта в глобальной области используются трюк с `window.eval/execScript`. При этом локальные переменные не будут затронуты, так что такое выполнение безопасно и иногда, в редких архитектурах, может быть полезным.
        • +
        • Если нужно выполняемый код всё же должен взаимодействовать с локальными переменными -- используйте `new Function`. Создавайте функцию из строки и передавайте переменные ей, это надёжно и безопасно.
        • +
        + +Ещё примеры использования `eval` вы найдёте далее, в главе [](/json). + diff --git a/1-js/7-js-misc/6-exception/1-finally-ili-prosto-kod/solution.md b/1-js/7-js-misc/6-exception/1-finally-ili-prosto-kod/solution.md new file mode 100644 index 00000000..d976cbee --- /dev/null +++ b/1-js/7-js-misc/6-exception/1-finally-ili-prosto-kod/solution.md @@ -0,0 +1,43 @@ +Разница в поведении станет очевидной, если рассмотреть код внутри функции. + +Поведение будет различным, если управление каким-то образом выпрыгнет из `try..catch`. + +Например, `finally` сработает после `return`: + +```js +function f() { + try { + ... +*!* + return result; +*/!* + } catch(e) { + ... + } finally { + очистить ресурсы + } +} +``` + +Или же управление может выпрыгнуть из-за `throw`: + +```js +function f() { + try { + ... + + } catch(e) { + ... + if (не умею обрабатывать эту ошибку) { +*!* + throw e; +*/!* + } + + } finally { + очистить ресурсы + } +} +``` + +В этих случаях именно `finally` гарантирует выполнение кода до окончания работы `f`, просто код не будет вызван. \ No newline at end of file diff --git a/1-js/7-js-misc/6-exception/1-finally-ili-prosto-kod/task.md b/1-js/7-js-misc/6-exception/1-finally-ili-prosto-kod/task.md new file mode 100644 index 00000000..29b0cbe6 --- /dev/null +++ b/1-js/7-js-misc/6-exception/1-finally-ili-prosto-kod/task.md @@ -0,0 +1,43 @@ +# Finally или просто код? + +[importance 5] + +Сравните два фрагмента кода. + +
          +
        1. Первый использует `finally` для выполнения кода по выходу из `try..catch`: + +```js +try { + начать работу + работать +} catch(e) { + обработать ошибку +} finally { +*!* + финализация: завершить работу +*/!* +} +``` + +
        2. +
        3. Второй фрагмент просто ставит очистку ресурсов за `try..catch`: + +```js +try { + начать работу +} catch(e) { + обработать ошибку +} + +*!* +финализация: завершить работу +*/!* +``` + +
        4. +
        + +Нужно, чтобы код финализации всегда выполнялся при выходе из блока `try..catch` и, таким образом, заканчивал начатую работу. Имеет ли здесь `finally` какое-то преимущество или оба фрагмента работают одинаково? + +Если имеет, то дайте пример когда код с `finally` работает верно, а без -- неверно. diff --git a/1-js/7-js-misc/6-exception/2-eval-calculator-errors/solution.md b/1-js/7-js-misc/6-exception/2-eval-calculator-errors/solution.md new file mode 100644 index 00000000..7fda9e72 --- /dev/null +++ b/1-js/7-js-misc/6-exception/2-eval-calculator-errors/solution.md @@ -0,0 +1,36 @@ +Вычислить любое выражение нам поможет `eval`: + +```js +//+ run +alert( eval("2+2") ); // 4 +``` + +Считываем выражение в цикле `while(true)`. Если при вычислении возникает ошибка -- ловим её в `try..catch`. + +Ошибкой считается, в том числе, получение `NaN` из `eval`, хотя при этом исключение не возникает. Можно бросить своё исключение в этом случае. + +Код решения: + +```js +//+ run demo +var expr, res; + +while(true) { + expr = prompt("Введите выражение?", '2-'); + if (expr == null) break; + + try { + res = eval(expr); + if (isNaN(res)) { + throw new Error("Результат неопределён"); + } + + break; + } catch(e) { + alert("Ошибка: "+e.message+", повторите ввод"); + } +} + +alert(res); +``` + diff --git a/1-js/7-js-misc/6-exception/2-eval-calculator-errors/task.md b/1-js/7-js-misc/6-exception/2-eval-calculator-errors/task.md new file mode 100644 index 00000000..73e5108f --- /dev/null +++ b/1-js/7-js-misc/6-exception/2-eval-calculator-errors/task.md @@ -0,0 +1,13 @@ +# Eval-калькулятор с ошибками + +[importance 5] + +Напишите интерфейс, который принимает математическое выражение (в `prompt`) и результат его вычисления через `eval`. + +**При ошибке нужно выводить сообщение и просить переввести выражение**. + +Ошибкой считается не только некорректное выражение, такое как `2+`, но и выражение, возвращающее `NaN`, например `0/0`. + +[demo /] + + diff --git a/1-js/7-js-misc/6-exception/article.md b/1-js/7-js-misc/6-exception/article.md new file mode 100644 index 00000000..0b44a210 --- /dev/null +++ b/1-js/7-js-misc/6-exception/article.md @@ -0,0 +1,512 @@ +# Перехват ошибок, "try..catch" + +Как бы мы хорошо ни программировали, в коде бывают ошибки. Обычно скрипт при ошибке, как говорят, "падает", с выводом ошибки в консоль. + +Но бывают случаи, когда нам хотелось бы как-то контролировать ситуацию, чтобы скрипт не просто "упал", а сделал что-то разумное. + +Для этого в JavaScript есть замечательная конструкция `try..catch`. + +[cut] + +## Конструкция try..catch + +Конструкция `try..catch` состоит из двух основных блоков: `try`, и затем `catch`: + +```js +try { + + // код ... + +} catch(err) { + + // обработка ошибки + +} +``` + +Работает она так: +
          +
        1. Выполняется код внутри блока `try`.
        2. +
        3. Если в нём ошибок нет, то блок `catch(err)` игнорируется, то есть выполнение доходит до конца `try` и потом прыгает через `catch`.
        4. +
        5. Если в нём возникнет ошибка, то выполнение `try` на ней прерывается, и управление прыгает в начало блока `catch(err)`. + +При этом переменная `err` (можно выбрать и другое название) будет содержать объект ошибки с подробной информацией о произошедшем.
        6. +
        + +**Таким образом, при ошибке в `try` скрипт не падает, а продолжает выполнение, и мы получаем возможность обработать ошибку внутри `catch`.** + +Посмотрим это на примерах. + +
          +
        • Пример без ошибок: при запуске сработают `alert` `(1)` и `(2)`: + +```js +//+ run +try { + + alert('Начало блока try'); // *!*(1) <--*/!* + + // .. код без ошибок + + alert('Конец блока try'); // *!*(2) <--*/!* + +} catch(e) { + + alert('Блок catch не получит управление, так как нет ошибок'); // (3) + +} + +alert("Потом код продолжит выполнение..."); +``` + +
        • +
        • Пример с ошибкой: при запуске сработают `(1)` и `(3)`: + +```js +//+ run +try { + + alert('Начало блока try'); // *!*(1) <--*/!* + +*!* + lalala; // ошибка, переменная не определена! +*/!* + + alert('Конец блока try'); // (2) + +} catch(e) { + + alert('Ошибка ' + e.name + ":" + e.message + "\n" + e.stack); // *!*(3) <--*/!* + +} + +alert("Потом код продолжит выполнение..."); +``` + +
        • +
        + +[warn header="`try..catch` подразумевает, что код синтаксически верен"] +Если грубо нарушена структура кода, например не закрыта фигурная скобка или где-то стоит лишняя запятая, то никакой `try..catch` здесь не поможет. Такие ошибки называются *синтаксическими*, интерпретатор не может понять такой код и запустить его. + +Здесь же мы рассматриваем ошибки *семантические*, то есть происходящие в корректном коде, в процессе выполнения. +[/warn] + + + +### Объект ошибки + +В примере выше мы видим объект ошибки. У него есть три основных свойства: +
        +
        `name`
        +
        Тип ошибки. Например, при обращении к несуществующей переменной: `"ReferenceError"`.
        +
        `message`
        +
        Текстовое сообщение о деталях ошибки.
        +
        `stack`
        +
        Везде, кроме IE8-, есть также свойство `stack`, которое содержит строку с информацией о последовательности вызовов, которая привела к ошибке.
        +
        + +В зависимости от браузера, у него могут быть и дополнительные свойства, см. Error в MDN и Error в MSDN. + +## Пример использования + +В JavaScript есть встроенный метод [JSON.parse(str)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), который используется для чтения JavaScript-объектов (и не только) из строки. + +Обычно он используется для того, чтобы обрабатывать данные, полученные по сети, с сервера или из другого источника. + +Мы получаем их и вызываем метод `JSON.parse`, вот так: + +```js +//+ run +var data = '{"name":"Вася", "age": 30}'; // строка с данными, полученная с сервера + +var user = JSON.parse(data); // преобразовали строку в объект + +// теперь user -- это JS-объект с данными из строки +alert( user.name ); // Вася +alert( user.age ); // 30 +``` + +Более детально формат JSON разобран в главе [](/json). + +**В случае, если данные некорректны, `JSON.parse` генерирует ошибку, то есть скрипт "упадёт".** + +Устроит ли нас такое поведение? Конечно нет! + +Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает. + +А люди очень-очень не любят, когда что-то "просто падает", без всякого объявления об ошибке. + +**Бывают ситуации, когда без `try..catch` не обойтись, это -- одна из таких.** + +Используем `try..catch`, чтобы обработать некорректный ответ: + +```js +//+ run +var data = "Has Error"; // в данных ошибка + +try { + + var user = JSON.parse(data); // <-- ошибка при выполнении + alert(user.name); // не сработает + +} catch(e) { + // ...выполнится catch + alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз"); + alert( e.name ); + alert( e.message ); +} +``` + +Здесь в `alert` только выводится сообщение, но область применения гораздо шире: можно повторять запрос, можно предлагать посетителю использовать альтернативный способ, можно отсылать информацию об ошибке на сервер... Свобода действий. + +## Генерация своих ошибок + +Представим на минуту, что данные являются корректным JSON... Но в этом объекте нет нужного свойства `name`: + +```js +//+ run +var data = '{ "age": 30 }'; // данные неполны + +try { + + var user = JSON.parse(data); // <-- выполнится без ошибок +*!* + alert(user.name); // undefined +*/!* + +} catch(e) { + // не выполнится + alert( "Извините, в данных ошибка"); +} +``` + +Вызов `JSON.parse` выполнится без ошибок, но ошибка в данных есть. И, так как свойство `name` обязательно должно быть, то для нас это такие же некорректные данные как и `"Has Error"`. + +Для того, чтобы унифицировать и объединить обработку ошибок парсинга и ошибок в структуре, мы воспользуемся оператором `throw`. + +### Оператор throw + +Оператор `throw` генерирует ошибку. + +Синтаксис: `throw <объект ошибки>`. + +Технически, в качестве объекта ошибки можно передать что угодно, это может быть даже не объект, а число или строка, но всё же лучше, чтобы это был объект, желательно -- совместимый со стандартным, то есть чтобы у него были как минимум свойства `name` и `message`. + +**В качестве конструктора ошибок можно использовать встроенный конструктор: `new Error(message)` или любой другой.** + +В данном случае мы используем конструктор `new SyntaxError(message)`, он создаст ошибку того же типа, что и `JSON.parse`. + +```js +//+ run +var data = '{ "age": 30 }'; // данные неполны + +try { + + var user = JSON.parse(data); // <-- выполнится без ошибок + +*!* + if (!user.name) { + throw new SyntaxError("Данные некорректны"); + } +*/!* + + alert(user.name); + +} catch(e) { + alert( "Извините, в данных ошибка"); +} +``` + +Получилось, что блок `catch` -- единое место для обработки ошибок во всех случаях: когда ошибка выявляется при `JSON.parse` или позже. + +## Проброс исключения + +В коде выше мы предусмотрели обработку ошибок, которые возникают при некорректных данных. Но может ли быть так, что возникнет какая-то другая ошибка? + +Конечно, может! Код -- это вообще мешок с ошибками, бывает даже так что библиотеку выкладывают в открытый доступ, она там 10 лет лежит, её смотрят миллионы людей и на 11й год находятся опаснейшие ошибки. Такова жизнь, таковы люди. + +**Блок `catch` в нашем примере предназначен для обработки ошибок, возникающих при некорректных данных.** + +Если же в него попала какая-то другая ошибка, то вывод сообщения о "некорректных данных" будет дезинформацией посетителя. Ошибку, о которой `catch` не знает, он должен пропустить. + +**Такая техника называется *"проброс исключения"*: в `catch(e)` мы анализируем объект ошибки, и если он нам не подходит, то делаем `throw e`.** + +При этом ошибка "выпадает" из `try..catch` наружу. Далее она может быть поймана либо внешним блоком `try..catch` (если есть), либо "повалит" скрипт. + +Например: + +```js +//+ run +var data = '{ "name": "Вася", "age": 30 }'; // данные корректны + +try { + + var user = JSON.parse(data); + + if (!user.name) { + throw new SyntaxError("Ошибка в данных"); + } + +*!* + blabla(); // произошла непредусмотренная ошибка +*/!* + + alert(user.name); + +} catch(e) { + +*!* + if (e.name == "SyntaxError") { + alert( "Извините, в данных ошибка"); + } else { + throw e; + } +*/!* + +} +``` + +**Ошибка, которая возникла внутри `catch`, "выпадает" наружу.** + +Возможно, что этот `try..catch` вызывается внутри другого, более общего `try.catch`, и он как раз умеет обрабатывать выпавшую ошибку. + +Тогда получится так: + +```js +//+ run +function readData() { + var data = '{ "name": "Вася", "age": 30 }'; + + try { + // ... +*!* + blabla(); // ошибка! +*/!* + } catch(e) { + // ... +*!* + if (e.name != 'SyntaxError') { + throw e; // пробрасываем + } +*/!* + } +} + + +try { + readData(); +} catch(e) { +*!* + alert("Поймал во внешнем catch: " + e); // ловим +*/!* +} +``` + +В примере выше `try..catch` внутри `readData` умеет обрабатывать только `SyntaxError`, а внешний -- все ошибки. + +Если же внешнего `try..catch` нет, то ошибка "вываливается" в консоль, и скрипт умирает. + +### Последняя надежда: window.onerror + +Допустим, ошибка произошла вне блока `try..catch` или выпала из `try..catch` наружу, во внешний код. Скрипт упал. + +Можно ли как-то узнать о том, что произошло? Да, конечно. + +**В браузере существует специальное свойство `window.onerror`, если в него записать функцию, то она выполнится и получит в аргументах сообщение ошибки, текущий URL и номер строки, откуда "выпала" ошибка.** + +Необходимо лишь позаботиться, чтобы функция была назначена заранее. + +Например: + +```html + + +``` + +Как правило, роль `window.onerror` заключается в том, чтобы не оживить скрипт -- скорее всего, это уже невозможно, а в том, чтобы отослать сообщение об ошибке на сервер, где разработчики о ней узнают. + +Существуют даже специальные веб-сервисы, которые предоставляют скрипты для отлова и аналитики таких ошибок, например: [](https://errorception.com/) или [](http://www.muscula.com/). + + + +## Секция finally + +Конструкция `try..catch` может содержать ещё один блок: `finally`. + +Выглядит этот расширенный синтаксис так: + +```js +*!*try*/!* { + .. пробуем выполнить код .. +} *!*catch*/!*(e) { + .. перехватываем исключение .. +} *!*finally*/!* { + .. выполняем всегда .. +} +``` + +Секция `finally` не обязательна, но если она есть, то она выполняется всегда: +
          +
        • после блока `try`, если ошибок не было,
        • +
        • после `catch`, если они были.
        • +
        + +Попробуйте запустить такой код? + +```js +//+ run +try { + alert('try'); + if (confirm('Сгенерировать ошибку?')) BAD_CODE(); +} catch(e) { + alert('catch'); +} finally { + alert('finally'); +} +``` + +У него два варианта работы: +
          +
        1. Если вы ответите на вопрос "Сгенерировать ошибку?" утвердительно, то `try -> catch -> finally`.
        2. +
        3. Если ответите отрицательно, то `try -> finally`. +
        + +**Секцию `finally` используют, чтобы завершить начатые операции при любом варианте развития событий.** + +Например, мы хотим подсчитать время на выполнение функции `sum(n)`, которая должна возвратить сумму чисел от `1` до `n` и работает рекурсивно: + +```js +//+ run +function sum(n) { + return n ? (n + sum(n-1)) : 0; +} + +var n = +prompt('Введите n?', 100); + +var start = new Date(); + +try { + var result = sum(n); +} catch(e) { + result = 0; +*!* +} finally { + var diff = new Date() - start; +} +*/!* + +alert(result ? result : 'была ошибка'); +alert("Выполнение заняло " + diff); +``` + +Здесь секция `finally` гарантирует, что время будет подсчитано в любых ситуациях -- при ошибке в `sum` или без неё. + +Вы можете проверить это, запустив код с указанием `n=100` -- будет без ошибки, `finally` выполнится после `try`, а затем с `n=100000` -- будет ошибка из-за слишком глубокой рекурсии, управление прыгнет в `finally` после `catch`. + +[smart header="`finally` и `return`"] + +Блок `finally` срабатывает при *любом* выходе из `try..catch`, в том числе и `return`. + +В примере ниже, из `try` происходит `return`, но `finally` получает управление до того, как контроль возвращается во внешний код. + +```js +//+ run +function func() { + + try { + // сразу вернуть значение + return 1; + + } catch(e) { + /* ... */ + } finally { +*!* + alert('finally'); +*/!* + } +} + +alert( func() ); // сначала finally, потом 1 +``` + +Если внутри `try` были начаты какие-то процессы, которые нужно завершить по окончании работы, во в `finally` это обязательно будет сделано. + +Кстати, для таких случаев иногда используют `try..finally` вообще без `catch`: + +```js +//+ run +function func() { + try { + return 1; + } finally { + alert('Вызов завершён'); + } +} + +alert( func() ); // сначала finally, потом 1 +``` + +В примере выше `try..finally` вообще не обрабатывает ошибки. Задача в другом -- выполнить код при любом выходе из `try` -- с ошибкой ли, без ошибок или через `return. +[/smart] + + + +## Итого + +Конструкция `try..catch` позволяет обработать произвольные ошибки в блоке кода. + +Это удобно в тех случаях, когда проще сделать действие и потом разбираться с результатом, чем долго и нудно проверять, не упадёт ли чего. + +Кроме того, иногда проверить просто невозможно, например `JSON.parse(str)` не позволяет "проверить" формат строки перед разбором. В этом случае блок `try..catch` необходим. + +Полный вид конструкции: + +```js +*!*try*/!* { + .. пробуем выполнить код .. +} *!*catch*/!*(e) { + .. перехватываем исключение .. +} *!*finally*/!* { + .. выполняем всегда .. +} +``` + +Возможны также варианты `try..catch` или `try..finally`. + +Вместе с `try..catch` используется оператор `throw err`, который генерирует ошибку `err`, причём в качестве `err` рекомендуется генерировать объекты встроенного типа, например [Error](http://javascript.ru/Error) или совместимые с ним. + +[warn header="`try..catch` работает только в синхронном коде"] +Ошибку, которая произойдёт в будущем, например, в `setTimeout`, `try..catch` не поймает: + +```js +//+ run +try { + setTimeout(function() { + throw new Error(); // вылетит в консоль + }, 1000); +} catch(e) { + alert("не сработает"); +} +// на момент срабатывания setTimeout этот код уже завершится +``` + +[/warn] + + + + + diff --git a/1-js/7-js-misc/index.md b/1-js/7-js-misc/index.md new file mode 100644 index 00000000..091a171f --- /dev/null +++ b/1-js/7-js-misc/index.md @@ -0,0 +1,3 @@ +# Некоторые другие возможности + +Различные возможности JavaScript, которые достаточно важны, но не заслужили отдельного раздела. \ No newline at end of file diff --git a/1-js/8-oop/1-about-oop/article.md b/1-js/8-oop/1-about-oop/article.md new file mode 100644 index 00000000..c6842bbc --- /dev/null +++ b/1-js/8-oop/1-about-oop/article.md @@ -0,0 +1,49 @@ +# Введение + +На протяжении долгого времени в программировании применялся [процедурный подход](http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D1%86%D0%B5%D0%B4%D1%83%D1%80%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5). При этом программа состоит из функций, вызывающих друг друга. + +Гораздо позже появилось [объектно-ориентированное программирование](http://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) (ООП), которое позволяет группировать функции и данные в единой сущности -- "объекте". + +Например, "пользователь", "меню", "компонент интерфейса"... + +**Чтобы ООП-подход "работал", объект должен представлять собой законченную, интуитивно понятную сущность.** + +[warn header="ООП -- это не просто объекты"] +В JavaScript объекты часто используются просто как коллекции. + +Например, встроенный объект [Math](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Math) содержит функции (`Math.sin`, `Math.pow`, ...) и данные (константа `Math.PI`). + +При таком использовании объектов мы не можем сказать, что "применён объектно-ориентированный подход". В частности, никакую "единую сущность" `Math` из себя не представляет. +[/warn] + + +Мы уже работали в ООП-стиле, создавая объекты такого вида: + +```js +//+ run +function User(name) { + + this.sayHi = function() { + alert("Привет, я " + name); + }; + +} + +var vasya = new User("Вася"); // создали пользователя +vasya.sayHi(); // пользователь умеет говорить "Привет" +``` + +Здесь мы видим ярко выраженную сущность -- `User` (посетитель). + +**При объектно-ориентированной разработке мы описываем происходящее на уровне объектов, которые создаются, меняют свои свойства, взаимодействуют друг с другом и (в случае браузера) со страницей, в общем, живут.** + +ООП -- это наука о том, как делать правильную архитектуру. У неё есть свои принципы, по ним пишут книги, к примеру: + + + +Далее мы поговорим подробно как про ООП, так и об основных принципах, которых нужно придерживаться. \ No newline at end of file diff --git a/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/solution.md b/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/solution.md new file mode 100644 index 00000000..2fd8f40f --- /dev/null +++ b/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/solution.md @@ -0,0 +1,40 @@ +Кофеварка с новым методом: + +```js +//+ run +function CoffeeMachine(power) { + this.waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; +*!* + var timerId; +*/!* + var self = this; + + function getBoilTime() { + return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + timerId = setTimeout(onReady, getBoilTime()); + }; + +*!* + this.stop = function() { + clearTimeout(timerId) + }; +*/!* +} + + +var coffeeMachine = new CoffeeMachine(50000); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +coffeeMachine.stop(); // кофе приготовлен не будет +``` + diff --git a/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/task.md b/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/task.md new file mode 100644 index 00000000..33123bf7 --- /dev/null +++ b/1-js/8-oop/2-internal-external-interface/1-add-method-property-coffeemachine/task.md @@ -0,0 +1,41 @@ +# Добавить метод и свойство кофеварке + +[importance 5] + +Улучшите готовый код кофеварки, который дан ниже: добавьте в кофеварку *публичный* метод `stop()`, который будет останавливать кипячение (через `clearTimeout`). + +```js +function CoffeeMachine(power) { + this.waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + + var self = this; + + function getBoilTime() { + return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + setTimeout(onReady, getBoilTime()); + }; + +} +``` + +Вот такой код должен ничего не выводить: + +```js +var coffeeMachine = new CoffeeMachine(50000); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +coffeeMachine.stop(); // кофе приготовлен не будет +``` + +P.S. Текущую температуру воды вычислять и хранить не требуется. +P.P.S. При решении вам, скорее всего, понадобится добавить *приватное* свойство `timerId`, которое будет хранить текущий таймер. \ No newline at end of file diff --git a/1-js/8-oop/2-internal-external-interface/article.md b/1-js/8-oop/2-internal-external-interface/article.md new file mode 100644 index 00000000..533545c2 --- /dev/null +++ b/1-js/8-oop/2-internal-external-interface/article.md @@ -0,0 +1,352 @@ +# Внутренний и внешний интерфейс + +Один из важнейших принципов ООП -- отделение внутреннего интерфейса от внешнего. + +Это -- обязательная практика в разработке чего угодно сложнее hello world. + +Чтобы это понять, отвлечемся от разработки и переведем взгляд на объекты реального мира. + +Как правило, устройства, с которыми мы имеем дело, весьма сложны. Но *разделение интерфейса на внешний и внутренний* позволяет использовать их без малейших проблем. +[cut] +## Пример из жизни + +Например, кофеварка. Простая снаружи: кнопка, индикатор, отверстия,... И, конечно, результат -- кофе :) + + + +Но внутри... (картинка из пособия по ремонту) + + + +Масса деталей. Но мы можем пользоваться ей, совершенно не зная об этом. + +Кофеварки -- довольно-таки надежны, не правда ли? Можно пользоваться годами, и только когда что-то пойдет не так -- придется нести к мастеру. + +Секрет надежности и простоты кофеварки -- в том, что все детали отлажены и *спрятаны* внутри. + +Если снять с кофеварки защитный кожух, то использование её будет более сложным (куда нажимать?) и опасным (током ударить может). + +Как мы увидим, объекты очень схожи с кофеварками. + +Только для того, чтобы прятать внутренние детали, используется не кожух, а специальные средства языка и соглашения. + +## Внутренний и внешний интерфейс + +В программировании есть чёткое разграничение методов и свойств объекта на две группы: + +
          +
        • *Внутренний интерфейс* -- это свойства и методы, доступ к которым может быть осуществлен только из других методов объекта, их также называют "приватными" (есть и другие термины, встретим их далее).
        • + +
        • *Внешний интерфейс* -- это свойства и методы, доступные снаружи объекта, их называют "публичными".
        • +
        + +Если продолжить аналогию с кофеваркой -- то, что спрятано внутри кофеварки: трубка кипятильника, нагревательный элемент, тепловой предохранитель и так далее -- это её внутренний интерфейс. + +Внутренний интерфейс используется для обеспечения работоспособности объекта, его детали используют друг друга. Например, трубка кипятильника подключена к нагревательному элементу. + +Но снаружи кофеварка закрыта специальным кожухом, чтобы никто к ним не подобрался. Детали скрыты и недоступны. Виден лишь внешний интерфейс. + +**Все, что нужно для пользования объектом -- это внешний интерфейс.** + +О внутреннем пользователю вообще знать не обязательно. + +[smart header="Между приватным и публичным"] +Приватные свойства полностью закрыты для доступа снаружи, а публичные -- наоборот, полностью открыты. Это крайности, между которыми бывают промежуточные варианты. + +
          +
        • В языке С++ можно открыть доступ к приватным переменным одного класса -- другому, объявив его "дружественным".
        • +
        • В языке Java можно объявлять переменные, которые доступны всем классам внутри "пакета".
        • +
        • Между объектами можно организовать "наследование" и сделать свойства открытыми только для "наследников", такой вариант доступа называют "защищённым".
        • +
        + +В этом учебнике будем изучать наследование и защищённые свойства, но позже, а пока сосредоточимся на приватном и публичном доступе... И, конечно, использовать мы будем JavaScript :) +[/smart] + +Это были общие слова по теории программирования. + +Далее мы реализуем кофеварку на JavaScript с приватными и публичными свойствами. В кофеварке много деталей, мы конечно, не будем моделировать каждый винтик, а сосредоточимся на основных приёмах разработки. + +### Шаг 1: публичное и приватное свойство + +Конструктор кофеварок будет называться `CoffeeMachine`. + +```js +//+ run +function CoffeeMachine(power) { + this.waterAmount = 0; // количество воды в кофеварке + + alert('Создана кофеварка мощностью: ' + power + ' ватт'); +} + +// создать кофеварку +var coffeeMachine = new CoffeeMachine(100); + +// залить воды +coffeeMachine.waterAmount = 200; +``` + +**Локальные переменные, включая параметры конструктора, являются приватными свойствами.** + +В примере выше это `power` -- мощность кофеварки, которая указывается при создании и далее будет использована для расчёта времени кипячения. + +К локальным переменным конструктора нельзя обратиться снаружи, но они доступны внутри самого конструктора. + +**Свойства, записанные в `this`, являются публичными.** + +Здесь свойство `waterAmount` записано в объект, а значит -- доступно для модификации снаружи. Можно доливать и выливать воду в любом количестве. + +[smart header="Вопрос терминологии"] +Может возникнуть вопрос -- почему я назвал `power` "приватным свойством", ведь это локальная *переменная*, а никакое не *свойство* объекта? + +Здесь небольшой конфликт терминологий. + +Термины "приватное свойство/метод", "публичное свойство/метод" относятся к общей теории ООП. А их конкретная реализация в языке программирования может быть различной. + +Здесь ООП-принцип "приватного свойства" реализован через локальные переменные, поэтому и "локальная переменная" и "приватное свойство" -- правильные термины, в зависимости от того, с какой точки зрения посмотреть -- кода или архитектуры ООП. +[/smart] + + +### Шаг 2: публичный и приватный методы + +Добавим публичный метод `run`, запускающий кофеварку, а также вспомогательные внутренние методы `getBoilTime` и `onReady`: + +```js +//+ run +function CoffeeMachine(power) { + + this.waterAmount = 0; + +*!* + // расчёт времени для кипячения + function getBoilTime() { + return 1000; // точная формула расчета будет позже + } + + // что делать по окончании процесса + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + // setTimeout - встроенная функция, + // она запустит onReady через getBoilTime() миллисекунд + setTimeout(onReady, getBoilTime()); + }; +*/!* +} + +var coffeeMachine = new CoffeeMachine(100); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +``` + +**Приватные методы, такие как `onReady`, `getBoilTime` объявляются как вложенные функции.** + +В результате естественным образом получается, что доступ к ним (через замыкание) имеют только другие функции, объявленные в том же конструкторе. + +### Шаг 3: константа + +Для расчёта времени на кипячение воды используется формула `c*m*ΔT / power`, где: +
          +
        • `c` -- коэффициент теплоёмкости воды, физическая константа равная `4200`.
        • +
        • `m` -- масса воды, которую нужно согреть.
        • +
        • `ΔT` -- температура, на которую нужно подогреть, будем считать, что изначально вода -- комнатной температуры 20°С, то есть до 100° нужно греть на `ΔT=80`.
        • +
        • `power` -- мощность.
        • +
        + +Используем её в более реалистичном варианте `getBoilTime()`, включающем использование приватных свойств и константу: + +```js +//+ run +"use strict" + +function CoffeeMachine(power) { + + this.waterAmount = 0; + +*!* + // физическая константа - удельная теплоёмкость воды для getBoilTime + var WATER_HEAT_CAPACITY = 4200; + + // расчёт времени для кипячения + function getBoilTime() { + return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } +*/!* + + // что делать по окончании процесса + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + setTimeout(onReady, getBoilTime()); + }; + +} + +var coffeeMachine = new CoffeeMachine(1000); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +``` + +Удельная теплоёмкость `WATER_HEAT_CAPACITY` выделена большими буквами, так как это константа. + +**Внимание, при запуске кода выше в методе `getBoilTime` будет ошибка. Как вы думаете, почему?** + +### Шаг 4: доступ к объекту из внутреннего метода + +Внутренний метод вызывается так: `getBoilTime()`. А чему при этом равен `this`?... Как вы наверняка помните, в современном стандарте он будет `undefined` (в старом -- `window`), из-за этого при чтении `this.waterAmount` возникнет ошибка! + +Её можно решить, если вызвать `getBoilTime` с явным указанием контекста: `getBoilTime.call(this)`: + +```js +//+ run +function CoffeeMachine(power) { + this.waterAmount = 0; + var WATER_HEAT_CAPACITY = 4200; + + function getBoilTime() { + return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { +*!* + setTimeout(onReady, getBoilTime.call(this)); +*/!* + }; + +} + +// создаю кофеварку, мощностью 100000W чтобы кипятила быстро +var coffeeMachine = new CoffeeMachine(100000); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +``` + +Такой подход будет работать, но он не очень-то удобен. Ведь получается, что теперь везде, где мы хотим вызвать `getBoilTime`, нужно явно указывать контекст, т.е. писать `getBoilTime.call(this)`. + +К счастью существуют более элегантные решения. + +### Привязка через bind + +Можно при объявлении привязать `getBoilTime` к объекту через `bind`, тогда вопрос контекста отпадёт сам собой: + +```js +//+ run +function CoffeeMachine(power) { + this.waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + +*!* + var getBoilTime = function() { + return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power; + }.bind(this); +*/!* + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { +*!* + setTimeout(onReady, getBoilTime()); +*/!* + }; + +} + +var coffeeMachine = new CoffeeMachine(100000); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +``` + +Это решение будет работать, теперь функцию можно просто вызывать без `call`. Но объявление функции стало менее красивым. + +### Сохранение this в замыкании + +Пожалуй, самый удобный и часто применяемый путь решения состоит в том, чтобы предварительно скопировать `this` во вспомогательную переменную и обращаться из внутренних функций уже к ней. + +Вот так: + +```js +//+ run +function CoffeeMachine(power) { + this.waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + +*!* + var self = this; + + function getBoilTime() { + return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } +*/!* + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + setTimeout(onReady, getBoilTime()); + }; + +} + +var coffeeMachine = new CoffeeMachine(100000); +coffeeMachine.waterAmount = 200; + +coffeeMachine.run(); +``` + +Теперь `getBoilTime` получает `self` из замыкания. + +**Конечно, чтобы это работало, мы не должны изменять `self`, а все приватные методы, которые хотят привязаться к текущему объекту, должны использовать внутри себя `self` вместо `this`.** + +Вместо `self` можно использовать любое другое имя переменной, например `var me = this`. + +## Что нам даст разделение доступов? + +Итак, мы сделали кофеварку с публичными и приватными методами и заставили их корректно работать. + +В терминологии ООП отделение и защита внутреннего интерфейса называется [инкапсуляция](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%BA%D0%B0%D0%BF%D1%81%D1%83%D0%BB%D1%8F%D1%86%D0%B8%D1%8F_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29). + +Кратко перечислим бонусы, которые она даёт: + +
        +
        Защита пользователей от выстрела себе в ногу
        +
        Представьте, команда разработчиков пользуется кофеваркой. Кофеварка создана фирмой "Лучшие Кофеварки" и, в общем, работает хорошо, но с неё сняли защитный кожух и, таким образом, внутренний интерфейс стал доступен. + +Все разработчики цивилизованны -- и пользуются кофеваркой как обычно. Но хитрый Вася решил, что он самый умный, и подкрутил кое-что внутри кофеварки, чтобы кофе заваривался покрепче. Вася не знал, что те изменения, которые он произвёл, приведут к тому, что кофеварка испортится через два дня. + +Виноват, разумеется, не только Вася, но и тот, кто снял защитный кожух с кофеварки, и тем самым позволил Васе проводить манипуляции. + +В программировании -- то же самое. Если пользователь объекта будет менять то, что не рассчитано на изменение снаружи -- последствия могут быть непредсказуемыми. +
        +
        Удобство в поддержке
        +
        Ситуация в программировании сложнее, чем с кофеваркой, т.к. кофеварку один раз купили и всё, а программа может улучшаться и дорабатываться. + +**При наличии чётко выделенного внешнего интерфейса, разработчик может свободно менять внутренние свойства и методы, без оглядки на коллег.** + +Гораздо легче разрабатывать, если знаешь, что ряд методов (все внутренние) можно переименовывать, менять их параметры, и вообще, переписать как угодно, так как внешний код к ним абсолютно точно не обращается. + +Ближайшая аналогия в реальной жизни -- это когда выходит "новая версию" кофеварки, которая работает гораздо лучше. Разработчик мог переделать всё внутри, но пользоваться ей по-прежнему просто, так как внешний интерфейс сохранён.
        +
        Управление сложностью
        +
        Люди обожают пользоваться вещами, которые просты с виду. А что внутри -- дело десятое. + +Программисты здесь не исключение. + +**Всегда удобно, когда детали реализации скрыты, и доступен простой, понятно документированный внешний интерфейс.** +
        +
        + diff --git a/1-js/8-oop/2-internal-external-interface/coffee-inside.jpg b/1-js/8-oop/2-internal-external-interface/coffee-inside.jpg new file mode 100755 index 00000000..4d2c3dd0 Binary files /dev/null and b/1-js/8-oop/2-internal-external-interface/coffee-inside.jpg differ diff --git a/1-js/8-oop/2-internal-external-interface/coffee.jpg b/1-js/8-oop/2-internal-external-interface/coffee.jpg new file mode 100755 index 00000000..14e2810c Binary files /dev/null and b/1-js/8-oop/2-internal-external-interface/coffee.jpg differ diff --git a/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/solution.md b/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/solution.md new file mode 100644 index 00000000..5713cb4e --- /dev/null +++ b/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/solution.md @@ -0,0 +1,29 @@ +Решение: + +```js +//+ run +function User() { + + var firstName, surName; + + this.setFirstName = function(newFirstName) { + firstName = newFirstName; + }; + + this.setSurname = function(newSurname) { + surname = newSurname; + }; + + this.getFullName = function() { + return firstName + ' ' + surname; + } +} + +var user = new User(); +user.setFirstName("Петя"); +user.setSurname("Иванов"); + +alert( user.getFullName() ); // Петя Иванов +``` + +Обратим внимание, что для "геттера" `getFullName` нет соответствующего свойства объекта, он конструирует ответ "на лету". Это нормально. Одна из целей существования геттеров/сеттеров -- как раз и есть изоляция внутренних свойств объекта, чтобы можно было их как угодно менять, генерировать "на лету", а внешний интерфейс оставался тем же. \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/task.md b/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/task.md new file mode 100644 index 00000000..d9efe056 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/1-object-with-getters-setters/task.md @@ -0,0 +1,25 @@ +# Написать объект с геттерами и сеттерами + +[importance 4] + +Напишите конструктор `User` для создания объектов: +
          +
        • С приватными свойствами имя `firstName` и фамилия `surname`.
        • +
        • С сеттерами для этих свойств.
        • +
        • С геттером `getFullName()`, который возвращает полное имя.
        • +
        + +Должен работать так: + +```js +function User() { + /* ваш код */ +} + +var user = new User(); +user.setFirstName("Петя"); +user.setSurname("Иванов"); + +alert( user.getFullName() ); // Петя Иванов +``` + diff --git a/1-js/8-oop/3-getters-setters/2-getter-power/solution.md b/1-js/8-oop/3-getters-setters/2-getter-power/solution.md new file mode 100644 index 00000000..b82c109a --- /dev/null +++ b/1-js/8-oop/3-getters-setters/2-getter-power/solution.md @@ -0,0 +1,28 @@ + + +```js +function CoffeeMachine(power, capacity) { + //... + this.setWaterAmount = function(amount) { + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить воды больше, чем " + capacity); + } + + waterAmount = amount; + }; + + this.getWaterAmount = function() { + return waterAmount; + }; + +*!* + this.getPower = function() { + return power; + }; +*/!* +} +``` + diff --git a/1-js/8-oop/3-getters-setters/2-getter-power/task.md b/1-js/8-oop/3-getters-setters/2-getter-power/task.md new file mode 100644 index 00000000..7301c6f5 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/2-getter-power/task.md @@ -0,0 +1,32 @@ +# Добавить геттер для power + +[importance 5] + +Добавьте кофеварке геттер для приватного свойства `power`, чтобы внешний код мог узнать мощность кофеварки. + +Исходный код: + +```js +function CoffeeMachine(power, capacity) { + //... + this.setWaterAmount = function(amount) { + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить воды больше, чем " + capacity); + } + + waterAmount = amount; + }; + + this.getWaterAmount = function() { + return waterAmount; + }; + +} +``` + +Обратим внимание, что ситуация, когда у свойства `power` есть геттер, но нет сеттера -- вполне обычна. + +Здесь это означает, что мощность `power` можно указать лишь при создании кофеварки и в дальнейшем её можно прочитать, но нельзя изменить. \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/solution.md b/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/solution.md new file mode 100644 index 00000000..d01cf173 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/solution.md @@ -0,0 +1,46 @@ +В решении ниже `addWater` будет просто вызывать `setWaterAmount`. + +```js +//+ run +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + this.setWaterAmount = function(amount) { + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить больше, чем " + capacity); + } + + waterAmount = amount; + }; + +*!* + this.addWater = function(amount) { + this.setWaterAmount(waterAmount + amount); + }; +*/!* + + function onReady() { + alert('Кофе готов!'); + } + + this.run = function() { + setTimeout(onReady, getTimeToBoil()); + }; + +} + +var coffeeMachine = new CoffeeMachine(100000, 400); +coffeeMachine.addWater(200); +coffeeMachine.addWater(100); +coffeeMachine.addWater(300); // Нельзя залить больше.. +coffeeMachine.run(); +``` + diff --git a/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/task.md b/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/task.md new file mode 100644 index 00000000..2243e0d3 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/3-add-public-coffeemachine/task.md @@ -0,0 +1,51 @@ +# Добавить публичный метод кофеварке + +[importance 5] + +Добавьте кофеварке публичный метод `addWater(amount)`, который будет добавлять воду. + +При этом, конечно же, должны происходить все необходимые проверки -- на положительность и превышение ёмкости. + +Исходный код: + +```js +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + this.setWaterAmount = function(amount) { + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить больше, чем " + capacity); + } + + waterAmount = amount; + }; + + function onReady() { + alert('Кофе готов!'); + } + + this.run = function() { + setTimeout(onReady, getTimeToBoil()); + }; + +} +``` + +Вот такой код должен приводить к ошибке: + +```js +var coffeeMachine = new CoffeeMachine(100000, 400); +coffeeMachine.addWater(200); +coffeeMachine.addWater(100); +coffeeMachine.addWater(300); // Нельзя залить больше, чем 400 +coffeeMachine.run(); +``` + diff --git a/1-js/8-oop/3-getters-setters/4-setter-onready/solution.md b/1-js/8-oop/3-getters-setters/4-setter-onready/solution.md new file mode 100644 index 00000000..da337433 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/4-setter-onready/solution.md @@ -0,0 +1,66 @@ + + +```js +//+ run +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + this.setWaterAmount = function(amount) { + // ... проверки пропущены для краткости + waterAmount = amount; + }; + + this.getWaterAmount = function(amount) { + return waterAmount; + }; + + function onReady() { + alert('Кофе готов!'); + } + +*!* + this.setOnReady = function(newOnReady) { + onReady = newOnReady; + }; +*/!* + + this.run = function() { +*!* + setTimeout(function() { onReady(); }, getTimeToBoil()); +*/!* + }; + +} + +var coffeeMachine = new CoffeeMachine(20000, 500); +coffeeMachine.setWaterAmount(150); + +coffeeMachine.run(); + +*!* +coffeeMachine.setOnReady(function() { + var amount = coffeeMachine.getWaterAmount(); + alert('Готов кофе: ' + amount + 'мл'); // Готов кофе: 150 мл +}); +*/!* +``` + +Обратите внимание на два момента в решении: +
          +
        1. В сеттере `setOnReady` параметр называется `newOnReady`. Мы не можем назвать его `onReady`, так как тогда изнутри сеттера мы никак не доберёмся до внешнего (старого значения): + +```js +// нерабочий вариант +this.setOnReady = function(onReady) { + onReady = onReady; // ??? внешняя переменная onReady недоступна +}; +``` + +
        2. +
        3. Чтобы `setOnReady` можно было вызывать в любое время, в `setTimeout` передаётся не `onReady`, а анонимная функция `function() { onReady() }`, которая возьмёт текущий (установленный последним) `onReady` из замыкания.
        4. +
        \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/4-setter-onready/task.md b/1-js/8-oop/3-getters-setters/4-setter-onready/task.md new file mode 100644 index 00000000..1d301654 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/4-setter-onready/task.md @@ -0,0 +1,58 @@ +# Создать сеттер для onReady + +[importance 5] + +Обычно когда кофе готов, мы хотим что-то сделать, например выпить его. + +Сейчас при готовности срабатывает функция `onReady`, но она жёстко задана в коде: + +```js +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + this.setWaterAmount = function(amount) { + // ... проверки пропущены для краткости + waterAmount = amount; + }; + + this.getWaterAmount = function(amount) { + return waterAmount; + }; + +*!* + function onReady() { + alert('Кофе готов!'); + } +*/!* + + this.run = function() { + setTimeout(onReady, getTimeToBoil()); + }; + +} +``` + +Создайте сеттер `setOnReady`, чтобы код снаружи мог назначить свой `onReady`, вот так: + +```js +var coffeeMachine = new CoffeeMachine(20000, 500); +coffeeMachine.setWaterAmount(150); + +*!* +coffeeMachine.setOnReady(function() { + var amount = coffeeMachine.getWaterAmount(); + alert('Готов кофе: ' + amount + 'мл'); // Кофе готов: 150 мл +}); +*/!* + +coffeeMachine.run(); +``` + +P.S. Значение `onReady` по умолчанию должно быть таким же, как и раньше. + +P.P.S. Постарайтесь сделать так, чтобы `setOnReady` можно было вызвать не только до, но и *после* запуска кофеварки, то есть чтобы функцию `onReady` можно было изменить в любой момент до её срабатывания. \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/solution.md b/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/solution.md new file mode 100644 index 00000000..dd4d0cf3 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/solution.md @@ -0,0 +1,61 @@ +Код решения модифицирует функцию `run` и добавляет приватный идентификатор таймера `timerId`, по наличию которого мы судим о состоянии кофеварки: + +```js +//+ run +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + +*!* + var timerId; + + this.isRunning = function() { + return !!timerId; + }; +*/!* + + var WATER_HEAT_CAPACITY = 4200; + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + this.setWaterAmount = function(amount) { + // ... проверки пропущены для краткости + waterAmount = amount; + }; + + this.getWaterAmount = function(amount) { + return waterAmount; + }; + + function onReady() { + alert('Кофе готов!'); + } + + this.setOnReady = function(newOnReady) { + onReady = newOnReady; + }; + + this.run = function() { +*!* + timerId = setTimeout(function() { + timerId = null; + onReady(); + }, getTimeToBoil()); + }; +*/!* + +} + +var coffeeMachine = new CoffeeMachine(20000, 500); +coffeeMachine.setWaterAmount(100); + +alert('До: ' + coffeeMachine.isRunning()); // До: false + +coffeeMachine.run(); +alert('В процессе: ' + coffeeMachine.isRunning()); // В процессе: true + +coffeeMachine.setOnReady(function() { + alert("После: " + coffeeMachine.isRunning()); // После: false +}); +``` + diff --git a/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/task.md b/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/task.md new file mode 100644 index 00000000..45cb1d97 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/5-coffeemachine-add-isrunning/task.md @@ -0,0 +1,25 @@ +# Добавить метод isRunning + +[importance 5] + +Из внешнего кода мы хотели бы иметь возможность понять -- запущена кофеварка или нет. + +Для этого добавьте кофеварке публичный метод `isRunning()`, который будет возвращать `true`, если она запущена и `false`, если нет. + +Нужно, чтобы такой код работал: + +```js +var coffeeMachine = new CoffeeMachine(20000, 500); +coffeeMachine.setWaterAmount(100); + +alert('До: ' + coffeeMachine.isRunning()); // До: false + +coffeeMachine.run(); +alert('В процессе: ' + coffeeMachine.isRunning()); // В процессе: true + +coffeeMachine.setOnReady(function() { + alert("После: " + coffeeMachine.isRunning()); // После: false +}); +``` + +Исходный код возьмите из решения [предыдущей задачи](/task/setter-onReady). \ No newline at end of file diff --git a/1-js/8-oop/3-getters-setters/article.md b/1-js/8-oop/3-getters-setters/article.md new file mode 100644 index 00000000..a6888832 --- /dev/null +++ b/1-js/8-oop/3-getters-setters/article.md @@ -0,0 +1,157 @@ +# Геттеры и сеттеры + +Для *управляемого* доступа к состоянию объекта используют специальные функции, так называемые "геттеры" и "сеттеры". +[cut] + +## Геттер и сеттер для воды + +На текущий момент количество воды в кофеварке является публичным свойством `waterAmount`: + +```js +//+ run +function CoffeeMachine(power) { + // количество воды в кофеварке + this.waterAmount = 0; + + ... +} +``` + +Это немного опасно. Ведь в это свойство можно записать произвольное количество воды, хоть весь мировой океан. + +```js +// не помещается в кофеварку! +coffeeMachine.waterAmount = 1000000; +``` + +Это ещё ничего, гораздо хуже, что можно наоборот -- вылить больше, чем есть: + +```js +// и не волнует, было ли там столько воды вообще! +coffeeMachine.waterAmount -= 1000000; +``` + +Так происходит потому, что свойство полностью доступно снаружи. + +Чтобы не было таких казусов, нам нужно ограничить контроль над свойством со стороны внешнего кода. + +**Для лучшего контроля над свойством его делают приватным, а запись значения осуществляется через специальный метод, который называют *"сеттер"* (setter method).** + +Типичное название для сеттера -- `setСвойство`, например, в случае с кофеваркой таким сеттером будет метод `setWaterAmount`: + +```js +//+ run +function CoffeeMachine(power, capacity) { // capacity - ёмкость кофеварки + var waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + +*!* + // "умная" установка свойства + this.setWaterAmount = function(amount) { + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить воды больше, чем " + capacity); + } + + waterAmount = amount; + }; +*/!* + + function onReady() { + alert('Кофе готов!'); + } + + this.run = function() { + setTimeout(onReady, getTimeToBoil()); + }; + +} + +var coffeeMachine = new CoffeeMachine(1000, 500); +coffeeMachine.setWaterAmount(600); // упс, ошибка! +``` + +Теперь `waterAmount` -- внутреннее свойство, его можно записать (через сеттер), но, увы, нельзя прочитать. + +**Для того, чтобы дать возможность внешнему коду узнать его значение, создадим специальную функцию -- "геттер" (getter method).** + +Геттеры обычно имеют название вида `getСвойство`, в данном случае `getWaterAmount`: + +```js +//+ run +function CoffeeMachine(power, capacity) { + //... + this.setWaterAmount = function(amount) { + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить воды больше, чем " + capacity); + } + + waterAmount = amount; + }; + +*!* + this.getWaterAmount = function() { + return waterAmount; + }; +*/!* +} + +var coffeeMachine = new CoffeeMachine(1000, 500); +coffeeMachine.setWaterAmount(450); +alert( coffeeMachine.getWaterAmount() ); // 450 +``` + +## Единый геттер-сеттер + +Для большего удобства иногда делают единый метод, который называется так же, как свойство и отвечает *и за запись и за чтение*. + +При вызове без параметров такой метод возвращает свойство, а при передаче параметра -- назначает его. + +Выглядит это так: + +```js +//+ run +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + +*!* + this.waterAmount = function(amount) { +*/!* + // вызов без параметра, значит режим геттера, возвращаем свойство + if (!arguments.length) return waterAmount; + + // иначе режим сеттера + if (amount < 0) { + throw new Error("Значение должно быть положительным"); + } + if (amount > capacity) { + throw new Error("Нельзя залить воды больше, чем " + capacity); + } + + waterAmount = amount; + }; + +} + +var coffeeMachine = new CoffeeMachine(1000, 500); + +// пример использования +*!* +coffeeMachine.waterAmount(450); +alert( coffeeMachine.waterAmount() ); // 450 +*/!* +``` + +Единый геттер-сеттер используется реже, чем две отдельные функции, но в некоторых JavaScript-библиотеках, например [jQuery](http://jquery.com) и [D3](http://d3js.org) подобный подход принят на уровне концепта. + +## Задачи + diff --git a/1-js/8-oop/4-descriptors-getters-setters/1-replace-property-getter/solution.md b/1-js/8-oop/4-descriptors-getters-setters/1-replace-property-getter/solution.md new file mode 100644 index 00000000..efe1006b --- /dev/null +++ b/1-js/8-oop/4-descriptors-getters-setters/1-replace-property-getter/solution.md @@ -0,0 +1,27 @@ + + +```js +//+ run +function CoffeeMachine(power, capacity) { + var waterAmount = 0; + + Object.defineProperty(this, "waterAmount", { + + get: function() { + return waterAmount; + }, + + set: function(amount) { + if (amount > capacity) { + throw new Error("Нельзя залить больше, чем " + capacity); + } + + waterAmount = amount; + } + }); +} + +var coffeeMachine = new CoffeeMachine(1000, 300); +coffeeMachine.waterAmount = 500; +``` + diff --git a/1-js/8-oop/4-descriptors-getters-setters/1-replace-property-getter/task.md b/1-js/8-oop/4-descriptors-getters-setters/1-replace-property-getter/task.md new file mode 100644 index 00000000..73d213ef --- /dev/null +++ b/1-js/8-oop/4-descriptors-getters-setters/1-replace-property-getter/task.md @@ -0,0 +1,22 @@ +# Заменить свойство на встроенные геттеры/сеттеры + +[importance 5] + +Вам попал в руки код кофеварки, который использует свойство `this.waterAmount` для хранения количества воды: + +```js +function CoffeeMachine(power, capacity) { + // количество воды в кофеварке + this.waterAmount = 0; +} + +// создать кофеварку +var coffeeMachine = new CoffeeMachine(1000, 300); + +// залить воды +coffeeMachine.waterAmount = 500; +``` + +Задача -- сделать так, чтобы при присвоении `coffeeMachine.waterAmount = 500` выдавалась ошибка, если значение больше `capacity` (в примере выше `300`). + +Для этого реализуйте `waterAmount` через геттер и сеттер, который будет проверять корректность установки. Используйте для этого `Object.defineProperty`. \ No newline at end of file diff --git a/1-js/8-oop/4-descriptors-getters-setters/article.md b/1-js/8-oop/4-descriptors-getters-setters/article.md new file mode 100644 index 00000000..bc7f5eb2 --- /dev/null +++ b/1-js/8-oop/4-descriptors-getters-setters/article.md @@ -0,0 +1,407 @@ +# Дескрипторы, геттеры и сеттеры свойств + +В этой главе мы рассмотрим возможности, которые позволяют очень гибко и мощно управлять всеми свойствами объекта, включая их аспекты -- изменяемость, видимость в цикле `for..in` и даже "невидимые" геттеры-сеттеры. + +Они поддерживаются всеми современными браузерами, но не IE8-. Точнее говоря, они поддерживаются даже в IE8, но не для всех объектов, а только для DOM-объектов (они используются при работе со страницей, это сейчас вне нашего рассмотрения). + +Большая часть этих методов, в частности, работа с дескрипторами, не задействуется в других главах учебника для обеспечения совместимости с IE8-, но во вспомогательных скриптах -- библиотеках для тестирования, сборки, а также для сервера Node.JS они используются достаточно активно. + +[cut] +## Дескрипторы в примерах + +Основной метод для управления свойствами -- [Object.defineProperty](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty). + +Он позволяет определить свойство путём задания "дескриптора" -- описания, включающего в себя ряд важных внутренних параметров. + +Синтаксис: + +```js +Object.defineProperty(obj, prop, descriptor) +``` + +Аргументы: +
        +
        `obj`
        +
        Объект, в котором объявляется свойство.
        +
        `prop`
        +
        Имя свойства, которое нужно объявить или модифицировать.
        +
        `descriptor`
        +
        Дескриптор -- объект, который описывает поведение свойства. В нём могут быть следующие поля: + +
        +
        `value`
        +
        Значение свойства, по умолчанию `undefined`
        +
        `writable`
        +
        Значение свойства можно менять, если `true`. По умолчанию `false`.
        +
        `configurable`
        +
        Если `true`, то свойство можно удалять, а также менять его в дальнейшем при помощи `defineProperty`. По умолчанию `false`.
        +
        `enumerable`
        +
        Если `true`, то свойство будет участвовать в переборе `for..in`. По умолчанию `false`.
        +
        `get`
        +
        Функция, которая возвращает значение свойства. По умолчанию `undefined`.
        +
        `set`
        +
        Функция, которая записывает значение свойства. По умолчанию `undefined`.
        +
        +
        +
        + +Чтобы избежать конфликта, запрещено одновременно указывать значение `value` и функции `get/set`. Либо значение, либо функции для его чтения-записи, одно из двух. Также запрещено и не имеет смысла указывать `writable` при наличии `get/set`-функций. + +Далее мы подробно разберём эти свойства на примерах. + +### Пример: обычное свойство + +Обычное свойство добавить очень просто. + +Два таких вызова работают одинаково: + +```js +var user = {}; + +// 1. простое присваивание +user.name = "Вася"; + +// 2. указание значения через дескриптор +Object.defineProperty(user, "name", { value: "Вася" }); +``` + +### Пример: свойство-константа + +Для того, чтобы сделать свойство неизменяемым, добавим ему флаги `writable` и `configurable`: + +```js +//+ run +*!* +"use strict"; +*/!* + +var user = {}; + +Object.defineProperty(user, "name", { + value: "Вася", + writable: false, // запретить присвоение "user.name=" + configurable: false // запретить удаление "delete user.name" +}); + +// Теперь попытаемся изменить это свойство. + +// в strict mode присвоение "user.name=" вызовет ошибку +*!* +user.name = "Петя"; +*/!* +``` + +**Заметим, что ошибки при попытке изменения такого свойства произойдут только при `use strict`.** + +Без `use strict` операция записи "молча" не сработает. + +### Пример: свойство, скрытое для for..in + +Встроенный метод `toString`, как и большинство встроенных методов, не участвует в цикле `for..in`. Это удобно, так как обычно такое свойство является "служебным". + +К сожалению, свойство `toString`, объявленное обычным способом, будет видно в цикле `for..in`, например: + +```js +//+ run +var user = { + name: "Вася", + toString: function() { return this.name; } +}; + +*!* +for(var key in user) alert(key); // name, toString +*/!* +``` + +`Object.defineProperty` может помочь исключить `toString` из списка итерации. Достаточно поставить ему флаг `enumerable: false`: + +```js +//+ run +var user = { + name: "Вася", + toString: function() { return this.name; } +}; + +*!* +Object.defineProperty(user, "toString", {enumerable: false}); + +for(var key in user) alert(key); // name +*/!* +``` + +Обратим внимание, вызов `defineProperty` не перезаписал свойство, а просто модифицировал настройки у существующего `toString`. + +### Пример: свойство как функция-геттер + +Дескриптор позволяет задать свойство, которое на самом деле работает как функция. Для этого в нём нужно указать эту функцию в `get`. + +Например, у объекта `user` есть обычные свойства: имя `firstName` и фамилия `surname`. + +Создадим свойство `fullName`, которое на самом деле является функцией: + +```js +//+ run +var user = { + firstName: "Вася", + surname: "Петров" +} + +Object.defineProperty(user, "fullName", { + *!*get*/!*: function() { + return this.firstName + ' ' + this.surname; + } +}); + +*!* +alert(user.fullName); // Вася Петров +*/!* +``` + +**Обратим внимание, снаружи это обычное свойство `user.fullName`.** + +Лишь в описании указывается, что на самом деле его значение возвращается функцией. + +### Пример: свойство геттер-сеттер + +Также можно указать функцию, которая используется для записи значения, при помощи дескриптора `set`. + +Например, добавим возможность присвоения `user.fullName` к примеру выше: + +```js +//+ run +var user = { + firstName: "Вася", + surname: "Петров" +} + +Object.defineProperty(user, "fullName", { + + get: function() { + return this.firstName + ' ' + this.surname; + }, + +*!* + set: function(value) { + var split = value.split(' '); + this.firstName = split[0]; + this.surname = split[1]; + } +*/!* +}); + +*!* +user.fullName = "Петя Иванов"; +*/!* +alert(user.firstName); // Петя +alert(user.surname); // Иванов +``` + +## Геттеры и сеттеры в литералах + +Если мы создаём объект при помощи синтаксиса `{ ... }`, то задать геттеры/сеттеры можно прямо в его определении. + +Для этого используется особый синтаксис: `get свойство` или `set свойство`. + +Например, ниже объявлен геттер-сеттер `fullName`: + +```js +//+ run +var user = { + firstName: "Вася", + surname: "Петров", + +*!* + get fullName() { +*/!* + return this.firstName + ' ' + this.surname; + }, + +*!* + set fullName(value) { +*/!* + var split = value.split(' '); + this.firstName = split[0]; + this.surname = split[1]; + } +}; + +*!* +alert(user.fullName); // Вася Петров (из геттера) + +user.fullName = "Петя Иванов"; +alert(user.firstName); // Петя (поставил сеттер) +alert(user.surname); // Иванов (поставил сеттер) +*/!* +``` + +## Да здравствуют геттеры и сеттеры! + +Казалось бы, зачем нам назначать геттеры и сеттеры через всякие хитрые вызовы? Можно же сделать функции `getFullName`, `setFullName`... + +**Основной бонус -- возможность получить контроль над свойством в любой момент!** + +В начале разработки мы можем использовать обычные свойства, например у `User` будет имя `name` и возраст `age`: + +```js +function User(name, age) { + this.name = name; + this.age = age; +} + +var pete = new User("Петя", 25); + +alert(pete.age); // 25 +``` + +**С обычными свойствами в коде меньше букв, они удобны.** + +...Но рано или поздно может наступить расплата! + +Например, когда написано много кода, который использует эти свойства, формат данных изменился и теперь вместо возраста `age` хранится дата рождения `birthday`: + +```js +function User(name, birthday) { + this.name = name; + this.birthday = birthday; +} + +var pete = new User("Петя", new Date(1987, 6, 1)); +``` + +Что теперь делать со старым кодом, который выводит свойство `age`? + +Можно, конечно, найти все места и поправить их, но это долго, а иногда и невозможно, скажем, если вы взаимодействуете со сторонней библиотекой, код в которой -- чужой и влезать в него нежелательно. + +Геттеры позволяют обойти проблему легко и непринуждённо. + +Просто добавляем геттер `age`: + +```js +//+ run +function User(name, birthday) { + this.name = name; + this.birthday = birthday; + +*!* + Object.defineProperty(this, "age", { + get: function() { + var todayYear = new Date().getFullYear(); + return todayYear - this.birthday.getFullYear(); + } + }); +*/!* +} + +var pete = new User("Петя", new Date(1987, 6, 1)); + +alert(pete.age); // получает возраст из даты рождения +``` + +**Таким образом, `defineProperty` позволяет нам использовать обычные свойства и, при необходимости, в любой момент заменить их на функции, сохраняя совместимость внешнего интерфейса.** + +## Другие методы работы со свойствами + +
        +
        [Object.defineProperties(obj, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperties)
        +
        Позволяет объявить несколько свойств сразу: + +```js +//+ run +var user = {} + +Object.defineProperties(user, { +*!* + firstName: { +*/!* + value: "Петя" + }, + +*!* + surname: { +*/!* + value: "Иванов" + }, + +*!* + fullName: { +*/!* + get: function() { + return this.firstName + ' ' + this.surname; + } + } +}); + +alert( user.fullName ); // Петя Иванов +``` + +
        +
        [Object.keys(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys), [Object.getOwnPropertyNames(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames)
        +
        Возвращают массив -- список свойств объекта. + +При этом `Object.keys` возвращает только `enumerable`-свойства, а `Object.getOwnPropertyNames` -- все: + +```js +//+ run +var obj = { + a: 1, + b: 2, + internal: 3 +}; + +Object.defineProperty(obj, "internal", {enumerable: false}); + +*!* +alert( Object.keys(obj) ); // a,b +alert( Object.getOwnPropertyNames(obj) ); // a, internal, b +*/!* +``` + +
        +
        [Object.getOwnPropertyDescriptor(prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)
        +
        Возвращает дескриптор для свойства с `prop`. + +Полученный дескриптор можно изменить и использовать `defineProperty` для сохранения изменений, например: + +```js +//+ run +var obj = { test: 5 }; +*!* +var descriptor = Object.getOwnPropertyDescriptor(obj, 'test'); +*/!* + +*!* +// заменим value на геттер, для этого... +*/!* +delete descriptor.value; // ..нужно убрать value/writable +delete descriptor.writable; +descriptor.get = function() { // и поставить get + alert("Preved :)"); +}; + +*!* +// поставим новое свойство вместо старого +*/!* + +// если не удалить - defineProperty объединит старый дескриптор с новым +delete obj.test; + +Object.defineProperty(obj, 'test', descriptor); + +obj.test; // Preved :) +``` + +
        +
        + +...И несколько методов, которые используются очень редко: +
        +
        [Object.preventExtensions(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)
        +
        Запрещает добавление свойств в объект.
        +
        [Object.seal(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)
        +
        Запрещает добавление и удаление свойств, все текущие свойства делает `configurable: false`.
        +
        [Object.freeze(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze)
        +
        Запрещает добавление, удаление и изменение свойств, все текущие свойства делает `configurable: false, writable: false`.
        +
        [Object.isExtensible(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isExtensible), [Object.isSealed(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isSealed), [Object.isFrozen(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isFrozen)
        +
        Возвращают `true`, если на объекте были вызваны методы `Object.preventExtensions/seal/freeze`.
        +
        + diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.md b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.md new file mode 100644 index 00000000..5a64ee15 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.md @@ -0,0 +1,15 @@ +Изменения в методе `run`: + +```js +this.run = function() { +*!* + if (!this._enabled) { + throw new Error("Кофеварка выключена"); + } +*/!* + + setTimeout(onReady, 1000); +}; +``` + +[edit src="solution" /] diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.view/index.html b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.view/index.html new file mode 100755 index 00000000..5a2be21c --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/solution.view/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/source.view/index.html b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/source.view/index.html new file mode 100755 index 00000000..d742ef78 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/source.view/index.html @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/task.md b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/task.md new file mode 100644 index 00000000..fcbe592e --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/1-coffeemachine-fix-run/task.md @@ -0,0 +1,22 @@ +# Запускать только при включённой кофеварке + +[importance 5] + +В коде `CoffeeMachine` сделайте так, чтобы метод `run` выводил ошибку, если кофеварка выключена. + +В итоге должен работать такой код: + +```js +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.run(); // ошибка, кофеварка выключена! +``` + +А вот так -- всё в порядке: + +```js +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.enable(); +coffeeMachine.run(); // ...Кофе готов! +``` + +[edit src="source" task /] \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.md b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.md new file mode 100644 index 00000000..33000522 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.md @@ -0,0 +1 @@ +[edit src="solution"/] \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.view/index.html b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.view/index.html new file mode 100755 index 00000000..7a6ba8b0 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/solution.view/index.html @@ -0,0 +1,55 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/task.md b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/task.md new file mode 100644 index 00000000..298cd301 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/2-coffeemachine-disable-stop/task.md @@ -0,0 +1,16 @@ +# Останавливать кофеварку при выключении + +[importance 5] + +Когда кофеварку выключают -- текущая варка кофе должна останавливаться. + +Например, следующий код кофе не сварит: + +```js +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.enable(); +coffeeMachine.run(); +coffeeMachine.disable(); // остановит работу, ничего не выведет +``` + +Реализуйте это на основе решения [предыдущей задачи](/task/coffeemachine-fix-run). diff --git a/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/solution.md b/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/solution.md new file mode 100644 index 00000000..f5a6c779 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/3-inherit-fridge/solution.md @@ -0,0 +1,29 @@ +Решение: + +```js +function Fridge(power) { + // унаследовать + Machine.apply(this, arguments); + + var food = []; // приватное свойство food + + this.addFood = function() { + if (!this._enabled) { + throw new Error("Холодильник выключен"); + } + if (food.length + arguments.length >= this._power / 100) { + throw new Error("Нельзя добавить, не хватает мощности"); + } + for(var i=0; i +
      • Приватное свойство `food` хранит массив еды.
      • +
      • Публичный метод `addFood(item)` добавляет в массив `food` новую еду, доступен вызов с несколькими аргументами `addFood(item1, item2...)` для добавления нескольких элементов сразу.
      • +
      • Если холодильник выключен, то добавить еду нельзя, будет ошибка.
      • +
      • Максимальное количество еды ограничено `power/100`, где `power` -- мощность холодильника, указывается в конструкторе. При попытке добавить больше -- будет ошибка
      • +
      • Публичный метод `getFood()` возвращает еду в виде массива, добавление или удаление элементов из которого не должно влиять на свойство `food` холодильника.
      • +
      + +Код для проверки: + +```js +var fridge = new Fridge(200); +fridge.addFood("котлета"); // ошибка, холодильник выключен +``` + +Ещё код для проверки: + +```js +// создать холодильник мощностью 500 (не более 5 еды) +var fridge = new Fridge(500); +fridge.enable(); +fridge.addFood("котлета"); +fridge.addFood("сок", "зелень"); +fridge.addFood("варенье", "пирог", "торт"); // ошибка, слишком много еды +``` + +Код использования холодильника без ошибок: + +```js +var fridge = new Fridge(500); +fridge.enable(); +fridge.addFood("котлета"); +fridge.addFood("сок", "варенье"); + +var fridgeFood = fridge.getFood(); +alert(fridgeFood); // котлета, сок, варенье + +// добавление элементов не влияет на еду в холодильнике +fridgeFood.push("вилка", "ложка"); + +alert(fridge.getFood()); // внутри по-прежнему: котлета, сок, варенье +``` + +Исходный код класса `Machine`, от которого нужно наследовать: + +```js +function Machine(power) { + this._power = power; + this._enabled = false; + + var self = this; + + this.enable = function() { + self._enabled = true; + }; + + this.disable = function() { + self._enabled = false; + }; +} +``` + diff --git a/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/solution.md b/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/solution.md new file mode 100644 index 00000000..50758d57 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/4-add-methods-fridge/solution.md @@ -0,0 +1,77 @@ + + +```js +//+ run +function Machine(power) { + this._power = power; + this._enabled = false; + + var self = this; + + this.enable = function() { + self._enabled = true; + }; + + this.disable = function() { + self._enabled = false; + }; +} + +function Fridge(power) { + // унаследовать + Machine.apply(this, arguments); + + var food = []; // приватное свойство food + + this.addFood = function() { + if (!this._enabled) { + throw new Error("Холодильник выключен"); + } + if (food.length + arguments.length >= this._power / 100) { + throw new Error("Нельзя добавить, не хватает мощности"); + } + for(var i=0; i +
    5. Публичный метод `filterFood(func)`, который возвращает всю еду, для которой `func(item) == true`
    6. +
    7. Публичный метод `removeFood(item)`, который удаляет еду `item` из холодильника.
    8. + + +Код для проверки: + +```js +var fridge = new Fridge(500); +fridge.enable(); +fridge.addFood({ title: "котлета", calories: 100 }); +fridge.addFood({ title: "сок", calories: 30 }); +fridge.addFood({ title: "зелень", calories: 10 }); +fridge.addFood({ title: "варенье", calories: 150 }); + +fridge.removeFood("нет такой еды"); // без эффекта +alert(fridge.getFood().length); // 4 + +var dietItems = fridge.filterFood(function(item) { + return item.calories < 50; +}); + +dietItems.forEach(function(item) { + alert(item.title); // сок, зелень + fridge.removeFood(item); +}); + +alert(fridge.getFood().length); // 2 +``` + +В качестве исходного кода используйте решение [предыдущей задачи](/task/inherit-fridge). diff --git a/1-js/8-oop/5-functional-inheritance/5-override-disable/solution.md b/1-js/8-oop/5-functional-inheritance/5-override-disable/solution.md new file mode 100644 index 00000000..1ea8ed77 --- /dev/null +++ b/1-js/8-oop/5-functional-inheritance/5-override-disable/solution.md @@ -0,0 +1,68 @@ + + +```js +//+ run +function Machine(power) { + this._power = power; + this._enabled = false; + + var self = this; + + this.enable = function() { + self._enabled = true; + }; + + this.disable = function() { + self._enabled = false; + }; +} + +function Fridge(power) { + Machine.apply(this, arguments); + + var food = []; // приватное свойство food + + this.addFood = function() { + if (!this._enabled) { + throw new Error("Холодильник выключен"); + } + if (food.length + arguments.length >= this._power / 100) { + throw new Error("Нельзя добавить, не хватает мощности"); + } + for(var i=0; i -- включение, шнур с розеткой нужно воткнуть в питание и т.п. + +Можно сказать, что "у всех машин есть общие свойства, а конкретные машины могут их дополнять". + +Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию. + +**Механизм наследования позволяет определить базовый класс `Машина`, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: `Кофеварка`, `Холодильник` и т.п.** + +[smart header="В веб-разработке всё так же"] +В веб-разработке нам могут понадобиться классы `Меню`, `Табы`, `Диалог` и другие компоненты интерфейса. + +Можно выделить полезный общий функционал в класс `Компонент` и наследовать их от него, чтобы не дублировать код. Это обычная практика, принятая во множестве библиотек. +[/smart] + +## Наследование от Machine + +Например, у нас есть класс `Machine`, который реализует методы "включить" `enable()` и "выключить" `disable()`: + +```js +function Machine() { + var enabled = false; + + this.enable = function() { + enabled = true; + }; + + this.disable = function() { + enabled = false; + }; +} +``` + +Унаследуем от него кофеварку. При этом она получит эти методы автоматически: + +```js +function CoffeeMachine(power) { +*!* + Machine.call(this); +*/!* + var waterAmount = 0; + + this.setWaterAmount = function(amount) { + waterAmount = amount; + }; + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + setTimeout(onReady, 1000); + }; + +} + +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.enable(); +``` + +Наследование реализовано вызовом `Machine.call(this)` в начале `CoffeeMachine`. + +Он вызывает функцию `Machine`, передавая ей в качестве контекста `this` текущий объект. `Machine`, в процессе выполнения, записывает в `this` различные полезные свойства и методы, в нашем случае `this.enable` и `this.disable`. + +Далее `CoffeeMachine` продолжает выполнение и может добавить свои свойства и методы, а также пользоваться унаследованными. + +## Защищённые свойства + +В коде выше есть одна проблема. + +**Наследник не имеет доступа к приватным свойствам родителя.** + +Иначе говоря, если кофеварка захочет обратиться к `enabled`, то её ждёт разочарование: + +```js +//+ run +function Machine() { + var enabled = false; + + this.enable = function() { + enabled = true; + }; + + this.disable = function() { + enabled = false; + }; +} + +function CoffeeMachine(power) { + Machine.call(this); + + this.enable(); + +*!* + // ошибка, переменная не определена! + alert(enabled); +*/!* +} + +var coffeeMachine = new CoffeeMachine(10000); +``` + +Это естественно, ведь `enabled` -- локальная переменная функции `Machine`. Она находится в другой области видимости. + +**Чтобы наследник имел доступ к свойству, оно должно быть записано в `this`.** + +**При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания `_`.** + +```js +//+ run +function Machine() { +*!* + this._enabled = false; +*/!* + + this.enable = function() { + this._enabled = true; + }; + + this.disable = function() { + this._enabled = false; + }; +} + +function CoffeeMachine(power) { + Machine.call(this); + + this.enable(); + +*!* + alert(this._enabled); // true +*/!* +} + +var coffeeMachine = new CoffeeMachine(10000); +``` + +**Подчёркивание в начале свойства -- общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют *защищёнными*.** + +Технически это, конечно, возможно, но приличный программист снаружи в такое свойство не полезет. + +**Вообще, это стандартная практика: конструктор сохраняет свои параметры в свойствах объекта. Иначе наследники не будут иметь к ним доступ.** + +## Перенос свойства в защищённые + +В коде выше есть свойство `power`. Сейчас мы его тоже сделаем защищённым и перенесём в `Machine`, поскольку "мощность" свойственна всем машинам, а не только кофеварке: + +```js +//+ run +function CoffeeMachine(power) { +*!* + Machine.apply(this, arguments); // (1) +*/!* + + alert(this._enabled); // false + alert(this._power); // 10000 +} + +function Machine(power) { +*!* + this._power = power; // (2) +*/!* + + this._enabled = false; + + this.enable = function() { + this._enabled = true; + }; + + this.disable = function() { + this._enabled = false; + }; +} + +var coffeeMachine = new CoffeeMachine(10000); +``` + +В коде выше при вызове `new CoffeeMachine(10000)` в строке `(1)` кофеварка передаёт аргументы и контекст родителю вызовом `Machine.apply(this, arguments)`. + +Можно было бы использовать `Machine.call(this, power)`, но использование `apply` гарантирует передачу всех аргументов, мало ли, вдруг мы в будущем захотим их добавить. + +Далее конструктор `Machine` в строке `(2)` сохраняет `power` в свойстве объекта `this._power`, благодаря этому кофеварка, когда наследование перейдёт обратно к `CoffeeMachine`, сможет сразу обращаться к нему. + +## Переопределение методов + +Итак, мы получили класс `CoffeeMachine`, который наследует от `Machine`. + +Аналогичным образом мы можем унаследовать от `Machine` холодильник `Fridge`, микроволновку `MicroOven` и другие классы, которые разделяют общий "машинный" функционал. + +Для этого достаточно вызвать `Machine` текущем контексте, а затем добавить свои методы. + +```js +// Fridge может добавить и свои аргументы, +// которые в Machine не будут использованы +function Fridge(power, temperature) { + Machine.call(this, arguments); + + // ... +} +``` + +Кроме создания новых методов, можно заменить унаследованные на свои: + +```js +function CoffeeMachine(power, capacity) { + Machine.apply(this, arguments); + + // переопределить this.enable + this.enable = function() { + /* enable для кофеварки */ + }; +} +``` + +...Однако, как правило, мы хотим не заменить, а *расширить* метод родителя. Например, сделать так, чтобы при включении кофеварка тут же запускалась. + +Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового `enable` -- там, где считают нужным: + +```js +function CoffeeMachine(power) { + Machine.apply(this, arguments); + +*!* + var parentEnable = this.enable; // (1) + this.enable = function() { // (2) + parentEnable.call(this); // (3) + this.run(); // (4) + } +*/!* + + ... +} +``` + +**Общая схема переопределения метода (по строкам выделенного фрагмента кода):** + +
        +
      1. Мы скопировали доставшийся от родителя метод `enable` в переменную, например `parentEnable`.
      2. +
      3. Заменили метод `this.enable()` на свою функцию...
      4. +
      5. Которая по-прежнему реализует старый функционал через вызов `parentEnable`...
      6. +
      7. И в дополнение к нему делает что-то своё, например запускает приготовление кофе.
      8. +
      + +Обратим внимание на строку `(3)`. + +В ней родительский метод вызывается так: `parentEnable.call(this)`. Если бы вызов был таким: `parentEnable()`, то ему бы не передался текущий `this` и возникла бы ошибка. + +Технически, можно сделать возможность вызывать его и как `parentEnable()`, но тогда надо гарантировать, что контекст будет правильным, например привязать его при помощи `bind` или при объявлении, в родителе, вообще не использовать `this`, а получать контекст через замыкание, вот так: + +```js +//+ run +function Machine(power) { + this._enabled = false; + +*!* + var self = this; + + this.enable = function() { + // используем внешнюю переменную вместо this + self._enabled = true; + }; +*/!* + + this.disable = function() { + this._enabled = false; + }; + +} + +function CoffeeMachine(power) { + Machine.apply(this, arguments); + + var waterAmount = 0; + + this.setWaterAmount = function(amount) { + waterAmount = amount; + }; + +*!* + var parentEnable = this.enable; + this.enable = function() { + parentEnable(); // теперь можно вызывать как угодно, this не важен + this.run(); + } +*/!* + + function onReady() { + alert('Кофе готово!'); + } + + this.run = function() { + setTimeout(onReady, 1000); + }; + +} + +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.setWaterAmount(50); +coffeeMachine.enable(); +``` + +В коде выше родительский метод `parentEnable = this.enable` успешно продолжает работать даже при вызове без контекста. А всё потому, что использует `self` внутри. + +## Итого + +Организация наследования, которая описана в этой главе, называется "функциональным паттерном наследования". + +Её общая схема (кратко): + +
        +
      1. Объявляется конструктор родителя `Machine`. В нём могут быть приватные (private), публичные (public) и защищённые (protected) свойства: + +```js +function Machine(params) { + // локальные переменные и функции доступны только внутри Machine + var private; + + // публичные доступны снаружи + this.public = ...; + + // защищённые доступны внутри Machine и для потомков + // мы договариваемся не трогать их снаружи + this._protected = ... +} + +var machine = new Machine(...) +machine.public(); +``` + +
      2. +
      3. Для наследования конструктор потомка вызывает родителя в своём контексте через `apply`. После чего может добавить свои переменные и методы: + +```js +function CoffeeMachine(params) { + // универсальный вызов с передачей любых аргументов +*!* + Machine.apply(this, arguments); +*/!* + + this.coffeePublic = ... +} + +var coffeeMachine = new CoffeeMachine(...); +coffeeMachine.public(); +coffeeMachine.coffeePublic(); +``` + +
      4. +
      5. В `CoffeeMachine` свойства, полученные от родителя, можно перезаписать своими. Но обычно требуется не заменить, а расширить метод родителя. Для этого он предварительно копируется в переменную: + +```js +function CoffeeMachine(params) { + Machine.apply(this, arguments); + +*!* + var parentProtected = this._protected; + this._protected = function(args) { + parentProtected.call(this, args); // (*) + // ... + }; +*/!* +} +``` + +Строку `(*)` можно упростить до `parentProtected(args)`, если метод родителя не использует `this`, а, например, привязан к `var self = this`: + +```js +function Machine(params) { + var self = this; + + this._protected = function() { + self.property = "value"; + }; +} +``` + +
      6. +
      + +**В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ, по сравнению с функциональным.** + +Но функциональный тоже бывает полезен. + +В своей практике разработки я обычно наследую функционально в тех случаях, когда *уже* есть какой-то код, который на нём построен. К примеру, уже существуют классы, написанные сторонними разработчиками, которые можно доопределить или расширить только так. + + + + + diff --git a/1-js/8-oop/index.md b/1-js/8-oop/index.md new file mode 100644 index 00000000..292b2d77 --- /dev/null +++ b/1-js/8-oop/index.md @@ -0,0 +1,3 @@ +# ООП в функциональном стиле + +Инкапсуляция и наследование в функциональном стиле, а также расширенные возможности объектов JavaScript. \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/1-property-after-delete/solution.md b/1-js/9-prototypes/1-prototype/1-property-after-delete/solution.md new file mode 100644 index 00000000..f6308c17 --- /dev/null +++ b/1-js/9-prototypes/1-prototype/1-property-after-delete/solution.md @@ -0,0 +1,5 @@ +
        +
      1. `true`, свойство взято из `rabbit`.
      2. +
      3. `null`, свойство взято из `animal`.
      4. +
      5. `undefined`, свойства больше нет.
      6. +
      \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/1-property-after-delete/task.md b/1-js/9-prototypes/1-prototype/1-property-after-delete/task.md new file mode 100644 index 00000000..cdcc0a9a --- /dev/null +++ b/1-js/9-prototypes/1-prototype/1-property-after-delete/task.md @@ -0,0 +1,24 @@ +# Чему равно cвойство после delete? + +[importance 5] + +Какие значения будут выводиться в коде ниже? + +```js +var animal = { jumps: null }; +var rabbit = { jumps: true }; + +rabbit.__proto__ = animal; + +alert( rabbit.jumps ); // ? (1) + +delete rabbit.jumps; + +alert( rabbit.jumps ); // ? (2) + +delete animal.jumps; + +alert( rabbit.jumps); // ? (3) +``` + +Итого три вопроса. \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/1.png b/1-js/9-prototypes/1-prototype/1.png new file mode 100755 index 00000000..85778ffc Binary files /dev/null and b/1-js/9-prototypes/1-prototype/1.png differ diff --git a/1-js/9-prototypes/1-prototype/1@2x.png b/1-js/9-prototypes/1-prototype/1@2x.png new file mode 100755 index 00000000..3439b25e Binary files /dev/null and b/1-js/9-prototypes/1-prototype/1@2x.png differ diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/proto5.png b/1-js/9-prototypes/1-prototype/2-proto-and-this/proto5.png new file mode 100755 index 00000000..b9c002f0 Binary files /dev/null and b/1-js/9-prototypes/1-prototype/2-proto-and-this/proto5.png differ diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/proto6.png b/1-js/9-prototypes/1-prototype/2-proto-and-this/proto6.png new file mode 100755 index 00000000..c9eb114f Binary files /dev/null and b/1-js/9-prototypes/1-prototype/2-proto-and-this/proto6.png differ diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/solution.md b/1-js/9-prototypes/1-prototype/2-proto-and-this/solution.md new file mode 100644 index 00000000..4206298f --- /dev/null +++ b/1-js/9-prototypes/1-prototype/2-proto-and-this/solution.md @@ -0,0 +1,18 @@ +**Ответ: свойство будет записано в `rabbit`.** + +Если коротко -- то потому что `this` будет указывать на `rabbit`, а прототип при записи не используется. + +Если в деталях -- посмотрим как выполняется `rabbit.eat()`: +
        +
      1. Интерпретатор ищет `rabbit.eat`, чтобы его вызвать. Но свойство `eat` отсутствует в объекте `rabbit`, поэтому он идет по ссылке `rabbit.__proto__` и находит это свойство там. + +
      2. +
      3. Функция `eat` запускается. Контекст ставится равным объекту перед точкой, т.е. `this = rabbit`. + +Итак -- получается, что команда `this.full = true` устанавливает свойство `full` в самом объекте `rabbit`. Итог: + + +
      4. +
      + +Эта задача демонстрирует, что несмотря на то, в каком прототипе находится свойство, это никак не влияет на установку `this`, которая осуществляется по своим, независимым правилам. \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/2-proto-and-this/task.md b/1-js/9-prototypes/1-prototype/2-proto-and-this/task.md new file mode 100644 index 00000000..a5ac9430 --- /dev/null +++ b/1-js/9-prototypes/1-prototype/2-proto-and-this/task.md @@ -0,0 +1,24 @@ +# Прототип и this + +[importance 5] + +Сработает ли вызов `rabbit.eat()` ? + +Если да, то в какой именно объект он запишет свойство `full`: в `rabbit` или `animal`? + +```js +var animal = { + eat: function() { + this.full = true; + } +}; + +var rabbit = { + __proto__: animal +}; + +*!* +rabbit.eat(); +*/!* +``` + diff --git a/1-js/9-prototypes/1-prototype/3-search-algorithm/solution.md b/1-js/9-prototypes/1-prototype/3-search-algorithm/solution.md new file mode 100644 index 00000000..60ba9501 --- /dev/null +++ b/1-js/9-prototypes/1-prototype/3-search-algorithm/solution.md @@ -0,0 +1,33 @@ +
        +
      1. Расставим `__proto__`: + +```js +//+ run +var head = { + glasses: 1 +}; + +var table = { + pen: 3 +}; +table.__proto__ = head; + +var bed = { + sheet: 1, + pillow: 2 +}; +bed.__proto__ = table; + +var pockets = { + money: 2000 +}; +pockets.__proto__ = bed; + +alert( pockets.pen ); // 3 +alert( bed.glasses ); // 1 +alert( table.money ); // undefined +``` + +
      2. +
      3. **В современных браузерах, с точки зрения производительности, нет разницы, брать свойство из объекта или прототипа.** Они запоминают, где было найдено свойство и в следующий раз при запросе, к примеру, `pockets.glasses` начнут искать сразу в прототипе (`head`).
      4. +
      \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/3-search-algorithm/task.md b/1-js/9-prototypes/1-prototype/3-search-algorithm/task.md new file mode 100644 index 00000000..ab5f9f16 --- /dev/null +++ b/1-js/9-prototypes/1-prototype/3-search-algorithm/task.md @@ -0,0 +1,32 @@ +# Алгоритм для поиска + +[importance 5] + +Есть объекты: + +```js +var head = { + glasses: 1 +}; + +var table = { + pen: 3 +}; + +var bed = { + sheet: 1, + pillow: 2 +}; + +var pockets = { + money: 2000 +}; +``` + +Задание состоит из двух частей: +
        +
      1. Присвойте объектам ссылки `__proto__` так, чтобы любой поиск чего-либо шёл по алгоритму `pockets -> bed -> table -> head`. + +То есть `pockets.pen == 3`, `bed.glasses == 1`, но `table.money == undefined`.
      2. +
      3. После этого ответьте на вопрос, как быстрее искать `glasses`: обращением к `pockets.glasses` или `head.glasses`? Попробуйте протестировать.
      4. +
      diff --git a/1-js/9-prototypes/1-prototype/article.md b/1-js/9-prototypes/1-prototype/article.md new file mode 100644 index 00000000..2eaed2c1 --- /dev/null +++ b/1-js/9-prototypes/1-prototype/article.md @@ -0,0 +1,211 @@ +# Прототип объекта + +Объекты в JavaScript можно организовать в цепочки, так, чтобы если свойство не найдено в одном объекте -- оно автоматически искалось бы его в другом. + +Связующим звеном выступает специальное свойство `__proto__`. + +[cut] +## Прототип __proto__ + +Если один объект имеет специальную ссылку `__proto__` на другой объект, то при чтении свойства из него, если свойство отсутствует в самом объекте, оно ищется в объекте `__proto__`. + +Свойство доступно во всех браузерах, кроме IE10-. Впрочем, в старых IE оно, на самом деле, тоже есть, но требуются чуть более сложные способы для работы с ним, которые мы рассмотрим позднее. + +Пример кода (кроме IE10-): + +```js +//+ run +var animal = { eats: true }; +var rabbit = { jumps: true }; + +*!* +rabbit.__proto__ = animal; +*/!* + +// в rabbit можно найти оба свойства +alert(rabbit.jumps); // true +alert(rabbit.eats); // true +``` + +
        +
      1. Первый `alert` здесь работает очевидным образом -- он выводит свойство `jumps` объекта `rabbit`.
      2. +
      3. Второй `alert` хочет вывести `rabbit.eats`, ищет его в самом объекте `rabbit`, не находит -- и продолжает поиск в объекте `rabbit.__proto__`, то есть, в данном случае, в `animal`.
      4. +
      + +Иллюстрация происходящего (поиск идет снизу вверх): + + + +**Объект, на который указывает ссылка `__proto__`, называется *"прототипом"*. В данном случае получилось, что `animal` является прототипом для `rabbit`.** + +**Также говорят, что объект `rabbit` *"прототипно наследует"* от `animal`.** + +Обратим внимание -- прототип используется исключительно при чтении. Запись значения, например, `rabbit.eats = value` или удаление `delete rabbit.eats` -- работает напрямую с объектом. + +В примере ниже мы записываем свойство в сам `rabbit`, после чего `alert` перестаёт брать его у прототипа, а берёт уже из самого объекта: + +```js +//+ run +var animal = { eats: true }; +var rabbit = { jumps: true, eats: false }; + +rabbit.__proto__ = animal; + +*!* +alert(rabbit.eats); // false, свойство взято из rabbit +*/!* +``` + +**Другими словами, прототип -- это "резервное хранилище свойств и методов" объекта, автоматически используемое при поиске.** + +[smart header="Ссылка __proto__ в спецификации"] +Если вы будете читать спецификацию EcmaScript -- свойство `__proto__` обозначено в ней как `[[Prototype]]`. + +Двойные квадратные скобки здесь важны, чтобы не перепутать его с совсем другим свойством, которое называется `prototype`, и которое мы рассмотрим позже. +[/smart] + + +## Цепочка прототипов + +У объекта, который является `__proto__`, может быть свой `__proto__`, у того -- свой, и так далее. + +Например, цепочка наследования из трех объектов `donkey -> winnie -> owl`: + +```js +//+ run +var donkey = { /* ... */ }; +var winnie = { /* ... */ }; +var owl = { knowsAll: true }; + +donkey.__proto__ = winnie; +winnie.__proto__ = owl; + +*!* +alert( donkey.knowsAll ); // true +*/!* +``` + +Картина происходящего: + + + +## Перебор свойств без прототипа + +Обычный цикл `for..in` не делает различия между свойствами объекта и его прототипа. + +Он перебирает всё, например: + +```js +//+ run +var animal = { + eats: true +}; + +var rabbit = { + jumps: true, + __proto__: animal +}; + +*!* +for (var key in rabbit) { + alert (key + " = " + rabbit[key]); // выводит и "eats" и "jumps" +} +*/!* +``` + +Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе. + +**Вызов [obj.hasOwnProperty(prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/HasOwnProperty) возвращает `true`, если свойство `prop` принадлежит самому объекту `obj`, иначе `false`.** + +Например: + +```js +//+ run +var animal = { + eats: true +}; + +var rabbit = { + jumps: true, + __proto__: animal +}; + +*!* +alert( rabbit.hasOwnProperty('jumps') ); // true: jumps принадлежит rabbit + +alert( rabbit.hasOwnProperty('eats') ); // false: eats не принадлежит +*/!* +``` + +Для того, чтобы перебрать свойства самого объекта, достаточно профильтровать `key` через `hasOwnProperty`: + +```js +//+ run +var animal = { + eats: true +}; + +var rabbit = { + jumps: true, + __proto__: animal +}; + +for (var key in rabbit) { +*!* + if ( !rabbit.hasOwnProperty(key) ) continue; // пропустить "не свои" свойства +*/!* + alert (key + " = " + rabbit[key]); // выводит только "jumps" +} +``` + +## Методы для работы с __proto__ + +В современных браузерах есть два дополнительных метода для работы с `__proto__`. Зачем они нужны, если есть `__proto__`? В общем-то, не очень нужны, но по историческим причинам тоже существуют. + +
      +
      [Object.getPrototypeOf(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getPrototypeOf)
      +
      Возвращает `obj.__proto__` (кроме IE8-)
      +
      [Object.setPrototypeOf(obj, proto)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/setPrototypeOf)
      +
      Устанавливает `obj.__proto__ = proto` (кроме IE10-).
      +
      + +Кроме того, есть ещё один вспомогательный метод: +
      [Object.create(proto)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create)
      +
      Создаёт пустой объект с `__proto__`, равным первому аргументу (кроме IE8-).
      +
      + +Метод `Object.create` -- несколько более мощный, чем здесь описано, у него есть необязательный второй аргумент, который позволяет также задать другие свойства объекта, но используется он редко и пока что нам не нужен. Мы рассмотрим его позже, в главе [](/descriptors-getters-setters). + +## Итого + +
        +
      • Объекты в JavaScript можно организовать в цепочку при помощи специального свойства `__proto__`.
      • +
      • При установке свойства `rabbit.__proto__ = animal` говорят, что объект `animal` будет "прототипом" `rabbit`.
      • +
      • При чтении свойства из объекта, если его в нём нет, оно ищется в `__proto__`. Прототип задействуется только при чтении свойства. Операции присвоения `obj.prop =` или удаления `delete obj.prop` совершаются всегда над самим объектом `obj`.
      • +
      + +Несколько прототипов одному объекту присвоить нельзя, но можно организовать объекты в цепочку, когда один объект ссылается на другой при помощи `__proto__`, тот ссылается на третий, и так далее. + +В современных браузерах есть методы для работы с прототипом: + +
        +
      • [Object.getPrototypeOf(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getPrototypeOf) (кроме IE8-)
      • +
      • [Object.setPrototypeOf(obj, proto)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) (кроме IE10-)
      • +
      • [Object.create(proto)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create) (кроме IE8-)
      • +
      + +Возможно, вас смущает недостаточная поддержка `__proto__` в старых IE. Но это временно. В последующих главах мы рассмотрим дополнительные методы работы с `__proto__`, включая те, которые работают везде. + +Также мы рассмотрим, как свойство `__proto__` используется внутри самого языка JavaScript. + + + +[head] + +[/head] \ No newline at end of file diff --git a/1-js/9-prototypes/1-prototype/donkey_winnie_owl.png b/1-js/9-prototypes/1-prototype/donkey_winnie_owl.png new file mode 100755 index 00000000..82352906 Binary files /dev/null and b/1-js/9-prototypes/1-prototype/donkey_winnie_owl.png differ diff --git a/1-js/9-prototypes/1-prototype/donkey_winnie_owl@2x.png b/1-js/9-prototypes/1-prototype/donkey_winnie_owl@2x.png new file mode 100755 index 00000000..3a3d1ca3 Binary files /dev/null and b/1-js/9-prototypes/1-prototype/donkey_winnie_owl@2x.png differ diff --git a/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/solution.md b/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/solution.md new file mode 100644 index 00000000..9256a9fb --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/solution.md @@ -0,0 +1,9 @@ +Результат: `true`, из прототипа + +Результат: `true`. Свойство `prototype` всего лишь задаёт `__proto__` у новых объектов. Так что его изменение не повлияет на `rabbit.__proto__`. Свойство `eats` будет получено из прототипа. + +Результат: `false`. Свойство `Rabbit.prototype` и `rabbit.__proto__` указывают на один и тот же объект. В данном случае изменения вносятся в сам объект. + +Результат: `true`, так как `delete rabbit.eats` попытается удалить `eats` из `rabbit`, где его и так нет. А чтение в `alert` произойдёт из прототипа. + +Результат: `undefined`. Удаление осуществляется из самого прототипа, поэтому свойство `rabbit.eats` больше взять неоткуда. \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/task.md b/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/task.md new file mode 100644 index 00000000..5ebab1fe --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/1-prototype-after-new/task.md @@ -0,0 +1,79 @@ +# Прототип после создания + +[importance 5] + +В примерах ниже создаётся объект `new Rabbit`, а затем проводятся различные действия с `prototype`. + +Каковы будут результаты выполнения? Почему? + +Начнём с этого кода. Что он выведет? + +```js +function Rabbit() { } +Rabbit.prototype = { eats: true }; + +var rabbit = new Rabbit(); + +alert(rabbit.eats); +``` + +Добавили строку (выделена), что будет теперь? + +```js +function Rabbit() { } +Rabbit.prototype = { eats: true }; + +var rabbit = new Rabbit(); + +*!* +Rabbit.prototype = {}; +*/!* + +alert(rabbit.eats); +``` + +А если код будет такой? (заменена одна строка): + +```js +function Rabbit(name) { } +Rabbit.prototype = { eats: true }; + +var rabbit = new Rabbit(); + +*!* +Rabbit.prototype.eats = false; +*/!* + +alert(rabbit.eats); +``` + +А такой? (заменена одна строка) + +```js +function Rabbit(name) { } +Rabbit.prototype = { eats: true }; + +var rabbit = new Rabbit(); + +*!* +delete rabbit.eats; // (*) +*/!* + +alert(rabbit.eats); +``` + +И последний вариант: + +```js +function Rabbit(name) { } +Rabbit.prototype = { eats: true }; + +var rabbit = new Rabbit(); + +*!* +delete Rabbit.prototype.eats; // (*) +*/!* + +alert(rabbit.eats); +``` + diff --git a/1-js/9-prototypes/2-new-prototype/2-default-arguments/solution.md b/1-js/9-prototypes/2-new-prototype/2-default-arguments/solution.md new file mode 100644 index 00000000..7dec9540 --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/2-default-arguments/solution.md @@ -0,0 +1,15 @@ +Можно прототипно унаследовать от `options` и добавлять/менять опции в наследнике: + +```js +//+ run +function Menu(options) { + options = Object.create(options); + options.width = options.width || 300; + + alert(options.width); // возьмёт width из наследника + alert(options.height); // возьмёт height из исходного объекта + ... +} +``` + +Все изменения будут происходить не в самом `options`, а в его наследнике, при этом исходный объект останется незатронутым. diff --git a/1-js/9-prototypes/2-new-prototype/2-default-arguments/task.md b/1-js/9-prototypes/2-new-prototype/2-default-arguments/task.md new file mode 100644 index 00000000..1f1c84d5 --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/2-default-arguments/task.md @@ -0,0 +1,27 @@ +# Аргументы по умолчанию + +[importance 4] + +Есть функция `Menu`, которая получает аргументы в виде объекта `options`: + +```js +/* options содержит настройки меню: width, height и т.п. */ +function Menu(options) { + ... +} +``` + +Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте `options`: + +```js +function Menu(options) { + options.width = options.width || 300; // по умолчанию ширина 300 + ... +} +``` + +...Но такие изменения могут привести к непредвиденным результатам, т.к. объект `options` может быть повторно использован во внешнем коде. Он передается в `Menu` для того, чтобы параметры из него читали, а не писали. + +Один из способов безопасно назначить значения по умолчанию -- скопировать все свойства `options` в локальные переменные и затем уже менять. Другой способ -- клонировать `options` путём копирования всех свойств из него в новый объект, который уже изменяется. + +При помощи наследования и `Object.create` предложите третий способ, который позволяет избежать копирования объекта и не требует новых переменных. diff --git a/1-js/9-prototypes/2-new-prototype/3-compare-calls/solution.md b/1-js/9-prototypes/2-new-prototype/3-compare-calls/solution.md new file mode 100644 index 00000000..ac2d442b --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/3-compare-calls/solution.md @@ -0,0 +1,29 @@ +# Разница между вызовами + +Первый вызов ставит `this == rabbit`, остальные ставят `this` равным `Rabbit.prototype`, следуя правилу "`this` -- объект перед точкой". + +Так что только первый вызов выведет `Rabbit`, в остальных он будет `undefined`. + +Код для проверки: + +```js +//+ run +function Rabbit(name) { this.name = name; } +Rabbit.prototype.sayHi = function() { alert(this.name); } + +var rabbit = new Rabbit("Rabbit"); + +rabbit.sayHi(); +Rabbit.prototype.sayHi(); +Object.getPrototypeOf(rabbit).sayHi(); +rabbit.__proto__.sayHi(); +``` + +# Совместимость + +
        +
      1. Первый вызов работает везде.
      2. +
      3. Второй вызов работает везде.
      4. +
      5. Третий вызов не будет работать в IE8-, там нет метода `getPrototypeOf`
      6. +
      7. Четвёртый вызов -- самый "несовместимый", он не будет работать в IE10-, ввиду отсутствия свойства `__proto__`.
      8. +
      \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/3-compare-calls/task.md b/1-js/9-prototypes/2-new-prototype/3-compare-calls/task.md new file mode 100644 index 00000000..98f1d037 --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/3-compare-calls/task.md @@ -0,0 +1,23 @@ +# Есть ли разница между вызовами? + +[importance 5] + +Создадим новый объект, вот такой: + +```js +function Rabbit(name) { this.name = name; } +Rabbit.prototype.sayHi = function() { alert(this.name); } + +var rabbit = new Rabbit("Rabbit"); +``` + +Одинаково ли сработают эти вызовы? + +```js +rabbit.sayHi(); +Rabbit.prototype.sayHi(); +Object.getPrototypeOf(rabbit).sayHi(); +rabbit.__proto__.sayHi(); +``` + +Все ли они являются кросс-браузерными? Если нет -- в каких браузерах сработает каждый? \ No newline at end of file diff --git a/1-js/9-prototypes/2-new-prototype/article.md b/1-js/9-prototypes/2-new-prototype/article.md new file mode 100644 index 00000000..f9e80be1 --- /dev/null +++ b/1-js/9-prototypes/2-new-prototype/article.md @@ -0,0 +1,156 @@ +# Свойство F.prototype и создание объектов через new + +До этого момента мы говорили о наследовании объектов, объявленных через `{...}`. + +Но что, если объекты создаются функцией-конструктором через `new`? Как указать прототип в этом случае? +[cut] + +## Свойство F.prototype + +Самым очевидным решением является назначение `__proto__` в конструкторе. + +Например, если я хочу, чтобы у всех объектов, которые создаются `new Rabbit`, был прототип `animal`, я могу сделать так: + +```js +//+ run +var animal = { eats: true } + +function Rabbit(name) { + this.name = name; +*!* + this.__proto__ = animal; +*/!* +} + +var rabbit = new Rabbit("Кроль"); + +alert( rabbit.eats ); // true, из прототипа +``` + +Недостаток этого подхода -- он не работает в IE10-. + +К счастью, в JavaScript с древнейших времён существует альтернативный, встроенный в язык и полностью кросс-браузерный способ. + +**Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство `prototype`.** + +**При создании объекта через `new`, в его прототип `__proto__` записывается ссылка из `prototype` функции-конструктора.** + +Например, код ниже полностью аналогичен предыдущему, но работает всегда и везде: + +```js +//+ run +var animal = { eats: true }; + +function Rabbit(name) { + this.name = name; +} + +*!* +Rabbit.prototype = animal; +*/!* + +var rabbit = new Rabbit("Кроль"); // rabbit.__proto__ == animal + +alert( rabbit.eats ); // true +``` + +Установка `Rabbit.prototype = animal` буквально говорит интерпретатору следующее: *"При создании объекта через `new Rabbit` запиши ему `__proto__ = animal`".* + +[smart header="Свойство `prototype` имеет смысл только у конструктора"] +Свойство `prototype` можно указать на любом объекте, но особый смысл оно имеет, лишь если назначено функции-конструктору. + +Само по себе оно вообще ничего не делает, его единственное назначение -- ставить `__proto__` новым объектам. +[/smart] + + + +[warn header="Значением `prototype` может быть только объект"] +Технически, в это свойство можно записать что угодно. + +Однако, при работе `new`, свойство `prototype` будет использовано лишь в том случае, если это объект. Примитивное значение, такое как число или строка, будет проигнорировано. +[/warn] + +## Эмуляция Object.create для IE8- [#inherit] + +Как мы только что видели, с конструкторами всё просто, назначить прототип можно кросс-браузерно при помощи `F.prototype`. + +Теперь вернёмся к созданию объектов без конструктора. + +Мы знаем, что в этом случае можно указывать прототип при помощи `__proto__`, но это не работает в IE10-. Также мы знаем, что есть метод `Object.create(proto)`, который создаёт пустой объект с данным прототипом, но он не работает в IE8-. + +**Используя `prototype`, вызов `Object.create` можно легко эмулировать, так что он будет работать во всех браузерах, включая даже очень-очень старые.** + +Кросс-браузерный аналог -- назовём его `inherit`, состоит буквально из нескольких строк: + +```js +function inherit(proto) { + function F() {} + F.prototype = proto; + var object = new F; + return object; +} +``` + +Результат вызова `inherit(animal)` идентичен `Object.create(animal)`. Это будет новый пустой объект с прототипом `animal`. + +Например: + +```js +//+ run +var animal = { eats: true }; + +var rabbit = inherit(animal); + +alert(rabbit.eats); // true +``` + +Посмотрите внимательно на функцию `inherit` и вы, наверняка, сами поймёте, как она работает... + +Давайте, на всякий случай, пройдём её по шагам: + +```js +function inherit(proto) { + function F() {} // (1) + F.prototype = proto // (2) + var object = new F; // (3) + return object; // (4) +} +``` + +
        +
      1. Создана новая функция `F`. Она ничего не делает с `this`, так что если вызвать `new F`, то получим пустой объект.
      2. +
      3. Свойство `F.prototype` устанавливается в будущий прототип `proto`
      4. +
      5. Результатом вызова `new F` будет пустой объект с `__proto__` равным значению `F.prototype`.
      6. +
      7. Мы получили пустой объект с заданным прототипом, как и хотели. Возвратим его.
      8. +
      + + +Эта функция широко используется в библиотеках и фреймворках. + +Здесь и далее мы будем использовать `Object.create`, предполагая что для IE8- выполнен код: + +```js +if (!Object.create) Object.create = inherit; /* определение inherit - выше */ +``` + +В частности, аналогичным образом работает библиотека [es5-shim](https://github.com/es-shims/es5-shim), при подключении которой `Object.create` станет доступен для всех браузеров. + +## Итого + +
        +
      • Прототип новых объектов, создаваемых через `new`, можно задавать кросс-браузерно, при помощи свойства конструктора `prototype`.
      • +
      • При создании объекта через `new F`, в его `__proto__` записывается ссылка на объект `F.prototype`.
      • +
      • Современный метод `Object.create(proto)` можно эмулировать его при помощи `prototype`, если хочется, чтобы он работал в IE8-.
      • +
      + + + +[head] + +[/head] \ No newline at end of file diff --git a/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/solution.md b/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/solution.md new file mode 100644 index 00000000..2db51390 --- /dev/null +++ b/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/solution.md @@ -0,0 +1,15 @@ + + +```js +//+ run +Function.prototype.defer = function(ms) { + setTimeout(this, ms); +} + +function f() { + alert("привет"); +} + +f.defer(1000); // выведет "привет" через 1 секунду +``` + diff --git a/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/task.md b/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/task.md new file mode 100644 index 00000000..01e01567 --- /dev/null +++ b/1-js/9-prototypes/3-native-prototypes/1-defer-to-prototype/task.md @@ -0,0 +1,16 @@ +# Добавить функциям defer + +[importance 5] + +Добавьте всем функциям в прототип метод `defer(ms)`, который откладывает вызов функции на `ms` миллисекунд. + +После этого должен работать такой код: + +```js +function f() { + alert("привет"); +} + +f.defer(1000); // выведет "привет" через 1 секунду +``` + diff --git a/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/solution.md b/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/solution.md new file mode 100644 index 00000000..9a262a07 --- /dev/null +++ b/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/solution.md @@ -0,0 +1,22 @@ + + +```js +//+ run +Function.prototype.defer = function(ms) { + var f = this; + return function() { + var args = arguments, context = this; + setTimeout(function() { + f.apply(context, args); + }, ms); + } +} + +// проверка +function f(a, b) { + alert(a + b); +} + +f.defer(1000)(1, 2); // выведет 3 через 1 секунду. +``` + diff --git a/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/task.md b/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/task.md new file mode 100644 index 00000000..e7efba30 --- /dev/null +++ b/1-js/9-prototypes/3-native-prototypes/2-defer-to-prototype-extended/task.md @@ -0,0 +1,19 @@ +# Добавить функциям defer с аргументами + +[importance 4] + +Добавьте всем функциям в прототип метод defer(ms), который возвращает обёртку, откладывающую вызов функции на ms миллисекунд. + +Например, должно работать так: + +```js +function f(a, b) { + alert(a + b); +} + +f.defer(1000)(1, 2); // выведет 3 через 1 секунду. +``` + +То есть, должны корректно передаваться аргументы. + + diff --git a/1-js/9-prototypes/3-native-prototypes/5.png b/1-js/9-prototypes/3-native-prototypes/5.png new file mode 100755 index 00000000..f4202de0 Binary files /dev/null and b/1-js/9-prototypes/3-native-prototypes/5.png differ diff --git a/1-js/9-prototypes/3-native-prototypes/5@2x.png b/1-js/9-prototypes/3-native-prototypes/5@2x.png new file mode 100755 index 00000000..9adfa463 Binary files /dev/null and b/1-js/9-prototypes/3-native-prototypes/5@2x.png differ diff --git a/1-js/9-prototypes/3-native-prototypes/6.png b/1-js/9-prototypes/3-native-prototypes/6.png new file mode 100755 index 00000000..a4ab88fd Binary files /dev/null and b/1-js/9-prototypes/3-native-prototypes/6.png differ diff --git a/1-js/9-prototypes/3-native-prototypes/6@2x.png b/1-js/9-prototypes/3-native-prototypes/6@2x.png new file mode 100755 index 00000000..6ac1ba26 Binary files /dev/null and b/1-js/9-prototypes/3-native-prototypes/6@2x.png differ diff --git a/1-js/9-prototypes/3-native-prototypes/article.md b/1-js/9-prototypes/3-native-prototypes/article.md new file mode 100644 index 00000000..590286ec --- /dev/null +++ b/1-js/9-prototypes/3-native-prototypes/article.md @@ -0,0 +1,293 @@ +# Встроенные "классы" в JavaScript + +В JavaScript есть встроенные объекты: `Date`, `Array`, `Object` и другие. Они используют прототипы и демонстрируют организацию "псевдоклассов" на JavaScript, которую мы вполне можем применить и для себя. + +[cut] + +## Откуда методы у {} ? + +Начнём мы с того, что создадим пустой объект и выведем его. + +```js +//+ run +var obj = { }; +alert( obj ); // "[object Object]" ? +``` + +В объекте, очевидно, ничего нет... Но кто же тогда генерирует строковое представление для `alert(obj)`? + +## Object.prototype + +...Конечно же, это сделал метод `toString`, который находится во встроенном прототипе `Object.prototype`. Этот прототип ставится всем объектам `Object` при создании и содержит все встроенные методы и свойства для объектов. + +В деталях, работает это так: +
        +
      1. Запись `obj = {}` является краткой формой `obj = new Object`, где `Object` -- встроенная функция-конструктор для объектов.
      2. +
      3. При выполнении `new Object`, создаваемому объекту ставится `__proto__` по `prototype` конструктора, то есть в данном случае `Object.prototype`.
      4. +
      5. В дальнейшем при обращении к `obj.toString()` -- функция будет взята из `Object.prototype`.
      6. +
      + + +Это можно легко проверить: + +```js +//+ run +var obj = { }; + +// метод берётся из прототипа? +alert(obj.toString == Object.prototype.toString); // true, да + +// проверим, правда ли что __proto__ это Object.prototype? +alert(obj.__proto__ == Object.prototype); // true +``` + +## Встроенные "классы" в JavaScript + +Точно такой же подход используется в массивах `Array`, функциях `Function` и других объектах. Встроенные методы для них находятся в `Array.prototype`, `Function.prototype` и т.п. + + + +Как видно из картинки, `Array.prototype` в свою очередь имеет прототипом `Object.prototype`, поэтому если метода нет у массива, то он ищется в объекте. + +Например, при вызове `arr.hasOwnProperty(...)` для массива `arr` метод `hasOwnProperty` берётся из `Object.prototype`. + +Получается иерархия наследования, которая всегда заканчивается на `Object.prototype`. Объект `Object.prototype` -- вершина иерархии, единственный, у которого `__proto__` равно `null`. + +**Поэтому говорят, что "все объекты наследуют от `Object`", а если более точно, то от `Object.prototype`.** + +**"Псевдоклассом" или, более коротко, "классом", называют функцию-конструктор вместе с её `prototype`.** + +[smart header="Переопределение методов в наследниках"] +**При наследовании часть методов переопределяется, например, у массива `Array` есть свой `toString`, который находится в `Array.prototype.toString`:** + +```js +//+ run +var arr = [1, 2, 3] +alert( arr ); // 1,2,3 <-- результат Array.prototype.toString +``` + +Для вывода объекта JavaScript ищет `toString` сначала в самом объекте `arr`, затем в `arr.__proto__`, который равен `Array.prototype`. + +Конечно, если бы его там не было -- поиск пошёл бы выше в `Array.prototype.__proto__`, который по стандарту (см. диаграмму выше) равен `Object.prototype`, и тогда использовался бы стандартный метод для объектов. +[/smart] + +Ранее мы говорили о применении методов массивов к "псевдомассивам", например, можно использовать `[].join` для произвольных объектов, имеющих нумерованные свойства и `length`: + +```js +//+ run +function showList() { +*!* + alert( [].join.call(arguments, " - ") ); +*/!* +} + +showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша +``` + +Так как метод `join` находится в `Array.prototype`, то можно вызвать его оттуда напрямую, вот так: + +```js +//+ run +function showList() { +*!* + alert( Array.prototype.join.call(arguments, " - ") ); +*/!* +} + +showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша +``` + +Это лучше, потому что не создаётся лишний объект массива `[]`, хотя, с другой стороны -- так больше писать. + + +## Примитивы + +Примитивы не являются объектами, но методы берут из соответствующих прототипов: `Number.prototype`, `Boolean.prototype`, `String.prototype`. + +По стандарту, если обратиться к свойству примитива, то будет создан объект соответствующего типа, например `new String`, произведена операция со свойством или вызов метода по обычным правилам, с поиском в прототипе, а затем этот объект будет уничтожен. + +Именно так работает код ниже: + +```js +//+ run +var user = "Вася"; // создали строку (примитив) + +*!* +alert( user.toUpperCase() ); // ВАСЯ +// создан временный объект new String +// вызван метод +// new String уничтожен, результат возвращён +*/!* +``` + +**Принципиальное отличие от объектов -- в примитив нельзя записать свойство.** + +```js +//+ run +// а теперь попытаемся записать свойство в строку: +var user = "Вася"; +user.age = 30; + +*!* +alert(user.age); // undefined +*/!* +``` + +Свойство `age` было записано во временный объект, который был тут же уничтожен. + +[warn header="Конструкторы `String/Number/Boolean` -- только для внутреннего использования"] +Технически, можно создавать объекты для примитивов и вручную, например `new Number`. Но в ряде случаев получится откровенно бредовое поведение. Например: + +```js +//+ run +alert(typeof 1); // "number" + +alert(typeof new Number(1)); // "object" ?!? +``` + +Или, ещё страннее: + +```js +//+ run +var zero = new Number(0); + +if (zero) { // объект - true, так что alert выполнится + alert("число ноль -- true?!?"); +} +``` + +Поэтому в явном виде `new String`, `new Number` и `new Boolean` никогда не вызываются. +[/warn] + +[warn header="Значения `null` и `undefined` не имеют свойств"] +Значения `null` и `undefined` стоят особняком. Вышесказанное к ним не относится. + +Для них нет соответствующих классов, в них нельзя записать свойство (будет ошибка), в общем, на конкурсе "самое примитивное значение" они точно разделили бы первое место. + +[/warn] + + +## Изменение встроенных прототипов [#native-prototype-change] + +Встроенные прототипы можно изменять. В том числе -- добавлять свои методы. + +Мы можем написать метод для многократного повторения строки, и он тут же станет доступным для всех строк: + +```js +//+ run +String.prototype.repeat = function(times) { + return new Array(times+1).join(this); +}; + +alert( "ля".repeat(3) ) // ляляля +``` + +Аналогично мы могли бы создать метод `Object.prototype.each(func)`, который будет применять `func` к каждому свойству: + +```js +//+ run +Object.prototype.each = function(f) { + for (var prop in this) { + var value = this[prop]; + f.call(value, prop, value); // вызовет f(prop, value), this=value + } +} + +// Попробуем! (внимание, пока что это работает неверно!) +var obj = { name: 'Вася', age: 25 }; + +obj.each(function(prop, val) { + alert(prop); // name -> age -> (!) each +}); +``` + +Обратите внимание -- пример выше работает неправильно. Он выводит лишнее свойство `each`, т.к. цикл `for..in` перебирает свойства в прототипе. Встроенные методы при этом пропускаются, а наш метод -- вылез. + +В данном случае это легко поправить добавлением проверки `hasOwnProperty`: + +```js +//+ run +Object.prototype.each = function(f) { + + for (var prop in this) { + +*!* + if (!this.hasOwnProperty(prop)) continue; +*/!* + + var value = this[prop]; + f.call(value, prop, value); + + } + +}; + +// Теперь все будет в порядке +var obj = { name: 'Вася', age: 25 }; + +obj.each(function(prop, val) { + alert(prop); // name -> age +}); +``` + +Здесь это сработало, теперь код работает верно. Но мы же не хотим добавлять `hasOwnProperty` в цикл по любому объекту! Поэтому... + + + +[warn header="Не добавляйте свойства в `Object.prototype`"] + +Свойства, добавленные в `Object.prototype`, появятся во всех `for..in` циклах. Они в них будут лишними. + +[/warn] + + + +[smart header="Современный стандарт и `for..in`"] + +Встроенные свойства и методы не перебираются в `for..in`, так как у них есть специальный внутренний флаг `[[Enumerable]]`, установленный в `false`. + +Современные браузеры (включая IE с версии 9) позволяют устанавливать этот флаг для любых свойств, используя специальные вызовы, описанные в главе [](/descriptors-getters-setters). При таком добавлении предупреждение станет неактуальным, так как они тоже не будут видны в `for..in`. +[/smart] + +**Многие объекты не участвуют в циклах `for..in`, например строки, функции... С ними уж точно нет такой проблемы, и в их прототипы, пожалуй, можно добавлять свои методы.** + +Но здесь есть свои "за" и "против": + +[compare] ++Методы в прототипе автоматически доступны везде, их вызов прост и красив. +-Новые свойства, добавленные в прототип из разных мест, могут конфликтовать между собой. Представьте, что вы подключили две библиотеки, которые добавили одно и то же свойство в прототип, но определили его по-разному. Конфликт неизбежен. +-Изменения встроенных прототипов влияют глобально, на все-все скрипты, делать их не очень хорошо с архитектурной точки зрения. +[/compare] + +С другой стороны, есть одно исключение, когда изменения встроенных прототипов не только разрешены, но и приветствуются. + +**Допустимо изменение прототипа встроенных объектов, которое добавляет поддержку метода из современных стандартов в те браузеры, где её пока нет.** + +Например, добавим `Object.create(proto)` в старые браузеры: + +```js +if (!Object.create) { + + Object.create = function(proto) { + function F() {} + F.prototype = proto; + return new F; + }; + +} +``` + +Именно так работает библиотека [es5-shim](https://github.com/kriskowal/es5-shim), которая предоставляет многие функции современного JavaScript для старых браузеров. Они добавляются во встроенные объекты и их прототипы. + +## Итого + +
        +
      • Методы встроенных объектов хранятся в их прототипах.
      • +
      • Встроенные прототипы можно расширить или поменять.
      • +
      • Добавление методов в `Object.prototype`, если оно не сопровождается `Object.defineProperty` с установкой `enumerable` (IE9+), "сломает" циклы `for..in`, поэтому стараются в этот прототип методы не добавлять. + +Другие прототипы изменять менее опасно, но все же не рекомендуется во избежание конфликтов. + +Отдельно стоит изменение с целью добавления современных методов в старые браузеры, таких как Object.create, Object.keys, Function.prototype.bind и т.п. Это допустимо и как раз делается [es5-shim](https://github.com/kriskowal/es5-shim).
      • +
      + diff --git a/1-js/9-prototypes/4-classes/1-rewrite-by-class/solution.md b/1-js/9-prototypes/4-classes/1-rewrite-by-class/solution.md new file mode 100644 index 00000000..09af813f --- /dev/null +++ b/1-js/9-prototypes/4-classes/1-rewrite-by-class/solution.md @@ -0,0 +1,32 @@ + + +```js +//+ run +function CoffeeMachine(power) { + // свойства конкретной кофеварки + this._power = power; + this._waterAmount = 0; +} + +// свойства и методы для всех объектов класса +CoffeeMachine.prototype.WATER_HEAT_CAPACITY = 4200; + +CoffeeMachine.prototype._getTimeToBoil = function() { + return this._waterAmount * this.WATER_HEAT_CAPACITY * 80 / this._power; +}; + +CoffeeMachine.prototype.run = function() { + setTimeout(function() { + alert('Кофе готов!'); + }, this._getTimeToBoil()); +}; + +CoffeeMachine.prototype.setWaterAmount = function(amount) { + this._waterAmount = amount; +}; + +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.setWaterAmount(50); +coffeeMachine.run(); +``` + diff --git a/1-js/9-prototypes/4-classes/1-rewrite-by-class/task.md b/1-js/9-prototypes/4-classes/1-rewrite-by-class/task.md new file mode 100644 index 00000000..dadde648 --- /dev/null +++ b/1-js/9-prototypes/4-classes/1-rewrite-by-class/task.md @@ -0,0 +1,39 @@ +# Перепишите в виде класса + +[importance 5] + +Есть класс `CoffeeMachine`, заданный в функциональном стиле. + +Задача: переписать `CoffeeMachine` в виде класса с использованием прототипа. + +Исходный код: + +```js +//+ run +function CoffeeMachine(power) { + var waterAmount = 0; + + var WATER_HEAT_CAPACITY = 4200; + + function getTimeToBoil() { + return waterAmount * WATER_HEAT_CAPACITY * 80 / power; + } + + this.run = function() { + setTimeout(function() { + alert('Кофе готов!'); + }, getTimeToBoil()); + }; + + this.setWaterAmount = function(amount) { + waterAmount = amount; + }; + +} + +var coffeeMachine = new CoffeeMachine(10000); +coffeeMachine.setWaterAmount(50); +coffeeMachine.run(); +``` + +P.S. При описании через прототипы локальные переменные недоступны методам, поэтому нужно будет переделать их в защищённые свойства. diff --git a/1-js/9-prototypes/4-classes/2-hamsters-with-proto/solution.md b/1-js/9-prototypes/4-classes/2-hamsters-with-proto/solution.md new file mode 100644 index 00000000..70cc2fc5 --- /dev/null +++ b/1-js/9-prototypes/4-classes/2-hamsters-with-proto/solution.md @@ -0,0 +1,52 @@ +# Почему возникает проблема + +Давайте подробнее разберем происходящее при вызове `speedy.found("яблоко")`: +
        +
      1. Интерпретатор ищет свойство `found` в `speedy`. Но `speedy` -- пустой объект, т.к. `new Hamster` ничего не делает с `this`.
      2. +
      3. Интерпретатор идёт по ссылке `speedy.__proto__ (==Hamster.prototype)` и находят там метод `found`, запускает его.
      4. +
      5. Значение `this` устанавливается в объект перед точкой, т.е. в `speedy`.
      6. +
      7. Для выполнения `this.food.push()` нужно найти свойство `this.food`. Оно отсутствует в `speedy`, но есть в `speedy.__proto__`.
      8. +
      9. Значение `"яблоко"` добавляется в `speedy.__proto__.food`.
      10. +
      + +**У всех хомяков общий живот!** Или, в терминах JavaScript, свойство `food` изменяется в прототипе, который является общим для всех объектов-хомяков. + +Заметим, что этой проблемы не было бы при простом присваивании: + +```js +this.food = something; +``` + +В этом случае значение записалось бы в сам объект, без поиска `found` в прототипе. + +**Проблема возникает только со свойствами-объектами в прототипе.** + +Исправьте её? + +# Исправление + +Для исправления проблемы нужно дать каждому хомяку свой живот. Это можно сделать, присвоив его в конструкторе. + +```js +//+ run +function Hamster() { +*!* + this.food = []; +*/!* +} + +Hamster.prototype.found = function(something) { + this.food.push(something); +}; + +speedy = new Hamster(); +lazy = new Hamster(); + +speedy.found("яблоко"); +speedy.found("орех"); + +alert(speedy.food.length) // 2 +alert(lazy.food.length) // 0(!) +``` + +Теперь всё в порядке. У каждого хомяка -- свой живот. \ No newline at end of file diff --git a/1-js/9-prototypes/4-classes/2-hamsters-with-proto/task.md b/1-js/9-prototypes/4-classes/2-hamsters-with-proto/task.md new file mode 100644 index 00000000..452c3d46 --- /dev/null +++ b/1-js/9-prototypes/4-classes/2-hamsters-with-proto/task.md @@ -0,0 +1,33 @@ +# Хомяки с __proto__ + +[importance 5] + +Вы -- руководитель команды, которая разрабатывает игру, хомяковую ферму. Один из программистов получил задание создать класс "хомяк" (англ - `"Hamster"`). + +Объекты-хомяки должны иметь массив `food` для хранения еды и метод `found`, который добавляет к нему. + +Ниже -- его решение. При создании двух хомяков, если поел один -- почему-то сытым становится и второй тоже. + +В чём дело? Как поправить? + +```js +//+ run +function Hamster() { } + +Hamster.prototype.food = [ ]; // пустой "живот" + +Hamster.prototype.found = function(something) { + this.food.push(something); +}; + +// Создаём двух хомяков и кормим первого +speedy = new Hamster(); +lazy = new Hamster(); + +speedy.found("яблоко"); +speedy.found("орех"); + +alert(speedy.food.length); // 2 +alert(lazy.food.length); // 2 (!??) +``` + diff --git a/1-js/9-prototypes/4-classes/7.png b/1-js/9-prototypes/4-classes/7.png new file mode 100755 index 00000000..9a43f55e Binary files /dev/null and b/1-js/9-prototypes/4-classes/7.png differ diff --git a/1-js/9-prototypes/4-classes/7@2x.png b/1-js/9-prototypes/4-classes/7@2x.png new file mode 100755 index 00000000..add9a340 Binary files /dev/null and b/1-js/9-prototypes/4-classes/7@2x.png differ diff --git a/1-js/9-prototypes/4-classes/article.md b/1-js/9-prototypes/4-classes/article.md new file mode 100644 index 00000000..cc6da884 --- /dev/null +++ b/1-js/9-prototypes/4-classes/article.md @@ -0,0 +1,138 @@ +# Свои классы на прототипах + +Используем ту же структуру, что JavaScript использует внутри себя, для объявления своих классов. + +[cut] +## Обычный конструктор + +Вспомним, как мы объявляли конструкторы ранее. + +Например, этот код задаёт объект `Animal` без всяких прототипов: + +```js +//+ run +function Animal(name) { + this.speed = 0; + this.name = name; + + this.run = function(speed) { + this.speed += speed; + alert(this.name + ' бежит, скорость ' + this.speed); + }; + + this.stop = function() { + this.speed = 0; + alert(this.name + ' стоит'); + }; +}; + +var animal = new Animal('Зверь'); + +alert(animal.speed); // 0, начальная скорость +animal.run(3); // Зверь бежит, скорость 3 +animal.run(10); // Зверь бежит, скорость 13 +animal.stop(); // Зверь стоит +``` + +## Класс через прототип + +А теперь создадим аналогичный класс, используя прототипы, наподобие того, как сделаны классы `Object`, `Date` и остальные. + +**Чтобы объявить свой класс, нужно:** +
        +
      1. Объявить функцию-конструктор.
      2. +
      3. Записать методы и свойства, нужные всем объектам класса, в `prototype`.
      4. +
      + +Опишем класс `Animal`: + +```js +//+ run +// конструктор +function Animal(name) { + this.name = name; +} + +// методы в прототипе +Animal.prototype.run = function(speed) { + this.speed += speed; + alert(this.name + ' бежит, скорость ' + this.speed); +}; + +Animal.prototype.stop = function() { + this.speed = 0; + alert(this.name + ' стоит'); +}; + +// свойство speed со значением "по умолчанию" +Animal.prototype.speed = 0; + +var animal = new Animal('Зверь'); + +alert(animal.speed); // 0, свойство взято из прототипа +animal.run(5); // Зверь бежит, скорость 5 +animal.run(5); // Зверь бежит, скорость 10 +animal.stop(); // Зверь стоит +``` + +Здесь объекту `animal` принадлежит лишь свойство `name`, а остальное находится в прототипе. + +Обратим внимание, значение `speed` по умолчанию тоже перенесено в прототип, ведь оно во всех объектах (в начале) одинаково, но вызовы `animal.run()`, `animal.stop()` в примере используют вызов `this.speed += speed`, а любая запись в `this.свойство` работает с самим объектом. + +То есть, начальное значение `speed` берётся из прототипа, а новое -- пишется уже в сам объект. И в дальнейшем используется. + + + +## Сравнение + +Чем такое задание класса лучше и хуже предыдущего? + +[compare] ++Прототип позволяет хранить общие свойства и методы в единственном экземпляре, таким образом экономится память и создание объекта происходит быстрее, чем если записывать их в каждый `this`. +-При объявлении класса через прототип, мы теряем возможность использовать локальные переменные и функции. +[/compare] + +К примеру, есть у нас приватное свойство `name` и метод `sayHi` в функциональном стиле ООП: + +```js +//+ run +function Animal(name) { + this.sayHi = function() { +*!* + alert(name); +*/!* + }; +} + +var animal = new Animal("Зверь"); +animal.sayHi(); // Зверь +``` + +При задании методов в прототипе мы не сможем её так оставить, ведь методы находятся *вне* конструктора, у них нет общей области видимости, поэтому приходится записывать `name` в сам объект, обозначив его как защищённое: + +```js +//+ run +function Animal(name) { +*!* + this._name = name; +*/!* +} + +Animal.prototype.sayHi = function() { +*!* + alert(this._name); +*/!* +} + +var animal = new Animal("Зверь"); +animal.sayHi(); // Зверь +``` + +Ранее в каждый объект `Animal` записывалась своя функция `this.sayHi`, а теперь есть одна функция такого рода в прототипе. В сам объект мы пишем только то, что свойственно именно этому объекту. + +## Задачи + +Обычно свойства по умолчанию хранятся в прототипе. Но если свойство по умолчанию -- объект, то его в прототипе хранить нельзя. + +Почему? Смотрите задачу ниже на эту тему. + diff --git a/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/solution.md b/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/solution.md new file mode 100644 index 00000000..f0b99ed5 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/solution.md @@ -0,0 +1,44 @@ +Ошибка в строке: + +```js +Rabbit.prototype = Animal.prototype; +``` + +Эта ошибка приведёт к тому, что `Rabbit.prototype` и `Animal.prototype` -- один и тот же объект. В результате методы `Rabbit` будут помещены в него и, при совпадении, перезапишут методы `Animal`. + +Получится, что все животные прыгают, вот пример: + +```js +//+ run +function Animal(name) { + this.name = name; +} + +Animal.prototype.walk = function() { + alert("ходит " + this.name); +}; + +function Rabbit(name) { + this.name = name; +} +*!* +Rabbit.prototype = Animal.prototype; +*/!* + +Rabbit.prototype.walk = function() { + alert("прыгает! и ходит: " + this.name); +}; + +*!* +var animal = new Animal("Хрюшка"); +animal.walk(); // прыгает! и ходит Хрюшка +*/!* +``` + +Правильный вариант этой строки: + +```js +Rabbit.prototype = Object.create(Animal.prototype); +``` + +Если так написать, то в `Rabbit.prototype` будет отдельный объект, который прототипно наследует от `Animal.prototype`, но может содержать и свои свойства, специфичные для кроликов. diff --git a/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/task.md b/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/task.md new file mode 100644 index 00000000..5b176d3d --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/1-inheritance-error-assign/task.md @@ -0,0 +1,25 @@ +# Найдите ошибку в наследовании + +[importance 5] + +Найдите ошибку в пототипном наследовании. К чему она приведёт? + +```js +function Animal(name) { + this.name = name; +} + +Animal.prototype.walk = function() { + alert("ходит " + this.name); +}; + +function Rabbit(name) { + this.name = name; +} +Rabbit.prototype = Animal.prototype; + +Rabbit.prototype.walk = function() { + alert("прыгает! и ходит: " + this.name); +}; +``` + diff --git a/1-js/9-prototypes/5-class-inheritance/10.png b/1-js/9-prototypes/5-class-inheritance/10.png new file mode 100755 index 00000000..f5fa8c74 Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/10.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/10@2x.png b/1-js/9-prototypes/5-class-inheritance/10@2x.png new file mode 100755 index 00000000..53c247af Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/10@2x.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/11.png b/1-js/9-prototypes/5-class-inheritance/11.png new file mode 100755 index 00000000..826d0239 Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/11.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/11@2x.png b/1-js/9-prototypes/5-class-inheritance/11@2x.png new file mode 100755 index 00000000..995a70eb Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/11@2x.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/solution.md b/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/solution.md new file mode 100644 index 00000000..8d0ae759 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/solution.md @@ -0,0 +1,40 @@ +Ошибка -- в том, что метод `walk` присваивается в конструкторе `Animal` самому объекту вместо прототипа. + +Казалось бы -- ничего страшного, ведь `Animal.apply(this, arguments)` создаст его. Код работает. + +Однако, если мы решим перезаписать этот метод своим, специфичным для кролика, то будет сюрприз: + +```js +//+ run +function Animal(name) { + this.name = name; + + this.walk = function() { + alert("ходит " + this.name); + }; +} + +function Rabbit(name) { + Animal.apply(this, arguments); +} +Rabbit.prototype = Object.create(Animal.prototype); + +*!* +Rabbit.prototype.walk = function() { + alert("прыгает! и ходит: " + this.name); +}; +*/!* + +var rabbit = new Rabbit("Кроль"); +*!* +rabbit.walk(); // ходит Кроль +*/!* +``` + +Переопределение не сработало! + +Причина -- метод `walk` создаётся в конструкторе, записывается в сам объект и тем самым игнорирует цепочку прототипов и говорит "гуд-бай" всей иерархии наследования. + +Правильно было бы определять `walk` как `Animal.prototype.walk`. + +Тем более, что этот метод является общим для всех объектов, тратить память и время на запись его в каждый конструктор определённо ни к чему. \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/task.md b/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/task.md new file mode 100644 index 00000000..b3efa118 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/2-inheritance-error-constructor/task.md @@ -0,0 +1,26 @@ +# В чём ошибка в наследовании + +[importance 5] + +Найдите ошибку в пототипном наследовании. К чему она приведёт? + +```js +//+ run +function Animal(name) { + this.name = name; + + this.walk = function() { + alert("ходит " + this.name); + }; + +} + +function Rabbit(name) { + Animal.apply(this, arguments); +} +Rabbit.prototype = Object.create(Animal.prototype); + +var rabbit = new Rabbit("Кроль"); +rabbit.walk(); +``` + diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/clock.js b/1-js/9-prototypes/5-class-inheritance/3-clock-class/clock.js new file mode 100755 index 00000000..a3b119c8 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/clock.js @@ -0,0 +1,33 @@ + +function Clock(options) { + this._template = options.template; +} + +Clock.prototype._render = function render() { + var date = new Date(); + + var hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + var min = date.getMinutes(); + if (min < 10) min = '0' + min; + + var sec = date.getSeconds(); + if (sec < 10) sec = '0' + sec; + + var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); + + console.log(output); +}; + +Clock.prototype.stop = function() { + clearInterval(this._timer); +}; + +Clock.prototype.start = function() { + this._render(); + var self = this; + this._timer = setInterval(function() { + self._render(); + }, 1000); +}; diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.md b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.md new file mode 100644 index 00000000..d1c08b87 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.md @@ -0,0 +1,7 @@ + + +```js +//+ src="clock.js" +``` + +[edit src="solution"]Открыть полное решение[/edit] \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/clock.js b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/clock.js new file mode 100755 index 00000000..a3b119c8 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/clock.js @@ -0,0 +1,33 @@ + +function Clock(options) { + this._template = options.template; +} + +Clock.prototype._render = function render() { + var date = new Date(); + + var hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + var min = date.getMinutes(); + if (min < 10) min = '0' + min; + + var sec = date.getSeconds(); + if (sec < 10) sec = '0' + sec; + + var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); + + console.log(output); +}; + +Clock.prototype.stop = function() { + clearInterval(this._timer); +}; + +Clock.prototype.start = function() { + this._render(); + var self = this; + this._timer = setInterval(function() { + self._render(); + }, 1000); +}; diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/index.html b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/index.html new file mode 100755 index 00000000..5b807b0c --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/solution.view/index.html @@ -0,0 +1,18 @@ + + + + Часики в консоли + + + + + + + + + diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/clock.js b/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/clock.js new file mode 100755 index 00000000..bb9662fc --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/clock.js @@ -0,0 +1,32 @@ +function Clock(options) { + + var template = options.template; + var timer; + + function render() { + var date = new Date(); + + var hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + var min = date.getMinutes(); + if (min < 10) min = '0' + min; + + var sec = date.getSeconds(); + if (sec < 10) sec = '0' + sec; + + var output = template.replace('h', hours).replace('m', min).replace('s', sec); + + console.log(output); + } + + this.stop = function() { + clearInterval(timer); + }; + + this.start = function() { + render(); + timer = setInterval(render, 1000); + } + +} diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/index.html b/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/index.html new file mode 100755 index 00000000..5b807b0c --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/source.view/index.html @@ -0,0 +1,18 @@ + + + + Часики в консоли + + + + + + + + + diff --git a/1-js/9-prototypes/5-class-inheritance/3-clock-class/task.md b/1-js/9-prototypes/5-class-inheritance/3-clock-class/task.md new file mode 100644 index 00000000..51dc1bd9 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/3-clock-class/task.md @@ -0,0 +1,11 @@ +# Класс "часы" + +[importance 5] + +Есть реализация часиков, оформленная в виде одной функции-конструктора. У неё есть приватные свойства `timer`, `template` и метод `render`. + +Задача: переписать часы на прототипах. Приватные свойства и методы сделать защищёнными. + +[edit src="source" task/] + +P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть). \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/extended-clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/extended-clock.js new file mode 100755 index 00000000..75a8478d --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/extended-clock.js @@ -0,0 +1,16 @@ + + +function ExtendedClock(options) { + Clock.apply(this, arguments); + this._precision = +options.precision || 1000; +} + +ExtendedClock.prototype = Object.create(Clock.prototype); + +ExtendedClock.prototype.start = function() { + this._render(); + var self = this; + this._timer = setInterval(function() { + self._render(); + }, this._precision); +}; diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.md b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.md new file mode 100644 index 00000000..f58d46d8 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.md @@ -0,0 +1,7 @@ +Наследник: + +```js +//+ src="extended-clock.js" +``` + +[edit src="solution"]Открыть полное решение в редакторе[/edit] \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/clock.js new file mode 100755 index 00000000..a3b119c8 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/clock.js @@ -0,0 +1,33 @@ + +function Clock(options) { + this._template = options.template; +} + +Clock.prototype._render = function render() { + var date = new Date(); + + var hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + var min = date.getMinutes(); + if (min < 10) min = '0' + min; + + var sec = date.getSeconds(); + if (sec < 10) sec = '0' + sec; + + var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); + + console.log(output); +}; + +Clock.prototype.stop = function() { + clearInterval(this._timer); +}; + +Clock.prototype.start = function() { + this._render(); + var self = this; + this._timer = setInterval(function() { + self._render(); + }, 1000); +}; diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/extended-clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/extended-clock.js new file mode 100755 index 00000000..75a8478d --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/extended-clock.js @@ -0,0 +1,16 @@ + + +function ExtendedClock(options) { + Clock.apply(this, arguments); + this._precision = +options.precision || 1000; +} + +ExtendedClock.prototype = Object.create(Clock.prototype); + +ExtendedClock.prototype.start = function() { + this._render(); + var self = this; + this._timer = setInterval(function() { + self._render(); + }, this._precision); +}; diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/index.html b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/index.html new file mode 100755 index 00000000..e7b9c984 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/solution.view/index.html @@ -0,0 +1,23 @@ + + + + Часики в консоли + + + + + + + + + + + + diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/clock.js new file mode 100755 index 00000000..a3b119c8 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/clock.js @@ -0,0 +1,33 @@ + +function Clock(options) { + this._template = options.template; +} + +Clock.prototype._render = function render() { + var date = new Date(); + + var hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + var min = date.getMinutes(); + if (min < 10) min = '0' + min; + + var sec = date.getSeconds(); + if (sec < 10) sec = '0' + sec; + + var output = this._template.replace('h', hours).replace('m', min).replace('s', sec); + + console.log(output); +}; + +Clock.prototype.stop = function() { + clearInterval(this._timer); +}; + +Clock.prototype.start = function() { + this._render(); + var self = this; + this._timer = setInterval(function() { + self._render(); + }, 1000); +}; diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/extended-clock.js b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/extended-clock.js new file mode 100755 index 00000000..05d93d88 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/extended-clock.js @@ -0,0 +1,12 @@ +function extend(Child, Parent) { + Child.prototype = inherit(Parent.prototype); + Child.prototype.constructor = Child; + Child.parent = Parent.prototype; +} +function inherit(proto) { + function F() {} + F.prototype = proto; + return new F; +} + +// ваш код \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/index.html b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/index.html new file mode 100755 index 00000000..9611fa12 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/source.view/index.html @@ -0,0 +1,33 @@ + + + + Часики в консоли + + + + + + + + + + diff --git a/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/task.md b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/task.md new file mode 100644 index 00000000..ad2d973a --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/4-clock-class-extended/task.md @@ -0,0 +1,15 @@ +# Класс "расширенные часы" + +[importance 5] + +Есть реализация часиков на прототипах. Создайте класс, расширяющий её, добавляющий поддержку параметра `precision`, который будет задавать частоту тика в `setInterval`. Значение по умолчанию: `1000`. + +[edit src="source" task/] + +
        +
      • Для этого класс `Clock` надо унаследовать. Пишите ваш новый код в файле `extended-clock.js`.
      • +
      • Исходный класс `Clock` менять нельзя.
      • +
      • Пусть конструктор потомка вызывает конструктор родителя. Это позволит избежать проблем при расширении `Clock` новыми опциями.
      • +
      + +P.S. Часики тикают в браузерной консоли (надо открыть её, чтобы увидеть). \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.md b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.md new file mode 100644 index 00000000..9cb942f7 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.md @@ -0,0 +1,3 @@ +[edit src="solution"]Открыть решение в редакторе[/edit] + +Обратите внимание: константы состояний перенесены в прототип, чтобы `AnimatingMenu` их тоже унаследовал. diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/index.html b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/index.html new file mode 100755 index 00000000..a3dec85f --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/index.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/menu.js b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/menu.js new file mode 100755 index 00000000..9180dec9 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/solution.view/menu.js @@ -0,0 +1,28 @@ +function Menu(state) { + this._state = state || this.STATE_CLOSED; +}; + +Menu.prototype.STATE_OPEN = 1; +Menu.prototype.STATE_CLOSED = 0; + +Menu.prototype.open = function() { + this._state = this.STATE_OPEN; +}; + +Menu.prototype.close = function() { + this._state = this.STATE_CLOSED; +}; + +Menu.prototype._stateAsString = function() { + switch(this._state) { + case this.STATE_OPEN: + return 'открыто'; + + case this.STATE_CLOSED: + return 'закрыто'; + } +}; + +Menu.prototype.showState = function() { + alert( this._stateAsString() ); +} \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/index.html b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/index.html new file mode 100755 index 00000000..bc17df1f --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/menu.js b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/menu.js new file mode 100755 index 00000000..9de39aa5 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/source.view/menu.js @@ -0,0 +1,28 @@ +function Menu(state) { + this._state = state || Menu.STATE_CLOSED; +}; + +Menu.STATE_OPEN = 1; +Menu.STATE_CLOSED = 0; + +Menu.prototype.open = function() { + this._state = Menu.STATE_OPEN; +}; + +Menu.prototype.close = function() { + this._state = Menu.STATE_CLOSED; +}; + +Menu.prototype._stateAsString = function() { + switch(this._state) { + case Menu.STATE_OPEN: + return 'открыто'; + + case Menu.STATE_CLOSED: + return 'закрыто'; + } +}; + +Menu.prototype.showState = function() { + alert( this._stateAsString() ); +}; \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/task.md b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/task.md new file mode 100644 index 00000000..dd080260 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/5-menu-timer-animated/task.md @@ -0,0 +1,14 @@ +# Меню с таймером для анимации + +[importance 5] + +Есть класс `Menu`. У него может быть два состояния: открыто `STATE_OPEN` и закрыто `STATE_CLOSED`. + +Создайте наследника `AnimatingMenu`, который добавляет третье состояние `STATE_ANIMATING`. +
        +
      • При вызове `open()` состояние меняется на `STATE_ANIMATING`, а через 1 секунду, по таймеру, открытие завершается вызовом `open()` родителя.
      • +
      • Вызов `close()` при необходимости отменяет таймер анимации (назначаемый в `open`) и передаёт вызов родительскому `close`.
      • +
      • Метод `showState` для нового состояния выводит `"анимация"`, для остальных -- полагается на родителя.
      • +
      + +[edit src="source"]Исходный документ, вместе с тестом[/edit] \ No newline at end of file diff --git a/1-js/9-prototypes/5-class-inheritance/array-object-prototype.png b/1-js/9-prototypes/5-class-inheritance/array-object-prototype.png new file mode 100755 index 00000000..1c732c4b Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/array-object-prototype.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/array-object-prototype@2x.png b/1-js/9-prototypes/5-class-inheritance/array-object-prototype@2x.png new file mode 100755 index 00000000..4905d1e2 Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/array-object-prototype@2x.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/article.md b/1-js/9-prototypes/5-class-inheritance/article.md new file mode 100644 index 00000000..32995e28 --- /dev/null +++ b/1-js/9-prototypes/5-class-inheritance/article.md @@ -0,0 +1,320 @@ +# Наследование классов в JavaScript + +*Наследование* -- это когда мы на основе одного объекта создаём другой, который его расширяет: добавляет свои свойства, методы и так далее. + +Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку `__proto__`. + +Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через `new Admin`, должны иметь все методы, которые есть у объектов, создаваемых через `new User`, и ещё какие-то свои. +[cut] + +## Наследование Array от Object + +Примеры организации наследования мы встречаем среди встроенных типов JavaScript. Например, массивы, которые создаются при помощи `new Array` (или квадратных скобок `[...]`), используют методы из своего прототипа `Array.prototype`, а если их там нет -- то методы из родителя `Object.prototype`. + +Как JavaScript это организует? + +Очень просто: встроенный `Array.prototype` имеет ссылку `__proto__` на `Object.prototype`: + + + +То есть, как и раньше, методы хранятся в прототипах, а для наследования прототипы организованы в `__proto__`-цепочку. + +Самый наглядный способ это увидеть -- запустить в консоли команду `console.dir([1,2,3])`. + +Вывод в Chrome будет примерно таким: + + + +Здесь отчётливо видно, что сами данные и `length` находятся в массиве, дальше в `__proto__` идут методы для массивов `concat`, то есть `Array.prototype`, а далее -- `Object.prototype`. + +[smart header="`console.dir` для доступа к свойствам"] +Обратите внимание, я использовал именно `console.dir`, а не `console.log`, поскольку `log` зачастую выводит объект в виде строки, без доступа к свойствам. +[/smart] + +## Наследование в наших классах + +Применим тот же подход для наших классах. Объявим класс `Rabbit`, который будет наследовать от `Animal`. + +Вначале создадим два этих класса по отдельности, они пока что будут совершенно независимы. + +`Animal`: + +```js +function Animal(name) { + this.name = name; +} + +Animal.prototype.speed = 0; + +Animal.prototype.run = function(speed) { + this.speed += speed; + alert(this.name + ' бежит, скорость ' + this.speed); +}; + +Animal.prototype.stop = function() { + this.speed = 0; + alert(this.name + ' стоит'); +}; +``` + +`Rabbit`: + +```js +function Rabbit(name) { + this.name = name; +} + +Rabbit.prototype.jump = function() { + this.speed++; + alert(this.name + ' прыгает'); +}; + +var rabbit = new Rabbit('Кроль'); +``` + +Для того, чтобы наследование работало, объект `rabbit = new Rabbit` должен использовать свойства и методы из своего прототипа `Rabbit.prototype`, а если их там нет, то -- свойства и метода родителя, которые хранятся в `Animal.prototype`. + +Если ещё короче -- порядок поиска свойств и методов должен быть таким: `rabbit -> Rabbit.prototype -> Animal.prototype`, по аналогии с тем, как это сделано для объектов и массивов. + +Для этого можно поставить ссылку `__proto__` явно: `Rabbit.prototype.__proto__ = Animal.prototype`. Но это не будет работать в IE10-. + +Поэтому лучше используем функцию `Object.create`, благо она либо встроена либо легко эмулируется во всех браузерах. + +Класс `Animal` остаётся без изменений, а для `Rabbit` добавим установку `prototype`: + +```js +function Rabbit(name) { + this.name = name; +} + +*!* +// задаём наследование +Rabbit.prototype = Object.create(Animal.prototype); +*/!* + +// и добавим свой метод (или методы...) +Rabbit.prototype.jump = function() { ... }; +``` + +Теперь выглядеть иерархия будет так: + + + +Для наглядности -- вот итоговый код с двумя классами `Animal` и `Rabbit`: + +```js +// 1. Конструктор Animal +function Animal(name) { + this.name = name; +} + +// 1.1. Методы и свойства по умолчанию -- в прототип +Animal.prototype.speed = 0; + +Animal.prototype.stop = function() { + this.speed = 0; + alert(this.name + ' стоит'); +} + +Animal.prototype.run = function(speed) { + this.speed += speed; + alert(this.name + ' бежит, скорость ' + this.speed); +}; + + +// 2. Конструктор Rabbit +function Rabbit(name) { + this.name = name; +} + +// 2.1. Наследование +Rabbit.prototype = Object.create(Animal.prototype); + +// 2.2. Методы Rabbit +Rabbit.prototype.jump = function() { + this.speed++; + alert(this.name + ' прыгает, скорость ' + this.speed); +} +``` + +Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте. + +Обратим внимание: `Rabbit.prototype = Object.create(proto)` присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы. + +[warn header="Альтернативный вариант: `Rabbit.prototype = new Animal`"] + +Можно унаследовать от `Animal` и по-другому: + +```js +// вместо Rabbit.prototype = Object.create(Animal.prototype) +Rabbit.prototype = new Animal(); +``` + +В этом случае мы получаем в прототипе не пустой объект с прототипом `Animal.prototype`, а полноценный `Animal`. + +Можно даже сконфигурировать его: + +```js +Rabbit.prototype = new Animal("Зверь номер два"); +``` + +Теперь новые `Rabbit` будут создаваться на основе конкретного экземпляра `Animal`. Это интересная возможность, но как правило мы не хотим создавать `Animal`, а хотим только унаследовать его методы! + +Более того, на практике создание объекта, скажем, меню `new Menu`, может иметь побочные эффекты, показывать что-то посетителю, и так далее, и этого мы хотели бы избежать. Поэтому рекомендуется использовать вариант с `Object.create`. +[/warn] + +## Вызов конструктора родителя + +Посмотрим внимательно на конструкторы `Animal` и `Rabbit` из примеров выше: + +```js +function Animal(name) { + this.name = name; +} + +function Rabbit(name) { + this.name = name; +} +``` + +Как видно, объект `Rabbit` не добавляет никакой особенной логики при создании, которой не было в `Animal`. + +Чтобы упростить поддержку кода, имеет смысл не дублировать код конструктора `Animal`, а напрямую вызвать его: + +```js +function Rabbit(name) { + Animal.apply(this, arguments); +} +``` + +Такой вызов запустит функцию `Animal` в контексте текущего объекта, со всеми аргументами, она выполнится и запишет в `this` всё, что нужно. + +Здесь можно было бы использовать и `Animal.call(this, name)`, но `apply` надёжнее, так как работает с любым количеством аргументов. + +## Переопределение метода + +Итак, `Rabbit` наследует `Animal`. Теперь если какого-то метода нет в `Rabbit.prototype` -- он будет взят из `Animal.prototype`. + +В `Rabbit` может понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод `run()`: + +```js +Rabbit.prototype.run = function(speed) { + this.speed++; + this.jump(); +}; +``` + +Вызов `rabbit.run()` теперь будет брать `run` из своего прототипа: + + + +[smart] +Кстати, можно назначить метод и на уровне конкретного объекта: + +```js +rabbit.run = function() { + alert('Особый метод подпрыгивания для этого кролика'); +}; +``` + +[/smart] + +### Вызов метода родителя после переопределения + +Более частая ситуация -- когда мы хотим не просто заменить метод на свой, а взять метод родителя и расширить его. Скажем, кролик бежит так же, как и другие звери, но время от времени подпрыгивает. + +Для вызова метода родителя можно обратиться к нему напрямую, взяв из прототипа: + +```js +//+ run + +Rabbit.prototype.run = function() { +*!* + Animal.prototype.run.apply(this, arguments); + this.jump(); +*/!* +} +``` + +Обратите внимание на `apply` и явное указание контекста. Если вызвать просто `Animal.prototype.run()`, то в качестве `this` функция `run` получит `Animal.prototype`, а это неверно, нужен текущий объект. + +## Итого + +
        +
      • Для наследования нужно, чтобы "склад методов потомка" (`Child.prototype`) наследовал от "склада метода родителей" (`Parent.prototype`). + +Это можно сделать при помощи `Object.create`: + +Код: + +```js +Rabbit.prototype = Object.create(Animal.prototype); +``` + +
      • +
      • Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя `apply(this, arguments)`, вот так: + +```js +function Rabbit(...) { + Animal.apply(this, arguments); +} +``` + +
      • +
      • При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа: + +```js +Rabbit.prototype.run = function() { + var result = Animal.prototype.run.apply(this, ...); + // result -- результат вызова метода родителя +} +``` + +
      • +
      + +Структура наследования полностью: + +```js +//+ run +// --------- *!*Класс-Родитель*/!* ------------ +// Конструктор родителя +function Animal(name) { + this.name = name; +} + +// Методы родителя +Animal.prototype.run= function() { + alert(this + " бежит!") +} + +Animal.prototype.toString = function() { + return this.name; +} + +// --------- *!*Класс-потомок*/!* ----------- +// Конструктор потомка +function Rabbit(name) { + Animal.apply(this, arguments); +} + +// *!*Унаследовать*/!* +*!* +Rabbit.prototype = Object.create(Animal.prototype); +*/!* + +// Методы потомка +Rabbit.prototype.run = function() { + Animal.prototype.run.apply(this); // метод родителя вызвать + alert(this + " подпрыгивает!"); +}; + +var rabbit = new Rabbit('Кроль'); +rabbit.run(); +``` + +
        +
      • Если метод или свойство начинается с `_`, то оно существует исключительно для внутренних нужд объекта, не нужно обращаться к нему из внешнего кода, но можно -- из методов объекта и наследников.
      • +
      • Остальные свойства и методы -- публичные.
      • +
      + diff --git a/1-js/9-prototypes/5-class-inheritance/console_dir_array.png b/1-js/9-prototypes/5-class-inheritance/console_dir_array.png new file mode 100755 index 00000000..1ae78dd6 Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/console_dir_array.png differ diff --git a/1-js/9-prototypes/5-class-inheritance/console_dir_array@2x.png b/1-js/9-prototypes/5-class-inheritance/console_dir_array@2x.png new file mode 100755 index 00000000..a55d2c89 Binary files /dev/null and b/1-js/9-prototypes/5-class-inheritance/console_dir_array@2x.png differ diff --git a/1-js/9-prototypes/6-constructor/1-new-object-same-constructor/solution.md b/1-js/9-prototypes/6-constructor/1-new-object-same-constructor/solution.md new file mode 100644 index 00000000..9f345069 --- /dev/null +++ b/1-js/9-prototypes/6-constructor/1-new-object-same-constructor/solution.md @@ -0,0 +1,19 @@ +Да, можем, но только если уверены, что кто-то позаботился о том, чтобы значение `constructor` было верным. + +В частности, без вмешательства в прототип код точно работает, например: + +```js +//+ run +function User(name) { + this.name = name; +} + +var obj = new User('Вася'); +var obj2 = new obj.constructor('Петя'); + +alert(obj2.name); // Петя (сработало) +``` + +Сработало, так как `User.prototype.constructor == User`. + +Но если кто-то, к примеру, перезапишет `User.prototype` и забудет указать `constructor`, то такой фокус не пройдёт. \ No newline at end of file diff --git a/1-js/9-prototypes/6-constructor/1-new-object-same-constructor/task.md b/1-js/9-prototypes/6-constructor/1-new-object-same-constructor/task.md new file mode 100644 index 00000000..12e9beb0 --- /dev/null +++ b/1-js/9-prototypes/6-constructor/1-new-object-same-constructor/task.md @@ -0,0 +1,13 @@ +# Создать объект тем же конструктором + +[importance 5] + +Пусть у нас есть произвольный объект `obj`, созданный каким-то конструктором, каким -- мы не знаем, но хотели бы создать новый объект с его помощью. + +Сможем ли мы сделать так? + +```js +var obj2 = new obj.constructor(); +``` + +В каком случае такой код будет работать, а в каком -- нет? diff --git a/1-js/9-prototypes/6-constructor/2-constructor-inherited/solution.md b/1-js/9-prototypes/6-constructor/2-constructor-inherited/solution.md new file mode 100644 index 00000000..34867c20 --- /dev/null +++ b/1-js/9-prototypes/6-constructor/2-constructor-inherited/solution.md @@ -0,0 +1,26 @@ +**Нет, не распознает, выведет `false`.** + +Свойство `constructor` содержится в `prototype` функции по умолчанию, интерпретатор не поддерживает его корректность. Посмотрим, чему оно равно и откуда оно будет взято в данном случае. + +Порядок поиска свойства `rabbit.constructor`, по цепочке прототипов: +
        +
      1. `rabbit` -- это пустой объект, в нём нет.
      2. +
      3. `Rabbit.prototype` -- в него при помощи `Object.create` записан пустой объект, наследующий от `Animal.prototype`. Поэтому `constructor'а` в нём также нет.
      4. +
      5. `Animal.prototype` -- у функции `Animal` свойство `prototype` никто не менял. Поэтому оно содержит `Animal.prototype.constructor == Animal`.
      6. +
      + +```js +//+ run +function Animal() { } + +function Rabbit() { } +Rabbit.prototype = Object.create(Animal.prototype); + +var rabbit = new Rabbit(); + +*!* +alert( rabbit.constructor == Rabbit ); // false +alert( rabbit.constructor == Animal ); // true +*/!* +``` + diff --git a/1-js/9-prototypes/6-constructor/2-constructor-inherited/task.md b/1-js/9-prototypes/6-constructor/2-constructor-inherited/task.md new file mode 100644 index 00000000..da6986d3 --- /dev/null +++ b/1-js/9-prototypes/6-constructor/2-constructor-inherited/task.md @@ -0,0 +1,19 @@ +# Что содержит constructor? + +[importance 5] + +В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`. + +Что содержит свойство `rabbit.constructor`? Распознает ли проверка в `alert` объект как `Rabbit`? + +```js +function Animal() { } + +function Rabbit() { } +Rabbit.prototype = Object.create(Animal.prototype); + +var rabbit = new Rabbit(); + +alert( rabbit.constructor == Rabbit ); // что выведет? +``` + diff --git a/1-js/9-prototypes/6-constructor/8.png b/1-js/9-prototypes/6-constructor/8.png new file mode 100755 index 00000000..b875db60 Binary files /dev/null and b/1-js/9-prototypes/6-constructor/8.png differ diff --git a/1-js/9-prototypes/6-constructor/8@2x.png b/1-js/9-prototypes/6-constructor/8@2x.png new file mode 100755 index 00000000..fc77fd69 Binary files /dev/null and b/1-js/9-prototypes/6-constructor/8@2x.png differ diff --git a/1-js/9-prototypes/6-constructor/9.png b/1-js/9-prototypes/6-constructor/9.png new file mode 100755 index 00000000..666fd19c Binary files /dev/null and b/1-js/9-prototypes/6-constructor/9.png differ diff --git a/1-js/9-prototypes/6-constructor/9@2x.png b/1-js/9-prototypes/6-constructor/9@2x.png new file mode 100755 index 00000000..3c193118 Binary files /dev/null and b/1-js/9-prototypes/6-constructor/9@2x.png differ diff --git a/1-js/9-prototypes/6-constructor/article.md b/1-js/9-prototypes/6-constructor/article.md new file mode 100644 index 00000000..3cc088ac --- /dev/null +++ b/1-js/9-prototypes/6-constructor/article.md @@ -0,0 +1,141 @@ +# F.prototype по умолчанию, "constructor" + +В стандарте JavaScript есть свойство `constructor`, которое, по идее, должно быть в каждом объекте и указывать, какой функцией он создан. + +Однако на практике оно ведет себя не всегда адекватным образом. Мы рассмотрим, как оно работает и почему именно так, а также -- как сделать, чтобы оно работало правильно. +[cut] + +## Свойство constructor [#constructor] + +По замыслу, свойство `constructor` объекта должно содержать ссылку на функцию, создавшую объект. И в простейших случаях так оно и есть, вот например: + +```js +//+ run +function Rabbit() { } + +var rabbit = new Rabbit(); + +*!* +alert( rabbit.constructor == Rabbit ); // true +*/!* +``` + +Как видим, всё работает. Мы получили из объекта функцию, которая его создала. + +Но всё не так просто. Расширим наш пример установкой `Rabbit.prototype`: + +```js +//+ run +function Rabbit() { } + +Rabbit.prototype = { jumps: true } ; + +var rabbit = new Rabbit(); + +*!* +alert( rabbit.constructor == Rabbit ); // false (упс, потеряли конструктор!) +*/!* +``` + +...Сломалось! Чтобы детальнее понять происходящее -- посмотрим, откуда берется свойство `constructor` и что с ним произошло. + +## Значение prototype по умолчанию + +До этого мы записывали и меняли свойство `prototype`, но оказывается, оно существует и без нашего вмешательства. + +**Свойство `prototype` есть у каждой функции, даже если его не ставить.** + +**Оно создается автоматически вместе с самой функцией, и по умолчанию является пустым объектом с единственным свойством `constructor`, которое ссылается обратно на функцию.** + +Вот такой вид имеет прототип по умолчанию: + +```js +Rabbit.prototype = { + constructor: Rabbit +} +``` + + + +Так что объект `rabbit` не хранит в себе свойство `constructor`, а берёт его из прототипа. + +Сделаем прямую проверку этого: + +```js +//+ run +function Rabbit() { } + +var rabbit = new Rabbit(); + +*!* +alert( rabbit.hasOwnProperty('constructor') ); // false, в объекте нет! + +alert( Rabbit.prototype.hasOwnProperty('constructor') ); // true, да, оно в прототипе +*/!* +``` + +## Потеря constructor + +JavaScript не прилагает никаких усилий для проверки корректности или поддержания правильного значения `constructor`. + +**При замене `Rabbit.prototype` на свой объект, свойство `constructor` из него обычно пропадает.** + +Например: + +```js +//+ run +function Rabbit() { } + +Rabbit.prototype = {}; // (*) + +var rabbit = new Rabbit(); + +alert( rabbit.constructor == Rabbit ); // false +alert( rabbit.constructor == Object ); // true +``` + +Ого! Теперь конструктором `rabbit` является не `Rabbit`, а `Object`. Но почему? + +Вот иллюстрация, которая наглядно демонстрирует причину: + + + +То есть, свойство `constructor` бралось из `Rabbit.prototype`, но мы заменили его на пустой объект `Rabbit.prototype = {}`. В нём свойства `constructor` там уже нет, значит оно ищется дальше по цепочке прототипов, в `{}.__proto__`, то есть берётся из встроенного `Object.prototype`. А там оно равно `Object`. + +Что делать? + +**Если мы хотим, чтобы свойство `constructor` объекта всегда хранило функцию, которая его создала -- об этом нужно позаботиться самостоятельно.** + +То есть, можно не заменять встроенный `Rabbit.prototype`, а расширить его. Или же, если обязательно нужно заменить, скажем при наследовании, то указать `constructor` явно: + +```js +//+ run +function Animal() { } +Animal.prototype.run = function() { } + +function Rabbit() { } +*!* +Rabbit.prototype = Object.create(Animal); +Rabbit.prototype.constructor = Rabbit; +*/!* + +Rabbit.prototype.run = function() { } + +var rabbit = new Rabbit(); + +*!* +alert( rabbit.constructor == Rabbit ); // true +*/!* +``` + +## Итого + +При создании функции, её `prototype` -- это объект с единственным свойством `constructor`, которое указывает обратно на функцию. + +**Забавен тот факт, что больше нигде в спецификации JavaScript свойство `constructor` не упоминается. Интерпретатор не использует его никак и нигде.** + +В результате, в частности, `constructor` теряется при замене `prototype` на новый объект. И ничего страшного в этом нет. + +Но если мы хотим использовать это свойство сами, чтобы при необходимости получать функцию-конструктор объекта, но при смене прототипа нужно самим смотреть, чтобы ненароком не перезаписать его. + + diff --git a/1-js/9-prototypes/7-instanceof/1-strange-instanceof/solution.md b/1-js/9-prototypes/7-instanceof/1-strange-instanceof/solution.md new file mode 100644 index 00000000..6bf55d43 --- /dev/null +++ b/1-js/9-prototypes/7-instanceof/1-strange-instanceof/solution.md @@ -0,0 +1,7 @@ +Да, это выглядит достаточно странно, поскольку объект `a` не создавался функцией `B`. + +Но методу `instanceof` на самом деле вообще не важна функция. Он смотрит на её `prototype` и сверяет его с цепочкой `__proto__` объекта. + +В данном случае `a.__proto__ == B.prototype`, поэтому `instanceof` возвращает `true`. + +По логике `instanceof` именно прототип задаёт "тип объекта", поэтому `instanceof` работает именно так. \ No newline at end of file diff --git a/1-js/9-prototypes/7-instanceof/1-strange-instanceof/task.md b/1-js/9-prototypes/7-instanceof/1-strange-instanceof/task.md new file mode 100644 index 00000000..c8638a4c --- /dev/null +++ b/1-js/9-prototypes/7-instanceof/1-strange-instanceof/task.md @@ -0,0 +1,20 @@ +# Странное поведение instanceof + +[importance 5] + +Почему `instanceof` в коде ниже возвращает `true`, ведь объект `a` явно создан не `B()`? + +```js +//+ run +function A() {} +function B() {} + +A.prototype = B.prototype = {}; + +var a = new A(); + +*!* +alert(a instanceof B); // true +*/!* +``` + diff --git a/1-js/9-prototypes/7-instanceof/2-instanceof-result/solution.md b/1-js/9-prototypes/7-instanceof/2-instanceof-result/solution.md new file mode 100644 index 00000000..071ec9e2 --- /dev/null +++ b/1-js/9-prototypes/7-instanceof/2-instanceof-result/solution.md @@ -0,0 +1,18 @@ +Да, распознает. + +Он проверяет наследование с учётом цепочки прототипов. + +```js +//+ run +function Animal() { } + +function Rabbit() { } +Rabbit.prototype = Object.create(Animal.prototype); + +var rabbit = new Rabbit(); + +alert( rabbit instanceof Rabbit ); // true +alert( rabbit instanceof Animal ); // true +alert( rabbit instanceof Object ); // true +``` + diff --git a/1-js/9-prototypes/7-instanceof/2-instanceof-result/task.md b/1-js/9-prototypes/7-instanceof/2-instanceof-result/task.md new file mode 100644 index 00000000..aa8c4c40 --- /dev/null +++ b/1-js/9-prototypes/7-instanceof/2-instanceof-result/task.md @@ -0,0 +1,23 @@ +# Что выведет instanceof? + +[importance 5] + +В коде ниже создаётся простейшая иерархия классов: `Animal -> Rabbit`. + +Что выведет [instanceof](/instanceof)? + +Распознает ли он `rabbit` как `Animal`, `Rabbit` и к тому же `Object`? + +```js +function Animal() { } + +function Rabbit() { } +Rabbit.prototype = Object.create(Animal.prototype); + +var rabbit = new Rabbit(); + +alert( rabbit instanceof Rabbit ); +alert( rabbit instanceof Animal ); +alert( rabbit instanceof Object ); +``` + diff --git a/1-js/9-prototypes/7-instanceof/article.md b/1-js/9-prototypes/7-instanceof/article.md new file mode 100644 index 00000000..827ad460 --- /dev/null +++ b/1-js/9-prototypes/7-instanceof/article.md @@ -0,0 +1,240 @@ +# Проверка класса: "instanceof" + +Оператор `instanceof` позволяет проверить, какому классу принадлежит объект, с учетом прототипного наследования. +[cut] + + +## Алгоритм работы instanceof [#ref-instanceof] + +Вызов `obj instanceof Constructor` возвращает `true`, если объект принадлежит классу `Constructor` или его родителям. + +Как это часто бывает в JavaScript, здесь есть ряд тонкостей. В некоторых ситуациях, проверка может даже ошибаться! + +Вначале -- простейший пример. + +Вот так, по замыслу, должен работать `instanceof`: + +```js +//+ run +function Rabbit() { } + +*!* +// создаём объект +*/!* +var rabbit = new Rabbit; + +*!* +// проверяем -- этот объект создан Rabbit? +alert(rabbit instanceof Rabbit); // true, верно +*/!* +``` + +Для встроенных объектов: + +```js +//+ run +var arr = []; +alert(arr instanceof Array); // true +alert(arr instanceof Object); // true +``` + +Массив `arr` принадлежит классу `Array`, но также и является объектом `Object`. Это верно, так как массивы наследуют от объектов. + +По этой же логике вызов `rabbit instanceof Object` -- тоже будет `true`, так как `rabbit` является объектом. + +**Алгоритм проверки `obj instanceof Constructor`:** + +
        +
      1. Получить `obj.__proto__`
      2. +
      3. Сравнить `obj.__proto__` с `Constructor.prototype`
      4. +
      5. Если не совпадает, тогда заменить `obj` на `obj.__proto__` и повторить проверку на шаге 2 до тех пор, пока либо не найдется совпадение (результат `true`), либо цепочка прототипов не закончится (результат `false`).
      6. +
      + +В проверки `rabbit instanceof Rabbit`, совпадение находится тут же, на первом же шаге, так как: `rabbit.__proto__ == Rabbit.prototype`. + +А если рассмотреть `rabbit instanceof Rabbit`, то совпадение будет найдено на следующем шаге, т.к. `rabbit.__proto__.__proto__ == Object.prototype`. + +[smart header="Примитивы -- не объекты, доказательство"] +Проверим, является ли число объектом `Number`: + +```js +//+ run +alert(123 instanceof Number); // false, нет! +``` + +...С другой стороны, если создать встроенный объект `Number` (не делайте так): + +```js +//+ run +alert( new Number(123) instanceof Number ); // true +``` + +[/smart] + +[warn header="Не друзья: `instanceof` и фреймы"] + +**Оператор `instanceof` не срабатывает, когда значение приходит из другого окна или фрейма.** + +Например, массив, который создан в ифрейме и передан родительскому окну -- будет массивом *в том ифрейме*, но не в родительском окне. Проверка `instanceof Array` в родительском окне вернёт `false`. + +Вообще, у каждого окна и фрейма -- своя иерархия объектов и свой `window` . + +Как правило, эта проблема возникает со встроенными объектами, в этом случае используется проверка внутреннего свойства `[[Class]]`. Более подробно это описано в главе [](/class-property). +[/warn] + +[smart header="Вызов `instanceof Rabbit` не использует саму функцию `Rabbit`"] +Забавно, что сама функция-констуктор не участвует в процессе проверки! + +Используется только цепочка прототипов для проверяемого объекта и `Rabbit.prototype`. + +Это может приводить к забавному результату и даже ошибкам в проверке при изменении `prototype`, например: + +```js +//+ run +// Создаём объект rabbit, как обычно +function Rabbit() { } +var rabbit = new Rabbit(); + +// изменили prototype... +Rabbit.prototype = {}; + +// ...instanceof перестал работать! +*!* +alert( rabbit instanceof Rabbit ); // false +*/!* +``` + +Стоит ли говорить, что это один из доводов для того, чтобы никогда не менять `prototype`? Так сказать, во избежание... +[/smart] + +## instanceof + наследование + try..catch = ♡ + +Когда мы работаем с внешними данными, возможны самые разные ошибки. + +Создание иерархии ошибок вносит порядок в происходящее, а `instanceof` внутри `try..catch` позволяет легко понять, что за ошибка произошла и обработать, либо пробросить её дальше. + +Для примера создадим функцию `readUser(json)`, которая будет разбирать JSON с данными посетителя. Мы его получаем с сервера -- может, нашего, а может -- чужого, в общем -- желательно проверить на ошибки. А может, это даже и не JSON, а какие-то другие данные -- не важно, для наглядности поработаем с JSON. + +Пример правильного JSON: `{ "name": "Вася", "age": 30 }`. + +Функция `readUser` должна бросать исключение в случаях, когда: + +
        +
      1. В JSON синтаксическая ошибка, то есть "падает" вызов `JSON.parse`.
      2. +
      3. В получившемся объекте нет свойства `name` или `age`.
      4. +
      5. Свойство `age` (возраст) -- не число.
      6. +
      + +Для каждого из этих типов ошибок сделаем отдельный класс -- это поможет позже легко идентифицировать произошедшее: + +
        +
      1. `SyntaxError` -- ошибка "что-то не так в данных", встроенный класс, ошибка такого типа генерируется как раз `JSON.parse`.
      2. +
      3. `PropertyRequiredError` -- ошибка "нет свойства", будет наследовать от `SyntaxError`, так как является подвидом синтаксической ошибки.
      4. +
      5. `FormatError` -- "ошибка форматирования", тоже наследник `SyntaxError`.
      6. +
      + +Вот ошибки в JS: + +```js +function PropertyRequiredError(property) { +*!* + this.property = property; + this.message = "Отсутствует свойство " + property; +*/!* + this.name = 'PropertyRequired'; +} +PropertyRequiredError.prototype = Object.create(SyntaxError.prototype); + + +function FormatError(message) { + this.message = message; + this.name = 'FormatError'; +} +FormatError.prototype = Object.create(SyntaxError.prototype); +``` + +Понятное дело, эти классы ошибок имеют общий характер и могут использоваться не только в данной конкретной функции, но и в других местах кода -- при обработке любых данных. + +**У разных типов ошибок могут быть разные конструкторы, разные дополнительные свойства, которые позволят в дальнейшем удобно работать с ошибкой.** + +В коде выше обратите внимание на `PropertyRequiredError` -- конструтор этой ошибки получает отсутствующее свойство и сохраняет его в дополнительном свойстве `property`, в дополнение к стандартному `message`. В дальнейшем, для особой обработки этой ошибки, его легко можно будет получить. + +Код ниже -- полная реализация `readUser`: + +```js +function PropertyRequiredError(property) { + this.property = property; + this.message = "Отсутствует свойство " + property; + this.name = 'PropertyRequired'; +} +PropertyRequiredError.prototype = Object.create(SyntaxError.prototype); + + +function FormatError(message) { + this.message = message; + this.name = 'FormatError'; +} +FormatError.prototype = Object.create(SyntaxError.prototype); + + +function readUser(data) { + + var user = JSON.parse(data); + + validateUser(user); + + return user; +} + +function validateUser(user) { + + if (!user.age) { + throw new PropertyRequiredError("age"); + } + if (typeof user.age != "number") { + throw new FormatError("Возраст - не число"); + } + + if (!user.name) { + throw new PropertyRequiredError("name"); + } + +} + +try { + readUser('{ "name": "Вася", "age": "unknown" }'); +} catch (e) { +*!* + if (e instanceof PropertyRequiredError) { + if (e.property == 'name') { + // в данном месте кода возможны анонимы, ошибка поправима + user[e.property] = "Аноним"; + } else { + alert(e.message); + } + } else if (e instanceof SyntaxError) { + alert("Ошибка в данных: " + e.message); + } else { + throw e; // неизвестная ошибка, не знаю что с ней делать + } +*/!* +} +``` + +Обратим внимание -- в данном конкретном месте кода мы допускаем анонимных посетителей, поэтому в случае, если отсутствует `name` -- исправляем эту ошибку. Мы можем легко это сделать, благодаря наличию у `PropertyRequiredError` дополнительного (по сравнению со стандартными ошибками) свойства `property`. + +**Для проверки, какая именно ошибка произошла, вместо `e.name` используется `instanceof`.** + +Это позволяет как выделить какие-то отдельные типы ошибок (`e instanceof PropertyRequiredError`), так и проверить общий тип, с которым мы умеем работать, без оглядки на его детали (`e instanceof SyntaxError`). + +Благодаря `instanceof` мы получили удобную поддержку иерархии ошибок, с возможностью в любой момент добавить новые классы, понятным кодом и предсказуемым поведением. + +## Итого + +
        +
      • Оператор `obj instanceof Func` проверяет тот факт, что `obj` является результатом вызова `new Func`. Он учитывает цепочку `__proto__`, поэтому наследование поддерживается.
      • +
      • Оператор `instanceof` не сможет проверить тип значения, если объект создан в одном окне/фрейме, а проверяется в другом. Это потому, что в каждом окне -- своя иерархия объектов. Для точной проверки типов встроенных объектов можно использовать свойство `[[Class]]`.
      • +
      • Оператор `instanceof` особенно востребован в случаях, когда мы работаем с иерархиями классов. Это наилучший способ проверить принадлежность тому или иному классу с учётом наследования.
      • +
      + + diff --git a/1-js/9-prototypes/8-class-extend/article.md b/1-js/9-prototypes/8-class-extend/article.md new file mode 100644 index 00000000..3483325e --- /dev/null +++ b/1-js/9-prototypes/8-class-extend/article.md @@ -0,0 +1,300 @@ +# Фреймворк Class.extend + +Можно использовать прототипное наследование и не повторять `Rabbit.prototype.method = ...` при определении каждого метода, не иметь проблем с конструкторами и так далее. + +Для этого используют ООП-фреймворк -- библиотеку, в которой об этом подумали "за нас". + +В этой главе мы рассмотрим один из таких фреймворков: +
        +
      • С удобным синтаксисом наследования.
      • +
      • С вызовом родительских методов.
      • +
      • С поддержкой статических методов и *примесей*.
      • +
      + +Можно сказать, что фреймворк представляет собой "синтаксический сахар" к наследованию на классах. +[cut] + +Оригинальный код этого фреймворка были предложены Джоном Ресигом: [Simple JavaScript Inheritance](http://ejohn.org/blog/simple-javascript-inheritance/), но подход это не новый, его варианты используются во многих фреймворках и знакомство с ним будет очень кстати. + +Полный код фреймворка: [class-extend.js](http://js.cx/libs/class-extend.js). Он содержит много комментариев, чтобы было проще его понять, но смотреть его лучше после того, как ознакомитесь с возможностями. + +## Создание класса + +Итак, начнём. + +Фреймворк предоставляет всего один метод: `Class.extend`. + +Чтобы представлять себе, как выглядит класс "на фреймворке", взглянем на рабочий пример: + +```js +//+ run +// Объявление класса Animal +var Animal = *!*Class.extend*/!*({ + + init: function(name) { + this.name = name; + }, + + run: function() { + alert(this.name + " бежит!"); + } + +}); + +// Создать (вызовется `init`) +var animal = new Animal("Зверь"); + +// Вызвать метод +animal.run(); // "Зверь бежит!" +``` + +Готово, создан класс `Animal`. + +Внутри `Class.extend(props)` делает следующее: + +
        +
      • Создаётся объект, в который копируются свойства и методы из `prop`. Это будет прототип.
      • +
      • Объявляется функция, которая вызывает `this.init`, в её `prototype` записывается созданный прототип.
      • +
      • Эта функция возвращается, она и есть конструктор `Animal`.
      • +
      + +Как видим, всё весьма просто. + +Но фреймворк этим не ограничивается и добавляет ряд других интересных возможностей. + +## Статические свойства + +У метода `Class.extend` есть и второй, необязательный аргумент: объект `staticProps`. + +Если он есть, то его свойства копируются в саму функцию-конструктор. + +Например: + +```js +//+ run +// Объявить класс Animal +var Animal = Class.extend({ + + init: function(name){ + this.name = name; + }, + + toString: function(){ + return this.name; + } + +}, +*!* +{ // статические свойства + compare: function(a, b) { + return a.name - b.name; + } +}); +*/!* + +var arr = [new Animal('Зорька'), new Animal('Бурёнка')] + +*!* +arr.sort(Animal.compare); +*/!* + +alert(arr); // Бурёнка, Зорька +``` + +## Наследование + +Метод `extend` копируется в создаваемые классы. + +Поэтому его можно вызывать на любом конструкторе, чтобы создать ему класс-наследник. + +Например, создадим `Rabbit`, наследующий от `Animal`: + +```js +//+ run +// Создать Animal, всё как обычно +var Animal = Class.extend({ + init: function(name) { + this.name = name; + }, + run: function() { + alert(this.name + ' бежит!'); + } +}); + +// Объявить класс Rabbit, *!*наследующий*/!* от Animal +var Rabbit = *!*Animal.extend*/!*({ + + init: function(name) { +*!* + this._super(name); // вызвать родительский init(name) +*/!* + }, + + run: function() { + this._super(); // вызвать родительский run + alert('..и мощно прыгает за морковкой!'); + } + +}); + +*!* +var rabbit = new Rabbit("Кроль"); +rabbit.run(); // "Кроль бежит!", затем "..и мощно прыгает за морковкой!" +*/!* +``` + +## Метод this._super + +В коде выше появился ещё один замечательный метод: `this._super`. + +**Вызов `this._super(аргументы)` вызывает метод *родительского класса*, с указанными аргументами.** + +То есть, здесь он запустит родительский `init(name)`: + +```js +init: function(name) { + this._super(name); // вызвать родительский init(name) +} +``` + +...А здесь -- родительский `run`: + +```js +run: function() { + this._super(); // вызвать родительский run + alert('..и мощно прыгает за морковкой!'); +} +``` + +Работает это, примерно, так: когда фреймворк копирует методы в прототип, он смотрит их код, и если видит там слово `_super`, то оборачивает метод в обёртку, которая ставит `this._super` в метод родителя, затем вызывает метод, а затем возвращает `this._super` как было ранее. + +Это вызывает некоторые дополнительные расходы при объявлении, так как чтобы проверить, есть ли обращение к `_super`, фреймворк при копировании методов преобразует их через `toString` в строку и ищет в ней обращение. + +Как правило, эти расходы несущественны, если нужно их минимизировать -- не составляет труда изъять эту возможность из фреймворка или учесть в инструментах сжатия (минификации) кода. + +Кстати, примерно это минификатор Google Closure Compiler, когда сжимает код, написанный на "дружащей" с ним Google Closure Library. + + +## Примеси [#mixins] + +Согласно теории ООП, *примесь* (англ. mixin) -- класс, реализующий какое-либо чётко выделенное поведение, который не предназначен для порождения самостоятельно используемых объектов, а используется для *уточнения* поведения других классов. + +Иными словами, *примесь* позволяет легко добавить в существующий класс новые возможности, например: +
        +
      • Публикация событий и подписка на них.
      • +
      • Работ c шаблонизатором.
      • +
      • ... любое поведение, дополняющее объект.
      • +
      + +**Как правило, примесь реализуется в виде объекта, свойства которого копируются в прототип.** + +Например, напишем примесь `EventMixin` для работы с событиями. Она будет содержать три метода -- `on/off` (подписка) и `trigger` (генерация события): + +```js +var EventMixin = { + + on: function (eventName, handler) { + if (!this._eventHandlers) this._eventHandlers = {}; + if (!this._eventHandlers[eventName]) { + this._eventHandlers[eventName] = []; + } + + this._eventHandlers[eventName].push(handler); + }, + + off: function(eventName, handler) { + ... + }, + + trigger: function (eventName, args) { + if (!this.eventHandlers || !this._eventHandlers[eventName]) { + return; + } + + var handlers = this._eventHandlers[eventName]; + for (var i = 0; i < handlers.length; i++) { + handlers[i].apply(this, args); + } + } + +}; +``` + +Скопировав свойства из `EventMixin` в любой объект, мы дадим ему возможность генерировать события (`trigger`) и подписываться на них (`on/off`). + +Чтобы было проще, во фреймворк добавлена возможность указания примесей при объявлении класса. + +**Для добавления примесей у метода `Class.extend` существует синтаксис с первым аргументом-массивом:** +
        +
      • **`Class.extend([mixin1, mixin2...], props, staticProps)`.**
      • +
      + +Если первый аргумент -- массив, то его элементы `mixin1, mixin2..` записываются в прототип по очереди, перед `props`, примерно так: + +```js +for(var key in mixin1) prototype[key] = mixin1[key]; +for(var key in mixin2) prototype[key] = mixin2[key]; +... +for(var key in props) prototype[key] = props[key]; +``` + +При этом, если названия методов совпадают, то последующий затрёт предыдущий, так как в объекте может быть только одно свойство с данным названием. Впрочем, обычно такого не происходит, т.к. примеси проектируются так, чтобы их методы были уникальными и ни с чем не конфликтовали. + +Применение: + +```js +*!* +var Rabbit = Class.extend( [ EventMixin ], { +*/!* + + /* свойства и методы для Rabbit */ + +}); + +var rabbit = new Rabbit(); + +*!*rabbit.on*/!*("jump", function() { // повесить функцию на событие jump + alert("jump &-@!"); +}); + +*!*rabbit.trigger*/!*('jump'); // alert сработает! +``` + +Примеси могут быть самыми разными. Например `TemplateMixin` для работы с шаблонами: + +```js +Rabbit = Class.extend([EventMixin, TemplateMixin], { + + /* Теперь Rabbit умеет использовать события и шаблоны */ + +}); +``` + +Красиво, не правда ли? Всего лишь указали одну-другую примесь и объект уже всё умеет! + +Примеси могут задавать и многое другое, например автоматически подписывать компонент на стандартные события, добавлять AJAX-функционал и т.п. + +## Итого + +
        +
      1. **Фреймворк имеет основной метод `Class.extend` с несколькими вариациями:** +
          +
        • `Class.extend(props)` -- просто класс с прототипом `props`.
        • +
        • `Class.extend(props, staticProps)` -- класс с прототипом `props` и статическими свойствами `staticProps`.
        • +
        • `Class.extend(mixins, props [, staticProps])` -- если первый аргумент массив, то он интерпретируется как примеси. Их свойства копируются в прототип перед `props`.
        • +
        +
      2. +
      3. **У созданных этим методом классов также есть `extend` для продолжения наследования.**
      4. +
      5. **Методы родителя можно вызвать при помощи `this._super(...)`.**
      6. +
      + +Плюсы и минусы: +[compare] ++Такой фреймворк удобен потому, что класс можно задать одним вызовом `Class.extend`, с читаемым синтаксисом, удобным наследованием и вызовом родительских методов. +-Редакторы и IDE, как правило, не понимают такой синтаксис, а значит, не предоставляют автодополнение. При этом они обычно понимают объявление методов через явную запись в объект или в прототип. +-Есть некоторые дополнительные расходы, связанные с реализацией `_super`. Если они критичны, то их можно избежать.[/compare] + +То, как работает фреймворк, подробно описано в комментариях: [class-extend.js](http://js.cx/libs/class-extend.js). +[head] + +[/head] \ No newline at end of file diff --git a/1-js/9-prototypes/9-why-prototypes-better/article.md b/1-js/9-prototypes/9-why-prototypes-better/article.md new file mode 100644 index 00000000..1bb6e93a --- /dev/null +++ b/1-js/9-prototypes/9-why-prototypes-better/article.md @@ -0,0 +1,266 @@ +# Сравнение с функциональным наследованием + +В этой главе мы озаботимся тем, чтобы сравнить прототипный и функциональный подход к ООП. + +Причём, сделать это грамотно, с учётом того, что реально происходит "под капотом" интерпретатора. + +Нас интересуют три показателя: +
        +
      1. Эффективность по памяти.
      2. +
      3. Скорость работы.
      4. +
      5. Архитектурные ограничения (если есть) и плюшки.
      6. +
      + +[cut] + +## Класс Machine + +Объявим класс `Machine` двумя способами: +
      +
      Функциональное объявление:
      +
      Состоит из единственной функции-конструктора, которая записывает в объект всё, что нужно. Приватные данные сохраняются в локальные переменные и доступны через замыкание: + +```js +function MachineOne(power) { + var enabled = false; + + this.enable = function() { enabled = true; }; + this.disable = function() { enabled = false; }; + + // ... +} +``` + +При этом у каждого объекта будет своя копия методов `enable` и `disable`, которые создаются каждый раз заново. +
      +
      Прототипное объявление:
      +
      В объекте хранится только то, что ему нужно. Методы записываются в прототип: + +```js +function MachineTwo(power) { + this._enabled = false; +} + +MachineTwo.prototype.enable = function() { + this._enabled = true; +}; + +MachineTwo.prototype.disable = function() { + this._enabled = false; +}; +``` + +
      +
      + +## Сравнение памяти + +Оценим затраты памяти в функциональном стиле: + +```js +var machine = new MachineOne(); + +затраты памяти = + сам объект + + свойства и методы в нём (this.enable/disable) + + замыкание, объект с приватными переменными (var enabled) +``` + +В этой, казалось бы, очевидной формуле кроется серьёзная ошибка. + +По коду кажется, каждый объект хранит свою копию методов `this.enable/disable`, однако это не совсем так. + +Интерпретаторы оптимизируют создание и хранение одинаковых одинаковых функций. "Под капотом" *строка с кодом* функции `enable/disable` хранится только один раз, и её разделяют между собой все объекты `MachineOne`. То есть, если вывести функцию в виде строки `alert(machine.enable)`, то каждый объект возьмёт код из единого для всех места в памяти. + +Далее, при использовании, строка с кодом на JavaScript превращается в *машинный код*, который может по-разному оптимизироваться, в зависимости от того, как именно используется функция, но и здесь интерпретатор старается разделять одинаково оптимизированный код между объектами. + +**То есть, на самом деле в каждом объекте хранится не полная копия метода, а скорее "метаданные", которые указывают, где в памяти лежит соответствующим образом оптимизированная функция.** + +Теперь прототипный стиль: + +```js +var machine = new MachineTwo(); + +затраты памяти = + сам объект + + свойства (this._enabled) +``` + +Если сравнить, то мы видим, что значение `var enabled` переместилось в сам объект, произошла небольшая экономия на объекте LexicalEnvironment, который больше не нужен. + +Кроме того, методы находятся в прототипе. Интерпретатор делает неплохую работу по оптимизации функционального стиля можно сказать, что "почти вся" информация о функциях будет разделяться между объектами, но в прототипном подходе функции разделяются на 100%, без "почти". + +**Вывод: прототипный стиль требует меньше памяти, так как не хранится LexicalEnvironment и методы (совсем).** + +В случае, когда объект хранит мало данных, и методы маленькие, разница в памяти может быть существенной. В браузере Chrome (V8) для описанных выше `MachineOne` и `MachineTwo` она может составлять 5-8 раз. Но это лишь потому, что объекты полностью синтетические, в них почти нет кода и данных. В реальности она меньше, порядка 1-3 раз, конечно это зависит от конкретного объекта. + +## Сравнение производительности + +Создание объекта в функциональном стиле дольше, поскольку происходят присвоения в `this`. Это очевидно. + +Но может показаться, что при этом скорость доступа к таким методом "особо быстрая", так как они хранятся в самом объекте, а не в его прототипе. + +Это не так. + +"Под капотом" интерпретатор при первом вызове метода пробежится по цепочке `__proto__`, запомнит место, где его нашёл, и далее будет обращаться прямо туда. + +**В современных браузерах скорость доступа к методам в прототипе и в объекте одинакова.** + +Функциональный стиль и здесь не имеет преимущества. + +## Красота синтаксиса + +В функциональном стиле мы имеем красивые приватные переменные и функции. Это хорошо. + +Но пользоваться публичными методами менее удобно. + +Скажем, мы хотим при создании `new Machine` тут же включить машину вызовом `this.enable()`: + +```js +function Machine(power) { + var enabled = false; + + this.enable = function() { enabled = true; }; + this.disable = function() { enabled = false; }; + +*!* + // нужно писать этот вызов внизу + this.enable(); +*/!* +} +``` + +Мы вынуждены написать вызов `this.enable()` внизу, под определением соответствующего метода. + +**Если методов много и они длинные, то получается, что при чтении кода нам нужно проматывать в конец файла. Это неудобно!** + +Типичное средство обхода -- объявлять все методы через Function Declaration, а внизу выносить во внешний интерфейс нужные: + +```js +function Machine(power) { + var enabled = false; + + enable(); + + function enable() { enabled = true; }; + function disable() { enabled = false; }; + +*!* + this.enable = enable; + this.disable = disable; +*/!* +} +``` + +Ничего такого, но приходится писать лишние буквы, а у программиста и так нелёгкий труд. + +Прототипное наследование похожей проблемы не имеет. Зато там нужно писать слово `prototype`, что, впрочем, исправляется различными ООП-фреймворками. + +## Архитектурные ограничения + +Наследование, реализованное в функциональном стиле, обладает важным архитектурным ограничением. + +**Конструктор наследника получает контроль лишь после полной инициализации родителя, и это может быть слишком поздно.** + +Например, пусть конструктор `Machine` при инициализации вызывает свой метод `work()`. Это достаточно типично, что при создании объект тут же делает что-то полезное или заполняет себя важными данными. + +Потомок -- `CoffeeMachine` захочет переопределить этот метод. Реализация будет выглядеть так: + +```js +//+ run +// Родитель: + +function Machine() { + this.work = function() { + alert('Гр-р-р-р! Бям-бямс...'); + }; + + this.work(); +} + +// Потомок: + +function CoffeeMachine() { + Machine.apply(this, arguments); + +*!* + // попытаемся переопределить метод в потомке + this.work = function() { + alert('Вжжжжжжжжж!'); + }; +*/!* +} + +// переопределение не сработало! +*!* +var coffeeMachine = new CoffeeMachine(); // Гр-р-р-р! Бям-бямс...! +*/!* +``` + +Вызвался метод `work` не потомка, а родителя! + +Это естественно, ведь первым делом мы вызвали `Machine.apply(this, arguments)`, в котором используется старый `work`. + +**Методы для инициализации, уже использованные родителем, переопределить в потомке нельзя: слишком поздно.** + +Недостаток этот -- весьма серьёзный. Фактически, он ограничивает возможности построения архитектуры. + +Заметим, что при использовании прототипов такой проблемы не возникает. Потому что сначала полностью задаются конструкторы, методы, задаётся порядок поиска через прототипы, а уже *потом* создаются объекты. + +Аналогичный код через прототипы: + +```js +//+ run +function Machine() { + this.work(); +} +Machine.prototype.work = function() { + alert('Гр-р-р-р! Бям-бямс...'); +}; + +function CoffeeMachine() { + Machine.apply(this, arguments); +} +CoffeeMachine.prototype = Object.create(Machine.prototype); + +CoffeeMachine.prototype.work = function() { + alert('Вжжжжжжжжж!'); +}; + +// переопределение сработает, work найден в CoffeeMachine.prototype +*!* +var coffeeMachine = new CoffeeMachine(); // Вжжжжжжжжж! +*/!* +``` + +## Не учитывается наследование в instanceof + +Есть и ещё одна проблема функционального подхода. + +При наследовании в функциональном стиле проверка `coffeeMachine instanceof Machine` вернёт `false`. + +Это вполне естественно, ведь, формально говоря, `CoffeeMachine` не является `Machine`. + +Единственная связь между ними -- конструктор `CoffeeMachine` вызвал функцию `Machine` в своём контексте. Оператор `instanceof` работает через проверку цепочки прототипов, а здесь её нет. + +**Здесь прототипный подход гораздо удобнее.** + +Конечно, можно попробовать запоминать, кого и в каком порядке вызывали, разработать свой аналог `instanceof`, но обычно так не делают, т.к. в прототипах встроенный `instanceof` просто работает. + +## Сжатие JavaScript + +При функциональном наследовании используются локальные переменные и функции. + +Современные средства сжатия JavaScript переименовывают их, делая короче и таким образом уменьшая размер кода. + +**Это означает, что код, написанный в функциональном стиле, сожмётся лучше.** + +## Итого + +Получилось, что функциональный паттерн в сочетании с наследованием обладает рядом серьёзных проблем. + +Его, по сути, основное достоинство -- это использование локальных функций и переменных, в которые никак нельзя залезть снаружи, и которые дают лучшее сжатие кода минификаторами. + +Кроме того, если программировать без фреймворков, то функциональный стиль -- наиболее нагляден и прост. + +Но если в проекте нужен единообразный стиль ООП, то лучше использовать прототипный подход, возможно прибавив "сахарку" в виде ООП-фреймворка (тысячи их), а функциональный использовать в тех случаях, когда *уже есть* сторонняя библиотека или конструкторы в этом стиле, которые нужно расширить. diff --git a/1-js/9-prototypes/index.md b/1-js/9-prototypes/index.md new file mode 100644 index 00000000..e9dc66dd --- /dev/null +++ b/1-js/9-prototypes/index.md @@ -0,0 +1,3 @@ +# ООП в прототипном стиле + +Изучаем прототипы -- де-факто стандарт объектно-ориентированной разработки, а также их использование для наследования. \ No newline at end of file diff --git a/1-js/index.md b/1-js/index.md new file mode 100644 index 00000000..f3fadc3a --- /dev/null +++ b/1-js/index.md @@ -0,0 +1,5 @@ +# Язык JavaScript + +Эта часть позволит вам изучить JavaScript с нуля или упорядочить и дополнить существующие знания. + +Мы будем использовать браузер в качестве окружения, но основное внимание будет уделяться именно самому языку JavaScript. \ No newline at end of file diff --git a/2-ui/1-document/1-browser-environment/article.md b/2-ui/1-document/1-browser-environment/article.md new file mode 100644 index 00000000..df975514 --- /dev/null +++ b/2-ui/1-document/1-browser-environment/article.md @@ -0,0 +1,45 @@ +# Окружение: DOM, BOM и JS + +Сам по себе язык JavaScript не предусматривает работы с браузером, он вообще не знает про HTML. + +Но в браузере есть ряд специальных объектов, которые образуют Document Object Model (DOM) и дают доступ к документу, а также объекты Browser Object Model (BOM), которые позволяют использовать различные возможности браузера, такие как коммуникация с сервером, открытие новых окон и т.п. +[cut] + +На рисунке ниже схематически отображена структура, которая получается если посмотреть на совокупность браузерных объектов с "высоты птичьего полёта". + + + +Как видно из рисунка, на вершине стоит `window`, который играет роль *глобального объекта*, но вместе с этим даёт доступ к функционалу по управлению окном браузера, у него есть методы `window.focus()`, `window.open()` и другие. +Все остальные объекты делятся на 3 группы. + +## Объектная модель документа (DOM) + +Глобальный объект `document` даёт возможность взаимодействовать с содержимым страницы. + +Он и громадное количество его свойств, методов и связанных с ним интерфейсов описаны в [стандарте W3C DOM](http://www.w3.org/DOM/DOMTR). + +По историческим причинам когда-то появилась первая версия стандарта DOM Level 1, затем придумали ещё свойства и методы, и появился DOM Level 2, на текущий момент поверх них добавили ещё DOM Level 3 и готовится DOM 4. + +Современные браузеры также поддерживают некоторые возможности, которые не вошли в стандарты, но де-факто существуют давным-давно и отказываться от них никто не хочет. Их условно называют "DOM Level 0". + +Также информацию по работе с элементами страницы можно найти в стандарте [HTML 5](http://www.w3.org/TR/html5/Overview.html). + +## Объектная модель браузера (BOM) + +BOM -- это объекты для работы с чем угодно, кроме документа. + +Например: +
        +
      • Объект [navigator](https://developer.mozilla.org/en/DOM/window.navigator) содержит общую информацию о браузере и операционной системе. Особенно примечательны два свойства: `navigator.userAgent` -- содержит информацию о браузере и `navigator.platform` -- содержит информацию о платформе, позволяет различать Windows/Linux/Mac и т.п.
      • +
      • Объект [location](https://developer.mozilla.org/en-US/docs/Web/API/Window.location) содержит информацию о текущем URL страницы и позволяет перенаправить посетителя на новый URL.
      • +
      • Функции `alert/confirm/prompt` -- тоже входят в BOM. +
      • +
      + +Большинство возможностей BOM стандартизированы в [HTML 5](http://www.w3.org/TR/html5/Overview.html), но браузеры любят изобрести что-нибудь своё, особенное. + +## Объекты и функции JavaScript + +JavaScript -- объекты и методы языка JavaScript, который даёт возможность управлять всем этим. Именно их описывает стандарт EcmaScript. + +Далее в этом курсе мы будем, преимущественно, изучать DOM, поскольку именно документ занимает центральную роль в организации интерфейса, и работа с ним -- сложнее всего. \ No newline at end of file diff --git a/2-ui/1-document/1-browser-environment/windowObjects.png b/2-ui/1-document/1-browser-environment/windowObjects.png new file mode 100755 index 00000000..5407ce72 Binary files /dev/null and b/2-ui/1-document/1-browser-environment/windowObjects.png differ diff --git a/2-ui/1-document/10-compare-document-position/article.md b/2-ui/1-document/10-compare-document-position/article.md new file mode 100644 index 00000000..e52dbd5a --- /dev/null +++ b/2-ui/1-document/10-compare-document-position/article.md @@ -0,0 +1,209 @@ +# Методы contains и compareDocumentPosition + +Если есть два элемента, то иногда бывает нужно понять, находится ли один в другом, и произвести обработку в зависимости от результата. + +Обычные поисковые методы здесь не дают ответа, но есть два специальных. Они используются редко, но когда подобная задача встаёт, то знание метода может сэкономить много строк кода. + +[cut] + +## Метод contains + +Синтаксис: + +```js +var result = parent.contains(child); +``` + +Возвращает `true`, если `parent` содержит `child` или `parent == child`. + +## Метод compareDocumentPosition + +Бывает, что у нас есть два элемента, к примеру, `
    9. ` в списке, и нужно понять, какой из них выше другого. + +Это поможет сделать другой метод. + +Синтаксис: + +```js +var result = nodeA.compareDocumentPosition(nodeB); +``` + +Возвращаемое значение -- битовая маска (см. [](/bitwise-operators)), биты в которой означают следующее: + + + + + + + + + + + + + + +
      БитыЧислоЗначение
      0000000`nodeA` и `nodeB` -- один и тот же узел
      0000011Узлы в разных документах (или один из них не в документе)
      0000102`nodeB` предшествует `nodeA` (в порядке обхода документа)
      0001004`nodeA` предшествует `nodeB`
      0010008`nodeB` содержит `nodeA`
      01000016`nodeA` содержит `nodeB`
      10000032Зарезервировано для браузера
      + +Понятие "предшествует" -- означает не только "предыдущий сосед при общем родителе", но и имеет более общий смысл: "раньше встречается в порядке [прямого обхода](http://algolist.manual.ru/ds/walk.php) дерева документа. + +Могут быть и сочетания битов. Примеры реальных значений: + +```html + +

      ...

      +
        +
      • 1.1
      • +
      +

      ...

      + + +``` + +Комментарии: +
        +
      1. Узлы являются соседями, поэтому стоит только бит "предшествования": какой перед каким.
      2. +
      3. Здесь стоят сразу два бита: `10100` означает, что `ul` одновременно содержит `ul.firstChild` и является его предшественником, то есть при прямом обходе дерева документа сначала встречается `nodeA`, а потом `nodeB`. +Аналогично, `1010` означает, что `ul.parentNode` содержит `ul` и предшествует ему.
      4. +
      5. Так как ни один из узлов не является предком друг друга, то стоит только бит предшествования: `li` предшествует последнему узлу документа, никакого сюрприза здесь нет.
      6. +
      + +[smart header="Перевод в двоичную систему"] +Самый простой способ самостоятельно посмотреть, как число выглядит в 2-ной системе -- вызвать для него `toString(2)`, например: + +```js +//+ run +var x = 20; +alert( x.toString(2) ); // "10100" +``` + +Или так: + +```js +//+ run +alert( 20..toString(2) ); +``` + +Здесь после `20` две точки, так как если одна, то JS подумает, что после неё десятичная часть -- будет ошибка. +[/smart] + +Проверка условия "`nodeA` содержит `nodeB`" с использованием битовых операций: `nodeA.compareDocumentPosition(nodeB) & 16`, например: + +```html + +
        +
      • 1
      • +
      + + +``` + +Более подробно о битовых масках: [](/bitwise-operators). + +## Поддержка в IE8- + +В IE8- поддерживаются свои, нестандартные, метод и свойство: + +
      +
      [nodeA.contains(nodeB)](http://msdn.microsoft.com/en-us/library/ms536377.aspx)
      +
      Результат: `true`, если `nodeA` содержит `nodeB`, а также в том случае, если `nodeA == nodeB`.
      +
      [node.sourceIndex](http://msdn.microsoft.com/en-us/library/ms534635.aspx)
      +
      Номер элемента `node` в порядке прямого обхода дерева. Только для узлов-элементов.
      +
      + +На их основе можно написать кросс-браузерную реализацию `compareDocumentPosition`: + +```js +// Адаптировано с http://ejohn.org/blog/comparing-document-position/ +function compareDocumentPosition(a, b) { + return a.compareDocumentPosition ? + a.compareDocumentPosition(b) : + (a != b && a.contains(b) && 16) + + (a != b && b.contains(a) && 8) + + (a.sourceIndex >= 0 && b.sourceIndex >= 0 ? + (a.sourceIndex < b.sourceIndex && 4) + + (a.sourceIndex > b.sourceIndex && 2) : + 1); +} +``` + +Эта функция будет работать для узлов-элементов во всех браузерах. + +## Итого + +Для проверки, лежит ли один узел внутри другого, достаточно метода `nodeA.contains(nodeB)`. + +Для расширенной проверки на предшествование есть метод `compareDocumentPosition`, кросс-браузерный вариант которого приведён выше. + +Пример использования: + +```html + +
        +
      • 1
      • +
      • 2
      • +
      + + +``` + +Список битовых масок для проверки: + + + + + + + + + + + + +
      БитыЧислоЗначение
      0000000`nodeA` и `nodeB` -- один и тот же узел
      0000011Узлы в разных документах (или один из них не в документе)
      0000102`nodeB` предшествует `nodeA` (в порядке обхода документа)
      0001004`nodeA` предшествует `nodeB`
      0010008`nodeB` содержит `nodeA`
      01000016`nodeA` содержит `nodeB`
      diff --git a/2-ui/1-document/11-modifying-document/1-createtextnode-vs-innerhtml/solution.md b/2-ui/1-document/11-modifying-document/1-createtextnode-vs-innerhtml/solution.md new file mode 100644 index 00000000..9bfaa536 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/1-createtextnode-vs-innerhtml/solution.md @@ -0,0 +1,31 @@ +Результат выполнения может быть разный: `innerHTML` вставит именно HTML, а `createTextNode` интерпретирует теги как текст. + +Запустите следующие примеры, чтобы увидеть разницу: +
        +
      • `createTextNode` создает текст '<b>текст</b>': + +```html + +
        + +``` + +
      • +
      • `innerHTML` присваивает HTML <b>текст</b>: + +```html + +
        + +``` + +
      • +
      diff --git a/2-ui/1-document/11-modifying-document/1-createtextnode-vs-innerhtml/task.md b/2-ui/1-document/11-modifying-document/1-createtextnode-vs-innerhtml/task.md new file mode 100644 index 00000000..eea47737 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/1-createtextnode-vs-innerhtml/task.md @@ -0,0 +1,21 @@ +# createTextNode vs innerHTML + +[importance 5] + +Есть *пустой* узел DOM `elem`. + +**Одинаковый ли результат дадут эти скрипты?** + +Первый: + +```js +elem.appendChild(document.createTextNode(text)); +``` + +Второй: + +```js +elem.innerHTML = text; +``` + +Если нет -- дайте пример значения `text`, для которого результат разный. \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/10-clock-settimeout/solution.md b/2-ui/1-document/11-modifying-document/10-clock-settimeout/solution.md new file mode 100644 index 00000000..f71c63c0 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/10-clock-settimeout/solution.md @@ -0,0 +1,7 @@ +Общее решение описано в [аналогичной задаче с setInterval](/task/clock-setinterval). + +Способ через `setTimeout` -- по сути, такой же, только функция `update` каждый раз ставит себя в очередь заново. + +Заметим, что в данном случае целесообразнее использовать `setInterval`, т.к. нужна не задержка между запусками, а просто запуск каждую секунду. + +[edit src="solution"]Открыть решение в песочнице[/edit] diff --git a/2-ui/1-document/11-modifying-document/10-clock-settimeout/solution.view/index.html b/2-ui/1-document/11-modifying-document/10-clock-settimeout/solution.view/index.html new file mode 100755 index 00000000..ceec78e3 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/10-clock-settimeout/solution.view/index.html @@ -0,0 +1,58 @@ + + + + + + + +
      + hh:mm:ss +
      + + + + + + + + + diff --git a/2-ui/1-document/11-modifying-document/10-clock-settimeout/source.view/index.html b/2-ui/1-document/11-modifying-document/10-clock-settimeout/source.view/index.html new file mode 100755 index 00000000..57b6c3f2 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/10-clock-settimeout/source.view/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/2-ui/1-document/11-modifying-document/10-clock-settimeout/task.md b/2-ui/1-document/11-modifying-document/10-clock-settimeout/task.md new file mode 100644 index 00000000..e227269f --- /dev/null +++ b/2-ui/1-document/11-modifying-document/10-clock-settimeout/task.md @@ -0,0 +1,10 @@ +# Часики при помощи "setTimeout" + +[importance 3] + +Создайте цветные часы, **используя `setTimeout` вместо `setInterval`**: + +[iframe src="solution"] + +[edit src="source" task/] + diff --git a/2-ui/1-document/11-modifying-document/2-remove-elements/solution.md b/2-ui/1-document/11-modifying-document/2-remove-elements/solution.md new file mode 100644 index 00000000..0d6d1ec6 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/2-remove-elements/solution.md @@ -0,0 +1,16 @@ +Родителя `parentNode` можно получить из `elem`. + +Нужно учесть два момента. +
        +
      1. Родителя может не быть (элемент уже удален или еще не вставлен).
      2. +
      3. Для совместимости со стандартным методом нужно вернуть удаленный элемент.
      4. +
      + +Вот так выглядит решение: + +```js +function remove(elem) { + return elem.parentNode ? elem.parentNode.removeChild(elem) : elem; +} +``` + diff --git a/2-ui/1-document/11-modifying-document/2-remove-elements/task.md b/2-ui/1-document/11-modifying-document/2-remove-elements/task.md new file mode 100644 index 00000000..763e2ab7 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/2-remove-elements/task.md @@ -0,0 +1,23 @@ +# Удаление элементов + +[importance 5] + +Напишите функцию, которая удаляет элемент из DOM. + +Синтаксис должен быть таким: `remove(elem)`, то есть, в отличие от `parentNode.removeChild(elem)` -- без родительского элемента. + +```html +
      Это
      +
      Все
      +
      Элементы DOM
      + + +``` + diff --git a/2-ui/1-document/11-modifying-document/3-insert-after/solution.md b/2-ui/1-document/11-modifying-document/3-insert-after/solution.md new file mode 100644 index 00000000..d935d32a --- /dev/null +++ b/2-ui/1-document/11-modifying-document/3-insert-after/solution.md @@ -0,0 +1,29 @@ +Для того, чтобы добавить элемент *после* `refElem`, мы можем вставить его *перед* `refElem.nextSibling`. + +Но что если `nextSibling` нет? Это означает, что `refElem` является последним потомком своего родителя и можем использовать `appendChild`. + +Код: + +```js +function insertAfter(elem, refElem) { + var parent = refElem.parentNode; + var next = refElem.nextSibling; + if (next) { + return parent.insertBefore(elem, next); + } else { + return parent.appendChild(elem); + } +} +``` + +Но код может быть гораздо короче, если использовать фишку со вторым аргументом null метода `insertBefore`: + +```js +function insertAfter(elem, refElem) { + return refElem.parentNode.insertBefore(elem, refElem.nextSibling); +} +``` + +Если нет `nextSibling`, то второй аргумент `insertBefore` становится `null` и тогда `insertBefore(elem,null)` работает как `appendChild`. + +В решении нет проверки на существование `refElem.parentNode`, поскольку вставка после элемента без родителя -- уже ошибка, пусть она возникнет в функции, это нормально. \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/3-insert-after/task.md b/2-ui/1-document/11-modifying-document/3-insert-after/task.md new file mode 100644 index 00000000..30947467 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/3-insert-after/task.md @@ -0,0 +1,27 @@ +# insertAfter + +[importance 5] + +Напишите функцию `insertAfter(elem, refElem)`, которая добавит `elem` после узла `refElem`. + +```html +
      Это
      +
      Элементы
      + + +``` + diff --git a/2-ui/1-document/11-modifying-document/4-remove-children/solution.md b/2-ui/1-document/11-modifying-document/4-remove-children/solution.md new file mode 100644 index 00000000..6a29e291 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/4-remove-children/solution.md @@ -0,0 +1,56 @@ +# Неправильное решение + +Для начала рассмотрим забавный пример того, как делать *не надо*: + +```js +function removeChildren(elem) { + for(var k=0; k + + ЭтоВсеЭлементы DOM + + + +
        +
      1. Вася
      2. +
      3. Петя
      4. +
      5. Маша
      6. +
      7. Даша
      8. +
      + + +``` + +P.S. Проверьте ваше решение в IE8. \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/5-why-aaa/solution.md b/2-ui/1-document/11-modifying-document/5-why-aaa/solution.md new file mode 100644 index 00000000..452540b9 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/5-why-aaa/solution.md @@ -0,0 +1,5 @@ +HTML в задаче некорректен. В этом всё дело. И вопрос легко решится, если открыть отладчик. + +В нём видно, что браузер поместил текст `aaa` *перед* таблицей. Поэтому он и остался в документе. + +Вообще, в стандарте HTML5 описано, как браузеру обрабатывать некорректный HTML, так что такое действие браузера является правильным. \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/5-why-aaa/task.md b/2-ui/1-document/11-modifying-document/5-why-aaa/task.md new file mode 100644 index 00000000..47a76ec2 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/5-why-aaa/task.md @@ -0,0 +1,25 @@ +# Почему остаётся "ааа" ? + +[importance 1] + +Запустите этот пример. Почему вызов `removeChild` не удалил текст `"aaa"`? + +```html + + + aaa + + + +
      Test
      + + +``` + diff --git a/2-ui/1-document/11-modifying-document/6-create-list/solution.md b/2-ui/1-document/11-modifying-document/6-create-list/solution.md new file mode 100644 index 00000000..d0235e68 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/6-create-list/solution.md @@ -0,0 +1,6 @@ + +Делайте проверку на `null` в цикле. `prompt` возвращает это значение только если был нажат ESC. + +Контент в `LI` добавляйте с помощью `document.createTextNode`, чтобы правильно работали <, > и т.д. + +[edit src="solution"]Решение в песочнице[/edit] diff --git a/2-ui/1-document/11-modifying-document/6-create-list/solution.view/index.html b/2-ui/1-document/11-modifying-document/6-create-list/solution.view/index.html new file mode 100755 index 00000000..757cf670 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/6-create-list/solution.view/index.html @@ -0,0 +1,27 @@ + + + + + + +

      Создание списка

      + + + + + diff --git a/2-ui/1-document/11-modifying-document/6-create-list/task.md b/2-ui/1-document/11-modifying-document/6-create-list/task.md new file mode 100644 index 00000000..c86fe48d --- /dev/null +++ b/2-ui/1-document/11-modifying-document/6-create-list/task.md @@ -0,0 +1,20 @@ +# Создать список + +[importance 4] + +Напишите интерфейс для создания списка. + +Для каждого пункта: +
        +
      1. Запрашивайте содержимое пункта у пользователя с помощью `prompt`.
      2. +
      3. Создавайте пункт и добавляйте его к `UL`.
      4. +
      5. Процесс прерывается, когда пользователь нажимает ESC.
      6. +
      + +**Все элементы должны создаваться динамически.** + +Если посетитель вводит теги -- в списке они показываются как обычный текст. + +[demo src="solution"] + +P.S. `prompt` возвращает `null`, если пользователь нажал ESC. \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/7-create-object-tree/build-tree-dom.view/index.html b/2-ui/1-document/11-modifying-document/7-create-object-tree/build-tree-dom.view/index.html new file mode 100755 index 00000000..7b920d9f --- /dev/null +++ b/2-ui/1-document/11-modifying-document/7-create-object-tree/build-tree-dom.view/index.html @@ -0,0 +1,67 @@ + + + + + + + +
      + + + + + diff --git a/2-ui/1-document/11-modifying-document/7-create-object-tree/solution.md b/2-ui/1-document/11-modifying-document/7-create-object-tree/solution.md new file mode 100644 index 00000000..ff334e70 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/7-create-object-tree/solution.md @@ -0,0 +1,6 @@ +Решения через рекурсию. + +
        +
      1. [edit src="solution"]Через innerHTML[/edit].
      2. +
      3. [edit src="build-tree-dom"]Через DOM[/edit].
      4. +
      \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/7-create-object-tree/solution.view/index.html b/2-ui/1-document/11-modifying-document/7-create-object-tree/solution.view/index.html new file mode 100755 index 00000000..e4c3501a --- /dev/null +++ b/2-ui/1-document/11-modifying-document/7-create-object-tree/solution.view/index.html @@ -0,0 +1,49 @@ + + + + + + + +
      + + + + diff --git a/2-ui/1-document/11-modifying-document/7-create-object-tree/source.view/index.html b/2-ui/1-document/11-modifying-document/7-create-object-tree/source.view/index.html new file mode 100755 index 00000000..35067bf8 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/7-create-object-tree/source.view/index.html @@ -0,0 +1,64 @@ + + + + + + + +
      + + + + + + + diff --git a/2-ui/1-document/11-modifying-document/7-create-object-tree/task.md b/2-ui/1-document/11-modifying-document/7-create-object-tree/task.md new file mode 100644 index 00000000..78726b96 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/7-create-object-tree/task.md @@ -0,0 +1,52 @@ +# Создайте дерево из объекта + +[importance 5] + +Напишите функцию, которая создаёт вложенный список `UL/LI` (дерево) из объекта. + +Например: + +```js +var data = { + "Рыбы":{ + "Форель":{}, + "Щука":{} + }, + + "Деревья":{ + "Хвойные":{ + "Лиственница":{}, + "Ель":{} + }, + "Цветковые":{ + "Берёза":{}, + "Тополь":{} + } + } +}; +``` + +Синтаксис: + +```js +var container = document.getElementById('container'); +*!* +createTree(container, data); // создаёт +*/!* +``` + +Результат (дерево): + +[iframe border=1 src="solution"] + +Выберите один из двух способов решения этой задачи: +
        +
      1. Создать строку, а затем присвоить через `container.innerHTML`.
      2. +
      3. Создавать узлы через методы DOM.
      4. +
      + +Если получится -- сделайте оба. + +[edit src="source" task/] + +P.S. Желательно, чтобы в дереве не было лишних элементов, в частности -- пустых `
        ` на нижнем уровне. \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/8-calendar-table/solution.md b/2-ui/1-document/11-modifying-document/8-calendar-table/solution.md new file mode 100644 index 00000000..a4e1a7f3 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/8-calendar-table/solution.md @@ -0,0 +1,11 @@ +Для решения задачи сгенерируем таблицу в виде строки: `"...
        "`, а затем присвоим в `innerHTML`. + +Алгоритм: +
          +
        1. Создать объект даты `d = new Date(year, month-1)`. Это первый день месяца `month` (с учетом того, что месяцы в JS начинаются от 0, а не от 1).
        2. +
        3. Ячейки первого ряда пустые от начала и до дня недели `d.getDay()`, с которого начинается месяц. Создадим их.
        4. +
        5. Увеличиваем день в `d` на единицу: `d.setDate(d.getDate()+1)`, и добавляем в календарь очередную ячейку, пока не достигли следующего месяца. При этом последний день недели означает вставку перевода строки "</tr><tr>".
        6. +
        7. При необходимости, если календарь окончился не на воскресенье - добавить пустые `TD` в таблицу, чтобы было все ровно.
        8. +
        + +[edit src="solution"]Открыть полное решение[/edit] diff --git a/2-ui/1-document/11-modifying-document/8-calendar-table/solution.view/index.html b/2-ui/1-document/11-modifying-document/8-calendar-table/solution.view/index.html new file mode 100755 index 00000000..2adc7474 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/8-calendar-table/solution.view/index.html @@ -0,0 +1,80 @@ + + + + + + + + + +
        + + + + + diff --git a/2-ui/1-document/11-modifying-document/8-calendar-table/source.view/index.html b/2-ui/1-document/11-modifying-document/8-calendar-table/source.view/index.html new file mode 100755 index 00000000..38008482 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/8-calendar-table/source.view/index.html @@ -0,0 +1,39 @@ + + + + + + + + + +
        + + + + diff --git a/2-ui/1-document/11-modifying-document/8-calendar-table/task.md b/2-ui/1-document/11-modifying-document/8-calendar-table/task.md new file mode 100644 index 00000000..a3b08b3c --- /dev/null +++ b/2-ui/1-document/11-modifying-document/8-calendar-table/task.md @@ -0,0 +1,19 @@ +# Создать календарь в виде таблицы + +[importance 4] + +Напишите функцию, которая умеет генерировать календарь для заданной пары (месяц, год). + +Календарь должен быть таблицей, где каждый день -- это `TD`. У таблицы должен быть заголовок с названиями дней недели, каждый день -- `TH`. + +Синтаксис: `createCalendar(id, year, month)`. + +Такой вызов должен генерировать текст для календаря месяца `month` в году `year`, а затем помещать его внутрь элемента с указанным `id`. + +Например: `createCalendar("cal", 2012, 9)` сгенерирует в <div id='cal'></div> следующий календарь: + +[iframe height=210 src="solution"] + +P.S. Достаточно сгенерировать календарь, кликабельным его делать не нужно. + +[edit src="source" task/] diff --git a/2-ui/1-document/11-modifying-document/9-clock-setinterval/solution.md b/2-ui/1-document/11-modifying-document/9-clock-setinterval/solution.md new file mode 100644 index 00000000..551e6f04 --- /dev/null +++ b/2-ui/1-document/11-modifying-document/9-clock-setinterval/solution.md @@ -0,0 +1,59 @@ +Для начала, придумаем подходящую HTML/CSS-структуру. + +Здесь каждый компонент времени удобно поместить в соответствующий `SPAN`: + +```html +
        + hh:mm:ss +
        +``` + +Каждый `SPAN` раскрашивается при помощи CSS. + +Жизнь часам будет обеспечивать функция `update`, вызываемая каждую секунду: `setInterval(update, 1000)`. + +```js +var timerId; // таймер, если часы запущены + +function clockStart() { // запустить часы + if (timerId) return; + + timerId = setInterval(update, 1000); + update(); // (*) +} + +function clockStop() { + clearInterval(timerId); + timerId = null; +} +``` + +Обратите внимание, что вызов `update` не только запланирован, но и производится тут же в строке `(*)`. Иначе посетителю пришлось бы ждать до первого выполнения `setInterval`. + +Функция обновления часов: + +```js +function update() { + var clock = document.getElementById('clock'); +*!* + var date = new Date(); // (*) +*/!* + var hours = date.getHours(); + if (hours < 10) hours = '0'+hours; + clock.children[0].innerHTML = hours; + + var minutes = date.getMinutes(); + if (minutes < 10) minutes = '0'+minutes; + clock.children[1].innerHTML = minutes; + + var seconds = date.getSeconds(); + if (seconds < 10) seconds = '0'+seconds; + clock.children[2].innerHTML = seconds; +} +``` + +В строке `(*)` каждый раз мы получаем текущую дату. Мы должны это сделать, несмотря на то, что, казалось бы, могли бы просто увеличивать счетчик каждую секунду. + +На самом деле мы не можем опираться на счетчик для вычисления даты, т.к. `setInterval` не гарантирует точную задержку. Если в другом участке кода будет вызван `alert`, то часы остановятся, как и любые счетчики. + +[edit src="solution"]Полный код решения[/edit] \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/9-clock-setinterval/solution.view/index.html b/2-ui/1-document/11-modifying-document/9-clock-setinterval/solution.view/index.html new file mode 100755 index 00000000..645c176a --- /dev/null +++ b/2-ui/1-document/11-modifying-document/9-clock-setinterval/solution.view/index.html @@ -0,0 +1,59 @@ + + + + + + + +
        + hh:mm:ss +
        + + + + + + + + + + diff --git a/2-ui/1-document/11-modifying-document/9-clock-setinterval/source.view/index.html b/2-ui/1-document/11-modifying-document/9-clock-setinterval/source.view/index.html new file mode 100755 index 00000000..f921b8fb --- /dev/null +++ b/2-ui/1-document/11-modifying-document/9-clock-setinterval/source.view/index.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/2-ui/1-document/11-modifying-document/9-clock-setinterval/task.md b/2-ui/1-document/11-modifying-document/9-clock-setinterval/task.md new file mode 100644 index 00000000..26a17c2f --- /dev/null +++ b/2-ui/1-document/11-modifying-document/9-clock-setinterval/task.md @@ -0,0 +1,9 @@ +# Часики с использованием "setInterval" + +[importance 4] + +Создайте цветные часики как в примере ниже, **используя `setInterval`**: + +[iframe src="solution"] + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/1-document/11-modifying-document/article.md b/2-ui/1-document/11-modifying-document/article.md new file mode 100644 index 00000000..d8c194db --- /dev/null +++ b/2-ui/1-document/11-modifying-document/article.md @@ -0,0 +1,341 @@ +# Добавление и удаление узлов + +Изменение DOM -- ключ к созданию "живых" страниц. + +В этой главе мы рассмотрим, как создавать новые элементы "на лету" и заполнять их данными. + +[cut] + +## Пример: показ сообщения + +В качестве примера рассмотрим добавление сообщения на страницу, чтобы оно было оформленно красивее чем обычный `alert`. + +HTML-код для сообщения (с подключённой библиотекой стилей [bootstrap](http://getbootstrap.com/)): + +```html + + + +
        + Ура! Вы прочитали это важное сообщение. +
        +``` + +## Создание элемента + +Для создания элементов используются следующие методы: + +
        +
        `document.createElement(tag)`
        +
        Создает новый элемент с указанным тегом: + +```js +var div = document.createElement('div'); +``` + +
        +
        `document.createTextNode(text)`
        +
        Создает новый *текстовый* узел с данным текстом: + +```js +var textElem = document.createTextNode('Тут был я'); +``` + +
        + +### Создание сообщения + +В нашем случае мы хотим сделать DOM-элемент `div`, дать ему классы и заполнить текстом: + +```js +var div = document.createElement('div'); +div.className = "alert alert-success"; +div.innerHTML = "Ура! Вы прочитали это важное сообщение."; +``` + +После этого кода у нас есть готовый DOM-элемент. Пока что он присвоен в переменную `div`, но не виден, так как никак не связан со страницей. + +## Добавление элемента: appendChild, insertBefore + +Чтобы DOM-узел был показан на странице, его необходимо вставить в `document`. + +Для этого первым делом нужно решить, куда мы будем его вставлять. Предположим, что мы решили, что вставлять будем в некий элемент `parentElem`, например `var parentElem = document.body`. + +Для вставки внутрь `parentElem` есть следующие методы: + +
        +
        `parentElem.appendChild(elem)`
        +
        Добавляет `elem` в конец дочерних элементов `parentElem`. + +Следующий пример добавляет новый элемент в конец `
          `: + +```html + +
            +
          1. 0
          2. +
          3. 1
          4. +
          5. 2
          6. +
          + + +``` + +
        +
        `parentElem.insertBefore(elem, nextSibling)`
        +
        Вставляет `elem` в список дочерних `parentElem` перед элементом `nextSibling`. + +Следующий код вставляет новый элемент перед вторым `
      • `: + +```html + +
          +
        1. 0
        2. +
        3. 1
        4. +
        5. 2
        6. +
        + +``` + +**Этот же метод используется для вставки в начало элемента.** + +Достаточно указать, что вставлять будем перед первым потомком: + +```js +list.insertBefore(newLi, list.firstChild); +``` + +У человека, который посмотрит на этот код внимательно, наверняка возникнет вопрос: "А что, если `list` вообще пустой, в этом случае ведь `list.firstChild = null`, произойдёт ли вставка?" + +Ответ -- да, произойдёт. + +**Дело в том, что если в качестве `nextSibling` указать `null`, то `insertBefore` сработает как `appendChild`:** + +```js +parentElem.insertBefore(elem, null); +// то же, что и: +parentElem.appendChild(elem) +``` + +
      • +
        + +Все методы вставки возвращают вставленный узел, например `parentElem.appendChild(elem)` возвращает `elem`. + +### Добавление сообщения + +Добавим сообщение в конец ``: + +```html + + + +

        Моя страница

        + + + +``` + +...А теперь -- в начало ``: + +```html + + + +

        Моя страница

        + + + +``` + +## Клонирование узлов: cloneNode + +А как бы мне вставить второе такое же сообщение? + +Конечно, можно сделать функцию для генерации сообщений и поместить туда этот код, но в ряде случаев гораздо эффективнее -- *клонировать* существующий `div`. В частности, если элемент большой, то клонировать его будет гораздо быстрее, чем пересоздавать. + +Вызов `elem.cloneNode(true)` создаст "глубокую" копию элемента -- вместе с атрибутами, включая подэлементы. Если же вызвать с аргумнтом `false`, то он копия будет без подэлементов, но это нужно гораздо реже. + +### Копия сообщения + +Пример со вставкой копии сообщения: + +```html + + + +

        Моя страница

        + + + +``` + +Обратите внимание на последнюю строку, которая вставляет `div2` после `div`: + +```js +div.parentNode.insertBefore( div2, div.nextSibling ); +``` + +
          +
        1. Для вставки нам нужен будущий родитель. Мы, возможно, не знаем, где точно находится `div` (или не хотим зависеть от того, где он), но если нужно вставить рядом с ним, то родителем определённо будет `div.parentNode`.
        2. +
        3. Вставляем в родителя. Мы хотели бы вставить после `div`, но метода `insertAfter` нет, есть только `insertBefore`, поэтому вставляем *перед* его правым соседом `div.nextSibling`.
        4. +
        + + +## Удаление узлов: removeChild + +Для удаления узла есть два метода: + +
        +
        `parentElem.removeChild(elem)`
        +
        Удаляет `elem` из списка детей `parentElem`.
        +
        `parentElem.replaceChild(elem, currentElem)`
        +
        Среди детей `parentElem` заменяет `currentElem` на `elem`.
        +
        + +Оба этих метода возвращают удаленный узел. Если нужно, его можно вставить в другое место DOM тут же или в будущем. + +[smart] +Если вы хотите *переместить* элемент на новое место -- не нужно его удалять со старого. + +**Все методы вставки автоматически удаляют вставляемый элемент со старого места.** + +Конечно же, это очень удобно. + +Например, поменяем элементы местами: + +```html + +
        Первый
        +
        Второй
        + +``` + +[/smart] + + +### Удаление сообщения + +Сделаем так, что через секунду сообщение пропадёт: + +```html + + + +

        Сообщение пропадёт через секунду

        + + + +``` + +## Пример использования текстовых узлов + +При работе с сообщением мы использовали только узлы-элементы и `innerHTML`. + +Но и текстовые узлы тоже имеют интересную область применения! Было бы несправедливо их обойти. + +У них есть две особенности. Начнем с небольшого вопроса. + + +Ответили на вопрос выше? Даже если нет, то, поглядев в решение, вы легко увидите разницу. + +Итак, отличий два: +
          +
        1. При создании текстового узла `createTextNode('...')` любые специальные символы и теги в строке будут интерпретированы *как текст*. А `innerHTML` обработал бы их *как HTML*.
        2. +
        3. Во всех современных браузерах создание и вставка текстового узла работает гораздо быстрее, чем присвоение HTML.
        4. +
        + +## Итого + +Методы для создания узлов: + +
          +
        • `document.createElement(tag)` -- создает элемент
        • +
        • `document.createTextNode(value)` -- создает текстовый узел
        • +
        • `elem.cloneNode(deep)` -- клонирует элемент, если `deep == true`, то со всеми потомками, если `false` -- без потомков.
        • +
        + +Вставка и удаление узлов: +
          +
        • `parent.appendChild(elem)`
        • +
        • `parent.insertBefore(elem, nextSibling)`
        • +
        • `parent.removeChild(elem)`
        • +
        • `parent.replaceChild(elem, currentElem)`
        • +
        + +Все эти методы возвращают `elem`. + +Запомнить порядок аргументов очень просто: **новый(вставляемый) элемент -- всегда первый.** + +Методы для изменения DOM также описаны в спецификации DOM Level 1. + + + + + + + diff --git a/2-ui/1-document/12-multi-insert/1-append-to-list/solution.md b/2-ui/1-document/12-multi-insert/1-append-to-list/solution.md new file mode 100644 index 00000000..f8202293 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/1-append-to-list/solution.md @@ -0,0 +1,8 @@ +Решение: + +```js +var ul = document.body.children[0]; + +ul.insertAdjacentHTML("beforeEnd", "
      • 3
      • 4
      • 5
      • "); +``` + diff --git a/2-ui/1-document/12-multi-insert/1-append-to-list/task.md b/2-ui/1-document/12-multi-insert/1-append-to-list/task.md new file mode 100644 index 00000000..88c334f6 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/1-append-to-list/task.md @@ -0,0 +1,15 @@ +# Вставьте элементы в конец списка + +[importance 5] + +Напишите код для вставки текста `html` в конец списка `ul` с использованием метода `insertAdjacentHTML`. Такая вставка, в отличие от присвоения `innerHTML+=`, не будет перезаписывать текущее содержимое. + +Добавьте к списку ниже элементы `
      • 3
      • 4
      • 5
      • `: + +```html +
          +
        • 1
        • +
        • 2
        • +
        +``` + diff --git a/2-ui/1-document/12-multi-insert/2-insertadjacenthtml-documentfragment/solution.md b/2-ui/1-document/12-multi-insert/2-insertadjacenthtml-documentfragment/solution.md new file mode 100644 index 00000000..9de9c2f5 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/2-insertadjacenthtml-documentfragment/solution.md @@ -0,0 +1,49 @@ +# Подсказки + +
          +
        • Проверить поддержку `insertAdjacentHTML` можно так: + +```js +if (elem.insertAdjacentHTML) { ... } +``` + +
        • +
        • Если этот метод не поддерживается, то сделайте временный элемент, через `innerHTML` поставьте туда `html`, а затем переместите содержимое в `DocumentFragment`. Последнее действие -- вставка в документ.
        • +
        + +# Решение + +```html + +
          +
        • 1
        • +
        • 2
        • +
        • 5
        • +
        + + +``` + diff --git a/2-ui/1-document/12-multi-insert/2-insertadjacenthtml-documentfragment/task.md b/2-ui/1-document/12-multi-insert/2-insertadjacenthtml-documentfragment/task.md new file mode 100644 index 00000000..ad9d8207 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/2-insertadjacenthtml-documentfragment/task.md @@ -0,0 +1,34 @@ +# Вставка insertAdjacentHTML/DocumentFragment + +[importance 4] + +Напишите кроссбраузерную функцию `insertBefore(elem, html)`, которая: + +
          +
        • Вставляет HTML-строку `html` перед элементом `elem`, используя `insertAdjacentHTML`,
        • +
        • Если он не поддерживается (старый Firefox) -- то через `DocumentFragment`.
        • +
        + +В обоих случаях должна быть лишь одна операция с DOM документа. + +Следующий код должен вставить два пропущенных элемента списка `
      • 3
      • 4
      • `: + +```html +
          +
        • 1
        • +
        • 2
        • +
        • 5
        • +
        + + +``` + diff --git a/2-ui/1-document/12-multi-insert/3-sort-table-performance/solution.md b/2-ui/1-document/12-multi-insert/3-sort-table-performance/solution.md new file mode 100644 index 00000000..f5201d1c --- /dev/null +++ b/2-ui/1-document/12-multi-insert/3-sort-table-performance/solution.md @@ -0,0 +1,11 @@ +Для сортировки нам поможет функция `sort` массива. + +Общая идея лежит на поверхности: сделать массив из строк и отсортировать его. Тонкости кроются в деталях. + +В ифрейме ниже загружен документ, описывающий и реализующий разные алгоритмы. Обратите внимание: разница в производительности может достигать нескольких раз! + +[iframe height=800 border=1 src="solution" link edit] + +P.S. Создавать `DocumentFragment` здесь ни к чему. Можно вытащить из документа `TBODY` и иметь дело с ним в отрыве от DOM (алгоритм 4). + +P.P.S. Если нужно сделать много узлов, то обычно `innerHTML` работает быстрее, чем генерация элементов через DOM-вызовы. Но в данном случае мы не создаём элементы, а сортируем и перевставляем готовые, так что результаты могут отличаться. diff --git a/2-ui/1-document/12-multi-insert/3-sort-table-performance/solution.view/index.html b/2-ui/1-document/12-multi-insert/3-sort-table-performance/solution.view/index.html new file mode 100755 index 00000000..ee89ce92 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/3-sort-table-performance/solution.view/index.html @@ -0,0 +1,173 @@ + + + + +
        + Алгоритм 1. +
          +
        1. Все TR удалить из таблицы, при этом собрав их в JavaScript-массив.
        2. +
        3. Отсортировать этот массив, используя свою функцию в sort(...) для сравнения TR
        4. +
        5. Добавить TR из массива в таблицу в нужном порядке
        6. +
        + +
        + +
        + Алгоритм 2. +
          +
        1. Скопировать TR в JavaScript-массив.
        2. +
        3. Отсортировать этот массив, используя свою функцию в sort(...) для сравнения TR
        4. +
        5. Добавить TR из массива в таблицу в нужном порядке. При добавлении каждый TR сам удалится с предыдущего места.
        6. +
        + +
        + + +
        + Алгоритм 3. +
          +
        1. Создать массив из объектов вида {elem: ссылка на TR, value: содержимое TR}.
        2. +
        3. Отсортировать массив по value. Функция сравнения во время сортировки теперь будет обращаться не к innerHTML, а к свойству объекта, это быстрее. Сортировка может потребовать многократных сравнений одного и того же элемента, отсюда выигрыш.
        4. +
        5. Добавить TR в таблицу в нужном порядке (автоудалятся с предыдущего места).
        6. +
        + +
        + + +
        + Алгоритм 4. +
          +
        1. Выполнить алгоритм 3, но перед этим удалить таблицу из документа, а после - вставить обратно.
        2. +
        + +
        + +
        + Алгоритм 5. +
          +
        1. Замерить время генерации таблицы (создаётся строка и пишется в innerHTML).
        2. +
        + +
        + +
        + + + + + + +

        Содержимое документа для придания "реалистичности"

        + +
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        01234567890123456789012345678901234567890123456789
        + + + + diff --git a/2-ui/1-document/12-multi-insert/3-sort-table-performance/task.md b/2-ui/1-document/12-multi-insert/3-sort-table-performance/task.md new file mode 100644 index 00000000..0a1dcbf1 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/3-sort-table-performance/task.md @@ -0,0 +1,47 @@ +# Отсортировать таблицу + +[importance 5] + +Есть таблица: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        ИмяФамилияОтчествоВозраст
        ВасяПетровАлександрович10
        ПетяИвановПетрович15
        ВладимирЛенинИльич9
        ............
        + +Строк в таблице много: может быть 20, 50, 100.. Есть и другие элементы в документе. + +Как бы вы предложили отсортировать содержимое таблицы по полю `Возраст`? Обдумайте алгоритм, реализуйте его. + +Как сделать, чтобы сортировка работала как можно быстрее? А если в таблице 10000 строк (бывает и такое)? + +P.S. Может ли здесь помочь `DocumentFragment`? +P.P.S. Если предположить, что у нас заранее есть массив данных для таблицы в JavaScript -- что быстрее: отсортировать эту таблицу или сгенерировать новую? \ No newline at end of file diff --git a/2-ui/1-document/12-multi-insert/article.md b/2-ui/1-document/12-multi-insert/article.md new file mode 100644 index 00000000..71b2e967 --- /dev/null +++ b/2-ui/1-document/12-multi-insert/article.md @@ -0,0 +1,312 @@ +# Мультивставка: insertAdjacentHTML и DocumentFragment + +Обычные методы вставки работают с одним узлом. Но есть и способы вставлять множество узлов одновременно. +[cut] +## Оптимизация вставки в документ + +Рассмотрим задачу: сгенерировать список `UL/LI`. + +Есть две возможных последовательности: + +
          +
        1. Сначала вставить `UL` в документ, а потом добавить к нему `LI`: + +```js +var ul = document.createElement('ul'); +document.body.appendChild(ul); // сначала в документ +for(...) ul.appendChild(li); // потом узлы +``` + +
        2. +
        3. Полностью создать список "вне DOM", а потом -- вставить в документ: + +```js +var ul = document.createElement('ul'); +for(...) ul.appendChild(li); // сначала вставить узлы +document.body.appendChild(ul); // затем в документ +``` + +
        4. +
        + +Как ни странно, между этими последовательностями есть разница. В большинстве браузеров, второй вариант -- быстрее. + +Почему же? Иногда говорят: "потому что браузер перерисовывает каждый раз при добавлении элемента". Это не так. **Дело вовсе не в перерисовке**. + +Браузер достаточно "умён", чтобы ничего не перерисовывать понапрасну. В большинстве случаев процессы перерисовки и сопутствующие вычисления будут отложены до окончания работы скрипта, и на тот момент уже совершенно без разницы, в какой последовательности были изменены узлы. + +**Тем не менее, при вставке узла происходят разные внутренние события и обновления внутренних структур данных, скрытые от наших глаз.** + +Что именно происходит -- зависит от конкретной, внутренней браузерной реализации DOM, но это отнимает время. Конечно, браузеры развиваются и стараются свести лишние действия к минимуму. + +### Бенчмарк [#insert-bench-tbody] + +Чтобы легко проверить текущее состояние дел -- вот два бенчмарка. + +Оба они создают таблицу 20x20, наполняя TBODY элементами TR/TD. + +При этом первый вставляет все в документ тут же, второй -- задерживает вставку TBODY в документ до конца процесса. + +Кликните, чтобы запустить. + + + +
        + +```js +//+ hide="открыть код" src="insert-bench.js" +``` + +## Добавление множества узлов + +Продолжим работать со вставкой узлов. + +Рассмотрим случай, когда в документе *уже есть* большой список `UL`. И тут понадобилось срочно добавить еще 20 элементов `LI`. + +Как это сделать? + +Если новые элементы пришли в виде строки, то можно попробовать добавить их так: + +```js +ul.innerHTML += "
      • 1
      • 2
      • ..."; +``` + +Но операция `+=` с `innerHTML` не работает с DOM. Она не прибавляет, а заменяет всё содержимое списка на дополненную строку. Это не только медленно, но все внешние ресурсы (картинки) будут загружены заново! Так лучше не делать. + +А если нужно вставить в середину списка? Здесь `innerHTML` вообще не поможет. + +Можно, конечно, вставить строку во временный DOM-элемент и перенести оттуда элементы, но есть и гораздо лучший вариант: метод `insertAdjacentHTML`! + +## insertAdjacent* + +Метод [insertAdjacentHTML](https://developer.mozilla.org/en/DOM/element.insertAdjacentHTML) позволяет вставлять произвольный HTML в любое место документа, в том числе *и между узлами*! + +Он поддерживается всеми браузерами, кроме Firefox меньше версии 8, ну а там его можно эмулировать. + +Синтаксис: + +```js +elem.insertAdjacentHTML(where, html); +``` + +
        +
        `html`
        +
        Строка HTML, которую нужно вставить
        +
        `where`
        +
        Куда по отношению к `elem` вставлять строку. Всего четыре варианта: +
          +
        1. `beforeBegin` -- перед `elem`.
        2. +
        3. `afterBegin` -- внутрь `elem`, в самое начало.
        4. +
        5. `beforeEnd` -- внутрь `elem`, в конец.
        6. +
        7. `afterEnd` -- после `elem`.
        8. +
        +
        + + + +Например, вставим пропущенные элементы списка *перед* `
      • 5
      • `: + +```html + +
          +
        • 1
        • +
        • 2
        • +
        • 5
        • +
        + + +``` + +Единственный недостаток этого метода -- он не работает в Firefox до версии 8. Но его можно легко добавить, используя следующий JavaScript: [insertAdjacentFF.js](/files/tutorial/browser/dom/insertAdjacentFF.js). + +У этого метода есть "близнецы-братья", которые поддерживаются везде, кроме FF, но в него они добавляются этим же скриптом: + +
          +
        • [elem.insertAdjacentElement(where, newElem)](http://help.dottoro.com/ljbreokf.php) -- вставляет в произвольное место не строку HTML, а элемент `newElem`.
        • +
        • [elem.insertAdjacentText(where, text)](http://help.dottoro.com/ljrsluxu.php) -- создаёт текстовый узел из строки `text` и вставляет его в указанное место относительно `elem`.
        • +
        + +Синтаксис этих методов, за исключением последнего параметра, полностью совпадает с `insertAdjacentHTML`. Вместе они образуют "универсальный швейцарский нож" для вставки чего угодно куда угодно. + +## DocumentFragment + +[warn header="Важно для старых браузеров"] +Оптимизация, о которой здесь идёт речь, важна в первую очередь для старых браузеров, включая IE9-. В современных браузерах эффект от нее, как правило, не превышает 20%, а иногда может быть и отрицательным. +[/warn] + +До этого мы говорили о вставке строки в DOM. А что делать в случае, когда надо в существующий `UL` вставить много *DOM-элементов*? + +Можно вставлять их один за другим, вызовом `insertBefore/appendChild`, но при этом получится много операций с большим живым документом. + +**Вставить пачку узлов единовременно поможет `DocumentFragment`. Это особенный *кросс-браузерный* DOM-объект, который похож на обычный DOM-узел, но им не является.** + +Синтаксис для его создания: + +```js +var fragment = document.createDocumentFragment(); +``` + +В него можно добавлять другие узлы. + +```js +fragment.appendChild(node); +``` + +Его можно клонировать: + +```js +fragment.cloneNode(true); // клонирование с подэлементами +``` + +**У `DocumentFragment` нет обычных свойств DOM-узлов, таких как `innerHTML`, `tagName` и т.п. Это не узел.** + +**"Фишка" заключается в том, что когда `DocumentFragment` вставляется в DOM -- то он исчезает, а вместо него вставляются его дети. Это свойство является уникальной особенностью `DocumentFragment`.** + +Например, если добавить в него много `LI`, и потом `appendChild` к `UL`, то фрагмент растворится, и в DOM вставятся именно `LI`, причём в том же порядке, в котором были во фрагменте. + +Псевдокод: + +```js +// хотим вставить в список UL много LI + +// делаем вспомогательный DocumentFragment +var fragment = document.createDocumentFragment(); + +for (цикл по li) { + fragment.appendChild(list[i]); // вставить каждый LI в DocumentFragment +} + +ul.appendChild(fragment); // вместо фрагмента вставятся элементы списка +``` + +В современных браузерах эффект от такой оптимизации может быть различным. Чтобы понять текущее положение вещей, попробуйте в различных браузерах следующий небольшой бенчмарк. + +При нажатии на кнопки ниже в список добавляются `100` элементов. +[pre] +
        + + +
        + [/pre] +
          + + + + +```js +//+ hide="открыть код" src="documentfragment-bench.js" +``` + +## Итого + +
            +
          • **Манипуляции, меняющие структуру DOM (вставка, удаление элементов), как правило, быстрее с отдельным маленьким узлом, чем с большим DOM, который находится в документе.** + +Конкретная разница зависит от внутренней реализации DOM в браузере.
          • +
          • **Семейство методов `elem.insertAdjacentHTML(where, html)`, `insertAdjacentElement`, `insertAdjacentText` позволяет вставлять HTML/элемент/текст в произвольное место документа.** + +Метод `insertAdjacentHTML` не поддерживается в Firefox до версии 8, остальные два метода не поддерживаются в Firefox, на момент написания текста, вообще, но есть небольшой скрипт [insertAdjacentFF.js](/files/tutorial/browser/dom/insertAdjacentFF.js), который добавляет их. Конечно, он нужен только для Firefox. +
          • +
          • **`DocumentFragment` позволяет минимизировать количество вставок в большой живой DOM. Эта оптимизация особо эффективна в старых браузерах, в новых эффект от неё меньше.** + +Элементы сначала вставляются в него, а потом -- он вставляется в DOM. При вставке `DocumentFragment` "растворяется", и вместо него вставляются содержащиеся в нём узлы. + +`DocumentFragment`, в отличие от `insertAdjacent*`, работает с коллекцией DOM-узлов. +
          • +
          + + +[head] + + + +[/head] \ No newline at end of file diff --git a/2-ui/1-document/12-multi-insert/documentfragment-bench.js b/2-ui/1-document/12-multi-insert/documentfragment-bench.js new file mode 100755 index 00000000..afa0134b --- /dev/null +++ b/2-ui/1-document/12-multi-insert/documentfragment-bench.js @@ -0,0 +1,45 @@ +var DocumentFragmentTest = new function() { + var benchList = document.getElementById('bench-list'); + + var items = []; + for(var i=0; i<100; i++) { + var li = document.createElement('li'); + li.innerHTML = i; + items.push(li); + } + + this.insertPlain = new function() { + + this.setup = function() { + while(benchList.firstChild) { + benchList.removeChild(benchList.firstChild); + } + } + + this.work = function() { + for(var i=0; i + +1 + +3 + +``` + +**Нет никаких ограничений на содержимое `document.write`**. + +Строка просто пишется в HTML-документ без проверки структуры тегов, как будто она всегда там была. + +Например: + +```html + + + + + + Текст внутри TD. + + +
          +``` + +Также существует метод `document.writeln(str)` -- не менее древний, который добавляет после `str` символ перевода строки `"\n"`. + +## Только до конца загрузки + +Во время загрузки браузер читает документ и тут же строит из него DOM, по мере получения информации достраивая новые и новые узлы, и тут же отображая их. Этот процесс идет непрерывным потоком. Вы наверняка видели это, когда заходили на сайты в качестве посетителя -- браузер зачастую отображает неполный документ, добавляя его новыми узлами по мере их получения. + +**Методы `document.write` и `document.writeln` пишут напрямую в текст документа, до того как браузер построит из него DOM, поэтому они могут записать в документ все, что угодно, любые стили и незакрытые теги.** + +Браузер учтет их при построении DOM, точно так же, как учитывает очередную порцию HTML-текста. + +Технически, вызвать `document.write` можно в любое время, однако, когда HTML загрузился, и браузер полностью построил DOM, документ становится *"закрытым"*. Попытка дописать что-то в закрытый документ открывает его заново. При этом все текущее содержимое удаляется. + +Текущая страница, скорее всего, уже загрузилась, поэтому если вы нажмёте на эту кнопку -- её содержимое удалится: + + + +Из-за этой особенности `document.write` для загруженных документов не используют. + +[warn header="XHTML и `document.write`"] +В некоторых современных браузерах при получении страницы с заголовком `Content-Type: text/xml` или `Content-Type: text/xhtml+xml` включается "XML-режим" чтения документа. Метод `document.write` при этом не работает. + +Это одна из причин, по которой XML-режим обычно не используют. +[/warn] + + +## Преимущества перед innerHTML + +Метод `document.write` -- динозавр, он существовал десятки миллионов лет назад. С тех пор, как появился и стал стандартным метод `innerHTML`, нужда в нём возникает редко, но некоторые преимущества, всё же, есть. + +
            +
          • **Метод `document.write` работает быстрее, фактически это самый быстрый способ добавить на страницу текст, сгенерированный скриптом.** + +Это естественно, ведь он не модифицирует существующий DOM, а пишет в текст страницы до его генерации.
          • +
          • **Метод `document.write` вставляет любой текст на страницу "как есть", в то время как `innerHTML` может вписать лишь валидный HTML.** (при попытке подсунуть невалидный -- браузер скорректирует его).
          • +
          + +Эти преимущества являются скорее средством оптимизации, которое нужно использовать именно там, где подобная оптимизация нужна или уместна. + +Однако, `document.write` по своей природе уникален: он добавляет текст "в текущее место документа", без всяких хитроумных DOM. Поэтому он бывает просто-напросто удобен, из-за чего его нередко используют не по назначению. + +## Антипример: реклама + +Например, `document.write` используют для вставки рекламных скриптов и различных счетчиков, когда URL скрипта необходимо генерировать динамически, добавляя в него параметры из JavaScript, например: + +```html + +``` + +[smart] +Закрывающий тег </script> в строке разделён, чтобы браузер не увидел `` и не посчитал его концом скрипта. + +Также используют запись: + +```js +document.write('`: обратный слеш `\` обычно используется для вставки спецсимволов типа `\n`, а если такго спецсимвола нет, в данном случае `\/` не является спецсимволом, то он просто исчезает. Так что получается такой альтернативный способ безопасно вставить строку ``. +[/smart] + +Сервер, получив запрос с такими параметрами, обрабатывает его и, исходя учитывая переданную информацию, генерирует текст скрипта, в котором обычно есть какой-то другой `document.write`, рисующий на этом месте баннер. + +**Проблема здесь в том, что загрузка такого скрипта блокирует отрисовку всей страницы.** + +То есть, дело даже не в самом `document.write`, а в том, что в страницу вставляется сторонний скрипт, а браузер устроен так, что пока он его не загрузит и не выполнит -- он не будет дальше строить DOM и показывать документ. + +Представим на минуту, что сервер `ads.com`, с которого грузится скрипт, работает медленно или вообще завис -- зависнет и наша страница. + +Что делать? + +В современных браузерах у скриптов есть атрибуты `async` и `defer`, которые разрешают браузеру продолжать обработку страницы, но применить их здесь нельзя, так как рекламный скрипт захочет вызвать `document.write` именно на этом месте, и браузер не должен двигаться вперёд по документу. + +Альтернатива -- использовать другие техники вставки рекламы и счётчиков. Примеры вы можете увидеть в коде Google Analytics, Яндекс.Метрики и других. + +Если это невозможно -- применяют всякие хитрые оптимизации, например заменяют метод `document.write` своей функцией, и она уже разбирается со скриптами и баннерами. + +## Итого + +Метод `document.write` (или `writeln`) пишет текст прямо в HTML, как будто он там всегда был. + +
            +
          • **Этот метод редко используется, так как работает только из скриптов, выполняемых в процессе загрузки страницы.** + +Запуск после загрузки приведёт к очистке документа.
          • +
          • **Метод `document.write` очень быстр.** + +В отличие от установки `innerHTML` и DOM-методов, он не изменяет существующий документ, а работает на стадии текста, до того как DOM-структура сформирована.
          • +
          • **Иногда `document.write` используют для добавления скриптов с динамическим URL.** + +Рекомендуется избегать этого, так как браузер остановится на месте добавления скрипта и будет ждать его загрузки. Если скрипт будет тормозить, то и страница -- тоже. + +Поэтому желательно подключать внешние скрипты, используя вставку скрипта через DOM. +
          • +
          diff --git a/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/solution.md b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/solution.md new file mode 100644 index 00000000..c78f0136 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/solution.md @@ -0,0 +1,43 @@ +Есть два варианта. + +
            +
          1. Можно использовать свойство `elem.style.cssText` и присвоить стиль в текстовом виде. При этом все присвоенные ранее свойства `elem.style` будут удалены.
          2. +
          3. Можно назначить подсвойства `elem.style` одно за другим. Этот способ более безопасен, т.к. меняет только явно присваемые свойства.
          4. +
          + +Мы выберем второй путь. + +[edit src="solution"]Открыть решение[/edit] + +**Описание CSS-свойств:** + +```css +.button { + -moz-border-radius: 8px; + -webkit-border-radius: 8px; + border-radius: 8px; + border: 2px groove green; + display: block; + height: 30px; + line-height: 30px; + width: 100px; + text-decoration: none; + text-align: center; + color: red; + font-weight: bold; +} +``` + +
          +
          `*-border-radius`
          +
          Добавляет скругленные углы. Свойство присваивается в вариантах для Firefox `-moz-...`, Chrome/Safari `-webkit-...` и стандартное CSS3-свойство для тех, кто его поддерживает (Opera).
          +
          `display`
          +
          По умолчанию, у `A` это свойство имеет значение `display: inline`.
          +
          `height`, `line-height`
          +
          Устанавливает высоту и делает текст вертикально центрированным путем установки `line-height` в значение, равное высоте. Такой способ центрирования текста работает, если он состоит из одной строки.
          +
          `text-align`
          +
          Центрирует текст горизонтально.
          +
          `color`, `font-weight`
          +
          Делает текст красным и жирным.
          +
          + diff --git a/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/solution.view/index.html b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/solution.view/index.html new file mode 100755 index 00000000..906df785 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/solution.view/index.html @@ -0,0 +1,37 @@ + + + + + +
          + Кнопка: + +
          + + + + + + diff --git a/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/source.view/index.html b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/source.view/index.html new file mode 100755 index 00000000..be378ff0 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/source.view/index.html @@ -0,0 +1,17 @@ + + + + + +
          + Кнопка: + +
          + + + + + + diff --git a/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/task.md b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/task.md new file mode 100644 index 00000000..f77417bc --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/1-round-button-javascript/task.md @@ -0,0 +1,33 @@ +# Скругленая кнопка со стилями из JavaScript + +[importance 3] + +Создайте кнопку в виде элемента `` с заданным стилем, используя JavaScript. + +В примере ниже такая кнопка создана при помощи HTML/CSS. В вашем решении кнопка должна создаваться, настраиваться и добавляться в документ при помощи *только JavaScript*, без тегов ` + +Нажми меня +``` + +**Проверьте себя: вспомните, что означает каждое свойство. В чём состоит эффект его появления здесь?** + +[edit src="source" task/] diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.md b/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.md new file mode 100644 index 00000000..099f5e81 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.view/index.css b/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.view/index.css new file mode 100755 index 00000000..e04a30b3 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.view/index.css @@ -0,0 +1,15 @@ +.notification { + position: fixed; + z-index: 1000; + padding: 5px; + border: 1px solid black; + font: normal 20px Georgia; + background: white; + text-align: center; +} + +.welcome { + background: red; + color: yellow; +} + diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.view/index.html b/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.view/index.html new file mode 100755 index 00000000..94dc16d3 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/2-create-notification/solution.view/index.html @@ -0,0 +1,61 @@ + + + + + + + + +

          Уведомление

          + +

          + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum aspernatur quam ex eaque inventore quod voluptatem adipisci omnis nemo nulla fugit iste numquam ducimus cumque minima porro ea quidem maxime necessitatibus beatae labore soluta voluptatum magnam consequatur sit laboriosam velit excepturi laborum sequi eos placeat et quia deleniti? Corrupti velit impedit autem et obcaecati fuga debitis nemo ratione iste veniam amet dicta hic ipsam unde cupiditate incidunt aut iure ipsum officiis soluta temporibus. Tempore dicta ullam delectus numquam consectetur quisquam explicabo culpa excepturi placeat quo sequi molestias reprehenderit hic at nemo cumque voluptates quidem repellendus maiores unde earum molestiae ad. +

          + + + + + + diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/source.view/index.css b/2-ui/1-document/14-styles-and-classes/2-create-notification/source.view/index.css new file mode 100755 index 00000000..e04a30b3 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/2-create-notification/source.view/index.css @@ -0,0 +1,15 @@ +.notification { + position: fixed; + z-index: 1000; + padding: 5px; + border: 1px solid black; + font: normal 20px Georgia; + background: white; + text-align: center; +} + +.welcome { + background: red; + color: yellow; +} + diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/source.view/index.html b/2-ui/1-document/14-styles-and-classes/2-create-notification/source.view/index.html new file mode 100755 index 00000000..752e22cc --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/2-create-notification/source.view/index.html @@ -0,0 +1,46 @@ + + + + + + + + +

          Уведомление

          + +

          + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum aspernatur quam ex eaque inventore quod voluptatem adipisci omnis nemo nulla fugit iste numquam ducimus cumque minima porro ea quidem maxime necessitatibus beatae labore soluta voluptatum magnam consequatur sit laboriosam velit excepturi laborum sequi eos placeat et quia deleniti? Corrupti velit impedit autem et obcaecati fuga debitis nemo ratione iste veniam amet dicta hic ipsam unde cupiditate incidunt aut iure ipsum officiis soluta temporibus. Tempore dicta ullam delectus numquam consectetur quisquam explicabo culpa excepturi placeat quo sequi molestias reprehenderit hic at nemo cumque voluptates quidem repellendus maiores unde earum molestiae ad. +

          + +

          В CSS есть готовый класс notification, который можно ставить уведомлению.

          + + + + + + diff --git a/2-ui/1-document/14-styles-and-classes/2-create-notification/task.md b/2-ui/1-document/14-styles-and-classes/2-create-notification/task.md new file mode 100644 index 00000000..b2bedfb3 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/2-create-notification/task.md @@ -0,0 +1,41 @@ +# Создать уведомление + +[importance 5] + +Напишите функцию `showNotification(options)`, которая показывает уведомление, пропадающее через 1.5 сек. + +Описание функции: + +```js +/** + * Показывает уведомление, пропадающее через 1.5 сек + * + * @param options.top {number} вертикальный отступ, в px + * @param options.right {number} правый отступ, в px + * @param options.cssText {string} строка стиля + * @param options.className {string} CSS-класс + * @param options.html {string} HTML-текст для показа + */ +function showNotification(options) { + // ваш код +} +``` + +Пример использования: + +```js +// покажет элемент с текстом "Привет" и классом welcome справа-сверху окна +showNotification({ + top: 10, + right: 10, + html: "Привет", + className: "welcome" +}); +``` + +[demo src="solution"] + +Элемент уведомления должен иметь CSS-класс `notification`, к которому добавляется класс из `options.className`, если есть. Исходный документ содержит готовые стили. + +[edit src="source" task/] + diff --git a/2-ui/1-document/14-styles-and-classes/article.md b/2-ui/1-document/14-styles-and-classes/article.md new file mode 100644 index 00000000..371e5962 --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/article.md @@ -0,0 +1,343 @@ +# Стили, getComputedStyle + +Эта глава -- о свойствах стиля, получении о них информации и изменении при помощи JavaScript. + +Перед прочтением убедитесь, что хорошо знакомы с блочной моделью CSS и понимаете, что такое `padding`, `margin`, `border`. + +[cut] + +## Объект стилей style + +Объект `element.style` дает доступ к стилю элемента на чтение и запись. + +С его помощью можно изменять большинство CSS-свойств, например `element.style.width='100px'` работает так, как будто у элемента в атрибуте прописано `style="width:100px"`. + +[warn header="Единицы измерения обязательны в `style`"] +Об этом иногда забывают, но в `style` так же, как и в CSS, нужно указывать единицы измерения, например `px`. + +Ни в коем случае не просто `elem.style.width = 100` -- работать не будет. +[/warn] + +**Для свойств, названия которых состоят из нескольких слов, используется вотТакаяЗапись:** + +```js +background-color => elem.style.backgroundColor +z-index => elem.style.zIndex +border-left-width => elem.style.borderLeftWidth +``` + +Пример использования `style`: + +```js +//+ run +document.body.style.backgroundColor = prompt('background color?', 'green'); +``` + +[warn header="`style.cssFloat` вместо `style.float`"] +Исключением является свойство `float`. В старом стандарте JavaScript слово `"float"` было зарезервировано и недоступно для использования в качестве свойства объекта. Поэтому используется `element.style.cssFloat`. +[/warn] + +[smart header="Свойства с префиксами"] +Специфические свойства браузеров, типа `-moz-border-radius`, `-webkit-border-radius`, записываются следующим способом: + +```js +button.style.MozBorderRadius = '5px'; +button.style.WebkitBorderRadius = '5px'; +``` + +То есть, каждый дефис дает большую букву. В этом смысле преобразование -- такое же, как для обычных свойств. +[/smart] + +**Чтобы сбросить поставленный стиль, присваивают в `style` пустую строку: `elem.style.width=""`.** + +При сбросе свойства `style` стиль будет взят из CSS. + +Например, для того, чтобы спрятать элемент, можно присвоить: `elem.style.display = "none"`. + +А вот чтобы показать его обратно -- не обязательно явно указывать другой `elem.style.display = "block"`! Можно просто снять поставленный стиль: `elem.style.display = ""`. + +```js +//+ run +// если запустить этот код, то "мигнёт" +document.body.style.display = "none"; + +setTimeout(function() { + document.body.style.display = ""; +}, 1000); +``` + +**Стиль в `style` находится в формате браузера, а не в том, в котором его присвоили.** + +Например: + +```html + + + + +``` + +Обратите внимание на то, как браузер "распаковал" свойство `style.margin`, предоставив для чтения `style.marginTop`. То же самое произойдет и для `border`, `background` и т.д. + + +[warn header="Свойство `style` мы используем лишь там, где не работают классы"] +В большинстве случаев внешний вид элементов задаётся классами. А JavaScript добавляет или удаляет их. Такой код красив и гибок, дизайн можно легко изменять. + +Свойство `style` нужно использовать лишь там, где классы не подходят, например если точное значение цвета/отступа/высоты... Вычисляется в JavaScript. +[/warn] + + +## Строка стилей style.cssText + +Свойство `style` является специальным объектом, ему нельзя присваивать строку. + +Запись `div.style="color:blue"` работать не будет. Но как же, всё-таки, поставить свойство стиля, если хочется задать его строкой? + +Можно попробовать использовать атрибут: `elem.setAttribute("style", ...)`, но самым правильным и, главное, кросс-браузерным (с учётом старых IE) решением такой задачи будет использование свойства `style.cssText`. + +**Свойство `style.cssText` позволяет поставить стиль целиком в виде строки.** + +Например: + +```html + +
          Button
          + + +``` + +Браузер разбирает строку `style.cssText` и применяет известные ему свойства. Нет никаких ограничений на запись несуществующих свойств, но если указать свойство `blabla` -- большинство браузеров его просто проигнорируют. + +**При установке `style.cssText` все существующие свойства `style` перезаписываются.** + +Поэтому, по возможности, во избежание конфликта, присваивают более конкретные подсвойства `style`: `style.color`, `style.width` и т.п, а `style.cssText` используют для более короткой записи, когда это заведомо безопасно. + +## Чтение стиля из style + +Записать в стиль очень просто. А как прочитать? + +Например, мы хотим узнать размер, отступы элемента, его цвет... Как это сделать? + +**Свойство `style` содержит лишь тот стиль, который указан в атрибуте элемента, без учёта каскада CSS.** + +Вот так `style` уже ничего не увидит: + +```html + + + + + + + Красный текст + + +``` + +## Стиль из getComputedStyle + +Итак, свойство `style` дает доступ только к той информации, которая хранится в `elem.style`. + +Он не скажет ничего об отступе, если он появился в результате наложения CSS или встроенных стилей браузера: + +А если мы хотим, например, сделать анимацию и плавно увеличивать `marginTop` от текущего значения? Как нам сделать это? Ведь для начала нам надо это текущее значение получить. + +**Для того, чтобы получить текущее используемое значение свойства, используется метод `window.getComputedStyle`, описанный в стандарте DOM Level 2.** + +Его синтаксис таков: + +```js +getComputedStyle(element, pseudo) +``` + +
          +
          element
          +
          Элемент, значения для которого нужно получить
          +
          pseudo
          +
          Указывается, если нужен стиль псевдо-элемента, например `"::before"`. Пустая строка означает сам элемент.
          +
          + +Поддерживается всеми браузерами, кроме IE<9. Следующий код будет работать во всех не-IE браузерах и в IE9+: + +```html + + + + + + + +``` + +[smart header="Вычисленное (computed) и окончательное (resolved) значения"] +В CSS есть две концепции: +
            +
          1. *Вычисленное* (computed) значение -- это то, которое получено после применения всех правил CSS и CSS-наследования. Например, `width: auto` или `font-size: 125%`.
          2. +
          3. *Окончательное* ([resolved](http://dev.w3.org/csswg/cssom/#resolved-values)) значение -- непосредственно применяемое к элементу. При этом все размеры приводятся к пикселям, например `width: 212px` или `font-size: 16px`. В некоторых браузерах пиксели могут быть дробными.
          4. +
          +Когда-то `getComputedStyle` задумывалось для возврата вычисленного значения, но со временем оказалось, что окончательное гораздо удобнее. Поэтому сейчас в целом все значения возвращаются именно такие, кроме некоторых небольших глюков в браузерах, которые постепенно вычищаются. +[/smart] + +[warn header="`getComputedStyle` требует полное свойство!"] +Для правильного получения значения нужно указать точное свойство. Например: `paddingLeft`, `marginTop`, `borderLeftWidth`. + +**При обращении к сокращенному: `padding`, `margin`, `border` -- правильный результат не гарантируется.** + +Действительно, допустим свойства `paddingLeft/paddingTop` взяты из разных классов CSS. Браузер не обязан объединять их в одно свойство `padding`. Иногда, в простейших случаях, когда свойство задано сразу целиком, `getComputedStyle` сработает для сокращённого свойства, но не во всех браузерах. + +Например, некоторые браузеры (Chrome) выведут `10px` в документе ниже, а некоторые (Firefox) -- нет: + +```html + + + +``` + +[/warn] + + +[smart header="Стили посещенных ссылок -- тайна!"] +У посещенных ссылок может быть другой цвет, фон, чем у обычных. Это можно поставить в CSS с помощью псевдокласса `:visited`. + +Но `getComputedStyle` не дает доступ к этой информации, чтобы произвольная страница не могла определить, посещал ли пользователь ту или иную ссылку. + +Кроме того, большинство браузеров запрещают применять к `:visited` CSS-стили, которые могут изменить геометрию элемента, чтобы даже окольным путем нельзя было это понять. В целях безопасности. +[/smart] + +## currentStyle для IE8- + +В IE8- нет `getComputedStyle`, но у элементов есть свойство currentStyle, которое возвращает вычисленное (computed) значение: уже с учётом CSS-каскада, но не всегда в окончательном формате. + +Чтобы код работал и в старых и новых браузерах, обычно пишут кросс-браузерный код, наподобие такого: + +```js +function getStyle(elem) { + return window.getComputedStyle ? getComputedStyle(elem, "") : elem.currentStyle; +} +``` + +Если вы откроете такой документ в IE8-, то размеры будут в процентах, а в современных браузерах -- в пикселях. + +```html + + + + + +``` + +[smart header="IE8-: перевод `pt,em,%` из `currentStyle` в пиксели"] +Эта информация -- дополнительная, она не обязательна для освоения. + +В IE для того, чтобы получить из процентов реальное значение в пикселях существует метод "runtimeStyle+pixel", [описанный Дином Эдвардсом](http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291). + +Он основан на свойствах `runtimeStyle` и `pixelLeft`, работающих только в IE. + +В следующем примере функция `getIEComputedStyle(elem, prop)` получает значение в пикселях для свойства `prop`, используя `elem.currentStyle` и метод Дина Эдвардса. + +Если вам интересно, как он работает, ознакомьтесь со свойствами с runtimeStyle и pixelLeft в MSDN и раскройте код. + +```js +//+ src="getIEComputedStyle.js" hide="Раскрыть код" + +``` + + + +Рабочий пример (только IE): + +```html + +
          Тестовый элемент с margin 1%
          + + +``` + +[pre] + +
          Тестовый элемент с margin 1%
          + + + +[/pre] +Современные Javascript-фреймворки используют этот прием для эмуляции `getComputedStyle` в старых IE. +[/smart] + + +## Итого + +Все DOM-элементы предоставляют следующие свойства. + +
            +
          • Свойство `style` -- это объект, в котором CSS-свойства пишутся `вотТакВот`. Чтение и изменение его свойств -- это, по сути, работа с компонентами атрибута `style`.
          • +
          • `style.cssText` -- строка стилей для чтения или записи. Аналог полного атрибута `style`.
          • + +
          • Свойство `currentStyle`(IE8-) и метод `getComputedStyle` (IE9+, стандарт) позволяют получить реальное, применённое сейчас к элементу свойство стиля с учётом CSS-каскада и браузерных стилей по умолчанию. + +При этом `currentStyle` возвращает значение из CSS, до окончательных вычислений, а `getComputedStyle` -- окончательное, непосредственно применённое к элементу (как правило).
          • +
          + +Более полная информация о `style`, включающая другие, реже используемые методы работы с ним, доступна здесь: [CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/DOM/CSSStyleDeclaration). + diff --git a/2-ui/1-document/14-styles-and-classes/getIEComputedStyle.js b/2-ui/1-document/14-styles-and-classes/getIEComputedStyle.js new file mode 100755 index 00000000..df64388e --- /dev/null +++ b/2-ui/1-document/14-styles-and-classes/getIEComputedStyle.js @@ -0,0 +1,18 @@ +function getIEComputedStyle(elem, prop) { + var value = elem.currentStyle[prop] || 0 + + // we use 'left' property as a place holder so backup values + var leftCopy = elem.style.left + var runtimeLeftCopy = elem.runtimeStyle.left + + // assign to runtimeStyle and get pixel value + elem.runtimeStyle.left = elem.currentStyle.left + elem.style.left = (prop === "fontSize") ? "1em" : value + value = elem.style.pixelLeft + "px"; + + // restore values for left + elem.style.left = leftCopy + elem.runtimeStyle.left = runtimeLeftCopy + + return value +} diff --git a/2-ui/1-document/15-metrics/1-get-scroll-height-bottom/solution.md b/2-ui/1-document/15-metrics/1-get-scroll-height-bottom/solution.md new file mode 100644 index 00000000..4d84cf8f --- /dev/null +++ b/2-ui/1-document/15-metrics/1-get-scroll-height-bottom/solution.md @@ -0,0 +1 @@ +Решение: `elem.scrollHeight - elem.scrollTop - elem.clientHeight`. \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/1-get-scroll-height-bottom/task.md b/2-ui/1-document/15-metrics/1-get-scroll-height-bottom/task.md new file mode 100644 index 00000000..944fe06c --- /dev/null +++ b/2-ui/1-document/15-metrics/1-get-scroll-height-bottom/task.md @@ -0,0 +1,9 @@ +# Найти размер прокрутки снизу + +[importance 5] + +Свойство `elem.scrollTop` содержит размер прокрученной области при отсчете сверху. А как подсчитать его снизу? + +Напишите соответствующее выражение для произвольного элемента `elem`. + +Проверьте: если прокрутки нет или элемент полностью прокручен -- оно должно давать ноль. \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/2-scrollbar-width/solution.md b/2-ui/1-document/15-metrics/2-scrollbar-width/solution.md new file mode 100644 index 00000000..4dda1121 --- /dev/null +++ b/2-ui/1-document/15-metrics/2-scrollbar-width/solution.md @@ -0,0 +1,23 @@ +Создадим элемент с прокруткой, но без `padding`. Тогда разница между его полной шириной `offsetWidth` и внутренней `clientWidth` будет равна как раз прокрутке: + +```js +//+ run +// создадим элемент с прокруткой +var div = document.createElement('div'); + +div.style.overflowY = 'scroll'; +div.style.width = '50px'; +div.style.height = '50px'; + +// при display:none размеры нельзя узнать +// нужно, чтобы элемент был видим, +// visibility:hidden - можно, т.к. сохраняет геометрию +div.style.visibility = 'hidden'; + +document.body.appendChild(div); +var scrollWidth = div.offsetWidth - div.clientWidth; +document.body.removeChild(div); + +alert( scrollWidth ); +``` + diff --git a/2-ui/1-document/15-metrics/2-scrollbar-width/task.md b/2-ui/1-document/15-metrics/2-scrollbar-width/task.md new file mode 100644 index 00000000..77867ac8 --- /dev/null +++ b/2-ui/1-document/15-metrics/2-scrollbar-width/task.md @@ -0,0 +1,7 @@ +# Узнать ширину полосы прокрутки + +[importance 3] + +Напишите код, который возвращает ширину стандартной полосы прокрутки. Именно самой полосы, где ползунок. Обычно она равна `16px`, в редких и мобильных браузерах может колебаться от `14px` до `18px`, а кое-где даже равна `0px`. + +P.S. Ваш код должен работать на любом HTML-документе, независимо от его содержимого. \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/3-div-placeholder/solution.md b/2-ui/1-document/15-metrics/3-div-placeholder/solution.md new file mode 100644 index 00000000..b6cf804d --- /dev/null +++ b/2-ui/1-document/15-metrics/3-div-placeholder/solution.md @@ -0,0 +1,31 @@ +Нам нужно создать `div` с такими же размерами и вставить его на место "переезжающего". + +Один из вариантов -- это просто клонировать элемент. + +Если делать это при помощи `div.cloneNode(true)`, то склонируется все содержимое, которого может быть много. Обычно нам это не нужно, поэтому можно использовать `div.cloneNode(false)` для клонирования элемента со стилями, и потом поправить его `width/height`. + +Можно и просто создать новый `div` и поставить ему нужные размеры. + +**Всё, кроме `margin`, можно получить из свойств DOM-элемента, а `margin` -- только через `getComputedStyle`.** + +Причём `margin` мы обязаны поставить, так как иначе элемент не будет отодвинут от внешних. + +Код: + +```js +var div = document.getElementById('moving-div'); + +var placeHolder = document.createElement('div'); +placeHolder.style.height = div.offsetHeight + 'px'; +// можно и width, но в этом примере это не обязательно + +// IE || другой браузер +var computedStyle = div.currentStyle || getComputedStyle(div, ''); + +placeHolder.style.marginTop = computedStyle.marginTop; // (1) +placeHolder.style.marginBottom = computedStyle.marginBottom; +``` + +В строке `(1)` использование полного название свойства `"marginTop"` гарантирует, что полученное значение будет корректным. + +[edit src="solution"]Открыть решение в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/3-div-placeholder/solution.view/index.html b/2-ui/1-document/15-metrics/3-div-placeholder/solution.view/index.html new file mode 100755 index 00000000..be9ce325 --- /dev/null +++ b/2-ui/1-document/15-metrics/3-div-placeholder/solution.view/index.html @@ -0,0 +1,47 @@ + + + + + + + +Before Before Before + +
          +Text Text Text
          +Text Text Text
          +
          + +After After After + + + + + + diff --git a/2-ui/1-document/15-metrics/3-div-placeholder/source.view/index.html b/2-ui/1-document/15-metrics/3-div-placeholder/source.view/index.html new file mode 100755 index 00000000..de20a8c1 --- /dev/null +++ b/2-ui/1-document/15-metrics/3-div-placeholder/source.view/index.html @@ -0,0 +1,33 @@ + + + + + + + +Before Before Before + +
          +Text Text Text
          +Text Text Text
          +
          + +After After After + + + + + diff --git a/2-ui/1-document/15-metrics/3-div-placeholder/task.md b/2-ui/1-document/15-metrics/3-div-placeholder/task.md new file mode 100644 index 00000000..f814304d --- /dev/null +++ b/2-ui/1-document/15-metrics/3-div-placeholder/task.md @@ -0,0 +1,50 @@ +# Подменить div на другой с таким же размером + +[importance 3] + +Посмотрим следующий случай из жизни. Был текст, который, в частности, содержал `div` с зелеными границами: + +```html + + + +Before Before Before + +
          +Text Text Text
          +Text Text Text
          +
          + +After After After +``` + +Программист Валера из вашей команды написал код, который позиционирует его абсолютно и смещает в правый верхний угол. Вот этот код: + +```js +var div = document.getElementById('moving-div'); +div.style.position = 'absolute'; +div.style.right = div.style.top = 0; +``` + +Побочным результатом явилось смещение текста, который раньше шел после `DIV`. Теперь он поднялся вверх: +[iframe height=90 src="source"] + +**Допишите код Валеры, сделав так, чтобы текст оставался на своем месте после того, как `DIV` будет смещен.** + +Сделайте это путем создания вспомогательного `DIV` с теми же размерами (`width`, `height`, `border`, `margin`, `padding`), что и у желтого `DIV`. Используйте только JavaScript, без CSS. + +Должно быть так (новому блоку задан фоновый цвет для демонстрации): + +[iframe height=140 src="solution"] + +[edit src="source" task/] + + + diff --git a/2-ui/1-document/15-metrics/4-put-ball-in-center/ball-half/index.html b/2-ui/1-document/15-metrics/4-put-ball-in-center/ball-half/index.html new file mode 100755 index 00000000..818ab431 --- /dev/null +++ b/2-ui/1-document/15-metrics/4-put-ball-in-center/ball-half/index.html @@ -0,0 +1,39 @@ + + + + + + + + +
          + +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
          + + + + + + + diff --git a/2-ui/1-document/15-metrics/4-put-ball-in-center/field.png b/2-ui/1-document/15-metrics/4-put-ball-in-center/field.png new file mode 100755 index 00000000..6d1fefb9 Binary files /dev/null and b/2-ui/1-document/15-metrics/4-put-ball-in-center/field.png differ diff --git a/2-ui/1-document/15-metrics/4-put-ball-in-center/solution.md b/2-ui/1-document/15-metrics/4-put-ball-in-center/solution.md new file mode 100644 index 00000000..2dd7a7a5 --- /dev/null +++ b/2-ui/1-document/15-metrics/4-put-ball-in-center/solution.md @@ -0,0 +1,51 @@ +При абсолютном позиционировании мяча внутри поля его координаты `left/top` отсчитываются от **внутреннего** угла поля, например верхнего-левого: + + + +Метрики для внутренней зоны поля -- это `clientWidth/Height`. + +Центр - это `(clientWidth/2, clientHeight/2)`. + +Но если мы установим мячу такие значения `ball.style.left/top`, то в центре будет не сам мяч, а его левый верхний угол: + +```js +var ball = document.getElementById('ball'); +var field = document.getElementById('field'); + +ball.style.left = Math.round(field.clientWidth / 2)+'px'; +ball.style.top = Math.round(field.clientHeight / 2)+'px'; +``` + +[iframe hide="Нажмите, чтобы посмотреть текущий результат" height=180 src="ball-half"] + +Для того, чтобы центр мяча находился в центре поля, нам нужно сместить мяч на половину его ширины влево и на половину его высоты вверх. + +```js +var ball = document.getElementById('ball'); +var field = document.getElementById('field'); + +ball.style.left = Math.round(field.clientWidth/2 - ball.offsetWidth/2)+'px'; +ball.style.top = Math.round(field.clientHeight/2 - ball.offsetHeight/2)+'px'; +``` + +**Внимание, подводный камень!** + +Код выше стабильно работать не будет, потому что `IMG` идет без ширины/высоты: + +```html + +``` + +**Высота и ширина изображения неизвестны браузеру до тех пор, пока оно не загрузится, если размер не указан явно.** + +После первой загрузки изображение уже будет в кеше браузера, и его размеры будут известны. Но когда браузер впервые видит документ -- он ничего не знает о картинке, поэтому значение `ball.offsetWidth` равно `0`. Вычислить координаты невозможно. + +Чтобы это исправить, добавим `width/height` к картинке: + +```html + +``` + +Теперь браузер всегда знает ширину и высоту, так что все работает. Тот же эффект дало бы указание размеров в CSS. + +[edit src="solution"]Полный код решения[/edit] \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/4-put-ball-in-center/solution.view/index.html b/2-ui/1-document/15-metrics/4-put-ball-in-center/solution.view/index.html new file mode 100755 index 00000000..e1c0a409 --- /dev/null +++ b/2-ui/1-document/15-metrics/4-put-ball-in-center/solution.view/index.html @@ -0,0 +1,39 @@ + + + + + + + + +
          + +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
          + + + + + + + diff --git a/2-ui/1-document/15-metrics/4-put-ball-in-center/source.view/index.html b/2-ui/1-document/15-metrics/4-put-ball-in-center/source.view/index.html new file mode 100755 index 00000000..99a6d4fc --- /dev/null +++ b/2-ui/1-document/15-metrics/4-put-ball-in-center/source.view/index.html @@ -0,0 +1,27 @@ + + + + + + + + +
          + +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
          + + + + diff --git a/2-ui/1-document/15-metrics/4-put-ball-in-center/task.md b/2-ui/1-document/15-metrics/4-put-ball-in-center/task.md new file mode 100644 index 00000000..4c07cb6c --- /dev/null +++ b/2-ui/1-document/15-metrics/4-put-ball-in-center/task.md @@ -0,0 +1,21 @@ +# Поместите мяч в центр поля + +[importance 5] + +Поместите мяч в центр поля. + +Исходный документ выглядит так: +[iframe src="source" edit link] + +**Используйте JavaScript, чтобы поместить мяч в центр:** +[iframe src="solution"] + +
            +
          • Менять CSS нельзя, мяч должен переносить в центр ваш скрипт, через установку нужных стилей элемента.
          • +
          • Код не должен быть привязан к конкретному размеру мяча.
          • +
          • Обратите внимание: мяч должен быть строго по центру! Независимо от местоположения поля и ширины его рамки.
          • +
          + +[edit src="source" task/] + +P.S. Да, это можно сделать при помощи чистого CSS, но задача именно на JavaScript. Далее будет развитие темы и более сложные ситуации, когда JavaScript будет уже точно необходим. diff --git a/2-ui/1-document/15-metrics/5-expand-element/solution.md b/2-ui/1-document/15-metrics/5-expand-element/solution.md new file mode 100644 index 00000000..1c4e554b --- /dev/null +++ b/2-ui/1-document/15-metrics/5-expand-element/solution.md @@ -0,0 +1,45 @@ +**Вначале рассмотрим неверный вариант.** + +Он выглядит так: + +```js +elem.style.width = '100%'; +``` + +Если вы его попробуете, то увидите, что элемент начинает вылезать за рамки родителя. + +Так происходит потому, что ширина -- это то, что *внутри `padding`*. То есть, ставя ширину в `100%`, вы говорите: "внутренняя область должна занимать `100%` доступной ширины". А на `padding` остаётся `0%`. В результате поля вылезают наружу. + +**Правильное решение через `clientWidth`.** + +Доступную внутреннюю ширину родителя можно получить, вычитая `padding` из `clientWidth`, и присвоить элементу: + +```js +var bodyClientWidth = document.body.clientWidth; + +var style = window.getComputedStyle ? getComputedStyle(elem, '') : elem.currentStyle; + +*!* +var bodyInnerWidth = bodyClientWidth - parseInt(style.paddingLeft) - parseInt(style.paddingRight); +*/!* + +elem.style.width = bodyInnerWidth + 'px'; +``` + +Этот вариант сломается, если в IE<9 значение `padding` указано не в пикселях. Получение пикселей из процентов и других единиц измерения рассмотрено в главе [](/styles-and-classes). + +**Правильный вариант с CSS.** + +**Самое лучшее решение получится, если вспомнить, что элемент и сам рад растянуться по всей доступной ширине, и делает это по умолчанию.** + +Достаточно вернуть ему стандартный алгоритм вычисленя ширины, установив `width: auto`: + +```js +elem.style.width = 'auto'; +``` + +Но.. **Это не будет работать для элементов, которые сами по себе не растягиваются**, например в случае `position: absolute` или `float`. + +Такой элемент можно расширить, используя предыдущее решение. + +[edit src="solution"]Документ с обоими решениями[/edit] \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/5-expand-element/solution.view/index.html b/2-ui/1-document/15-metrics/5-expand-element/solution.view/index.html new file mode 100755 index 00000000..a2ab1d63 --- /dev/null +++ b/2-ui/1-document/15-metrics/5-expand-element/solution.view/index.html @@ -0,0 +1,58 @@ + + + + + + + + +
          +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +
          + + + + + + diff --git a/2-ui/1-document/15-metrics/5-expand-element/source.view/index.html b/2-ui/1-document/15-metrics/5-expand-element/source.view/index.html new file mode 100755 index 00000000..d2c10ef0 --- /dev/null +++ b/2-ui/1-document/15-metrics/5-expand-element/source.view/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
          +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст +
          + + + + + + diff --git a/2-ui/1-document/15-metrics/5-expand-element/task.md b/2-ui/1-document/15-metrics/5-expand-element/task.md new file mode 100644 index 00000000..4a8f9293 --- /dev/null +++ b/2-ui/1-document/15-metrics/5-expand-element/task.md @@ -0,0 +1,18 @@ +# Расширить элемент + +[importance 3] + +В `BODY` есть элемент `DIV` с заданной шириной `width`. + +Задача -- написать код, который "распахнет" `DIV` по ширине на всю страницу. + +Исходный документ (`DIV` -- красный): +[iframe height=220 src="source"] + +[edit src="source" task/] + +Расширить нужно точно по ширине, чтобы красный `DIV` не вылез за границы `BODY`. + +P.S. Пользоваться следует исключительно средствами JS, при этом не подглядывая в стили. То есть, код должен быть универсален и не ломаться, если цифры в CSS станут другими. + +P.P.S. После того, как решите... Будет ли ваше решение работать, если у красного `DIV` стоит `position:absolute`? Если нет, то почему и как его поправить? \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/6-width-vs-clientwidth/solution.md b/2-ui/1-document/15-metrics/6-width-vs-clientwidth/solution.md new file mode 100644 index 00000000..60b19846 --- /dev/null +++ b/2-ui/1-document/15-metrics/6-width-vs-clientwidth/solution.md @@ -0,0 +1,11 @@ +Отличия: + +
            +
          1. `getComputedStyle` не работает в IE8-.
          2. +
          3. `clientWidth` возвращает число, а `getComputedStyle(...).width` -- в `px`.
          4. +
          5. `getComputedStyle` не всегда даст ширину, он может вернуть, к примеру, `"auto"` для инлайнового элемента.
          6. +
          7. `clientWidth` соответствует внутренней видимой области элемента, *включая `padding`, а CSS-ширина `width`, при стандартном значении [box-sizing](/box-sizing), соответствует зоне *внутри `padding`*.
          8. +
          9. Если есть полоса прокрутки, то некоторые браузеры включают её ширину в `width`, а некоторые -- нет. + +Свойство `clientWidth`, с другой стороны, полностью кросс-браузерно. Оно всегда обозначает размер *за вычетом прокрутки*, т.е. реально доступный для содержимого.
          10. +
          diff --git a/2-ui/1-document/15-metrics/6-width-vs-clientwidth/task.md b/2-ui/1-document/15-metrics/6-width-vs-clientwidth/task.md new file mode 100644 index 00000000..0acb4dfa --- /dev/null +++ b/2-ui/1-document/15-metrics/6-width-vs-clientwidth/task.md @@ -0,0 +1,7 @@ +# В чём отличие "width" и "clientWidth" ? + +[importance 5] + +В чём отличия между `getComputedStyle(elem, "").width` и `elem.clientWidth`? + +В решении приведены пять отличий. Постарайтесь найти столько же или больше :) \ No newline at end of file diff --git a/2-ui/1-document/15-metrics/article.md b/2-ui/1-document/15-metrics/article.md new file mode 100644 index 00000000..a01c3b4e --- /dev/null +++ b/2-ui/1-document/15-metrics/article.md @@ -0,0 +1,276 @@ +# Размеры и прокрутка элементов + +Для того, чтобы показывать элементы правильно, подгонять на нужные места страницы, управлять ими при помощи мыши, необходимо во-первых, знать CSS-позиционирование, а во-вторых -- уметь работать с "геометрией элементов" из JavaScript. + +В этой главе мы поговорим о размерах элементов DOM, способах их вычисления и *метриках* -- различных свойствах, которые содержат эту информацию. +[cut] +## Образец документа + +Мы будем использовать для примера блок, у которого есть рамка (border), поля (padding), отступы (margin) и прокрутка: + +```html +
          + ...Текст... +
          + +``` + +Результат выглядит так: + + + +Вы можете открыть документ [edit src="metric"]по этой ссылке[/edit]. +## Получение width/height из CSS + +Какой способ первый приходит на ум, когда есть задача определить `width/height`? + +Если вы внимательно читали до этого момента, то уж точно знаете, что CSS-высоту и ширину `width/height` можно установить с помощью `elem.style` и извлечь, используя `getComputedStyle()/currentStyle`, которые в подробностях обсуждаются в главе [](/styles-and-classes). + +Решение может быть таким: + +```js +//+ run +var elem = document.body; + +var style = window.getComputedStyle ? getComputedStyle(elem, "") : elem.currentStyle; +alert(style.width); // вывести CSS-ширину body +``` + +Всегда ли такой подход сработает? Увы, нет! + +
            +
          1. Во-первых, CSS-свойства `width/height` зависят от другого свойства -- `box-sizing`, которое определяет, что такое, собственно, эти ширина и высота. По умолчанию они относятся к размеру внутренней части элемента, которая лежит внутри `padding`, а если нужно узнать полную высоту/ширину?
          2. +
          3. В IE8- могут быть нестыковки с единицами измерения -- как мы помним, `currentStyle` не пересчитывает размеры в пиксели.
          4. +
          5. И, наконец, самое главное, свойства `width/height` могут быть равны `auto`! + +Например, для инлайн-элемента: + +```html + +Привет! + + +``` + +Конечно, с точки зрения CSS размер `auto` -- совершенно нормально, но нам-то в JavaScript нужен конкретный размер в пикселях, который мы сможем использовать для вычислений. +
          6. +
          + + +## Полоса прокрутки + +Полоса прокрутки -- причина многих проблем и недопониманий. Как говорится, "дьявол кроется в деталях". Недопустимо, чтобы наш код работал на элементах без прокрутки и начинал "глючить" с ней. Поэтому мы с самого начала будем её учитывать. + +**При наличии вертикальной полосы прокрутки -- она забирает себе часть ширины элемента.** + +Ширина полосы прокрутки обычно составляет около `14-18px`, в зависимости от браузера и операционной системы. Бывает и `0` для полупрозрачной прокрутки, не отъедающей место. В примере подразумевается, что прокрутка место ест, поэтому внутренняя область будет уже не `300px`, а около `284px`. + +**Несмотря на то, что на рисунке полоса прокрутки находится визуально в правом поле -- отнимает место она не у `padding`, а у внутренней области элемента.** + +...Но при этом некоторые браузеры отражают это уменьшение ширины в результате `getComputedStyle(...).width`, а некоторые -- нет. + +В примере ниже в стилях указано `width:300px`. А вот `getComputedStyle` возвращает `300px/284px`, в зависимости от браузера. + +Если ваш браузер в принципе показывает полосу прокрутки (например, под Windows почти все браузеры так делают), то вы можете протестировать это сами, нажав на кнопку в ифрейме ниже: + +[iframe src="cssWidthScroll" link border=1] + +Описанные разночтения касаются только чтения свойства `getComputedStyle(...).width` из JavaScript, визуальное отображение корректно в обоих случаях -- ширина текста при наличии прокрутки в обоих случаях уменьшается. + +**Здесь и далее, мы будем понимать под `width` именно реальную ширину внутренней области (около `284px`), а не результат чтения CSS-свойства `width`, который может быть разным в зависимости от браузера/OS.** + +## JavaScript-метрики + +В JavaScript существует ряд дополнительных свойств, содержащих размеры элементов. Мы будем называть их "метриками". + +**Метрики JavaScript, в отличие от свойств CSS, содержат числа, всегда в пикселях и без единиц измерения на конце.** + +### clientWidth/Height + +Размер *клиентской зоны*, а именно: внутренняя область плюс `padding`. + + + +Общая ширина внутри рамки -- это `284 (width) + 20(padding left) + 20 (padding right) = 324`. + +Получаем: + +```js +clientWidth = 284(width) + 2*20(padding) = 324 +clientHeight = 200(height) + 2*20(padding) = 240 +``` + +Обратите внимание, в `clientHeight` входят и верхнее и нижнее поля, несмотря на то, что нижнее поле заполнено текстом. + +**Если `padding` нет, то `clientWidth/Height` покажет реальный размер области данных, внутри рамок и полосы прокрутки.** + + + +### scrollWidth/Height + +Ширина и высота контента *с учетом прокручиваемой области*. + +
            +
          • `scrollHeight = 723` -- полная высота, включая прокрученную область
          • +
          • `scrollWidth = 324` -- полная ширина, включая прокрученную область
          • +
          + +**`scrollWidth/Height` то же самое, что и `clientWidth/Height`, но включает в себя прокручиваемую область.** + + + +Эти свойства можно использовать, чтобы "распахнуть" элемент на всю ширину/высоту: + +```js +element.style.height = element.scrollHeight + 'px'; +``` + +Нажмите на кнопку, чтобы распахнуть элемент: + +
          текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст
          + + + +### scrollTop/scrollLeft + +Размеры текущей прокрученной части элемента -- вертикальной и горизонтальной. + +Следующее изображение иллюстрирует `scrollHeight` и `scrollTop` для блока с вертикальной прокруткой. + + + +[smart header="`scrollLeft/scrollTop` можно изменять"] +**В отличие от большинства свойств, которые доступны только для чтения, значения `scrollLeft/scrollTop` можно изменить, и браузер выполнит прокрутку элемента**. + +При клике на следующий элемент будет выполняться код `elem.scrollTop += 10`. Поэтому он будет прокручиваться на `10px` вниз: + +
          Кликни
          Меня
          1
          2
          3
          4
          5
          6
          7
          8
          9
          +[/smart] + + +### offsetWidth/Height + +Внешняя ширина/высота блока, полный размер, включая рамки, исключая внешние отступы `margin`. + +
            +
          • `offsetWidth = 390` -- внешняя ширина блока
          • +
          • `offsetHeight = 290` -- внешняя высота блока
          • +
          + + + +Эти свойства показывают *внешние* ширину и высоту блока, то как блок выглядит снаружи. + +### clientTop/Left + +Отступ *клиентской области* от внешнего угла блока. + +Другими словами, это ширина верхней/левой рамки(border) в пикселях. + +
            +
          • `clientLeft = 25` -- ширина левой рамки
          • +
          • `clientTop = 25` -- ширина верхней рамки
          • +
          + + + +Казалось бы, зачем еще какие-то свойства, если ширину рамки можно получить напрямую из CSS? Обычно они действительно не нужны. + +Но есть две ситуации, когда эти свойства бывают полезны: + +
            +
          1. В случае, когда документ располагается *справа налево* (арабский язык, иврит), свойство `clientLeft` включает в себя еще и ширину *правой* полосы прокрутки.
          2. +
          3. В IE<8 документ, а точнее -- элемент `document.documentElement` немного смещен относительно верхнего левого угла документа. Несмотря на то, что рамки там нет, сдвиг существует и хранится в `document.body.clientLeft/clientTop` (обычно это 2 пикселя).
          4. +
          + +### offsetParent, offsetLeft/Top + +[warn header="Используются редко..."] +Ситуации, когда эти свойства нужны, можно перечислить по пальцам. Они возникают действительно редко. Как правило, эти свойства используют по ошибке, потому что не знают средств правильной работы с координатами, о которых мы поговорим позже. +[/warn] + +**`offsetParent` -- это родительский элемент в смысле отображения на странице.** + +Когда браузер рисует страницу, то он высчитывает дерево расположения элементов, иначе говоря "дерево геометрии" или "дерево рендеринга". + +Обычно элементы вложены друг в друга и структура дерева рендеринга повторяет DOM. + +Но, к примеру, если у элемента стоит `position: absolute`, то его расположение вычисляется уже не относительно непосредственного родителя `parentNode`, а относительно ближайшего позиционированного элемента (т.е. свойство `position` которого не равно `static`), или `BODY`, если таковой отсутствует. + +Получается, что элемент имеет одного родителя в DOM и другого -- в плане позиционирования, относительно которого он рисуется. Этот элемент и будет в свойстве `offsetParent`. + +**Свойства `offsetLeft/Top` задают смещение относительно `offsetParent`.** + +```html +
          +
          ...
          +
          +``` + + + +
          + + +[smart header="Метрики для невидимых элементов равны нулю."] + +Координаты и размеры в JavaScript устанавливаются только для *видимых* элементов. + +Для элементов с `display:none` или находящихся вне документа дерево рендеринга не строится. Для них метрики равны нулю. Кстати, и `offsetParent` для таких элементов тоже `null`. + +Это дает нам **замечательный способ для проверки, виден ли элемент**: + +```js +function isHidden(elem) + return !elem.offsetWidth && !elem.offsetHeight; +} +``` + +
            +
          • Работает, даже если родителю элемента установлено свойство `display:none`.
          • +
          • Работает для всех элементов, кроме `TR`, с которым возникают некоторые проблемы в разных браузерах. Обычно, проверяются не `TR`, поэтому всё ок :).
          • +
          • Считает элемент видимым, даже если позиционирован за пределами экрана или имеет свойство `visibility:hidden`.
          • +
          • "Схлопнутый" элемент, например пустой `div` без высоты и ширины, будет считаться невидимым.
          • +
          +[/smart] + + +## Итого + +У элементов есть следующие метрики: +
            +
          • `clientWidth/clientHeight` -- ширина/высота видимой области, включая поля, но не полосы прокрутки.
          • +
          • `clientLeft/clientTop` -- ширина левой/верхней рамки или, точнее, сдвиг клиентской области, относительно верхнего левого угла блока. +Используется, преимущественно, в IE<8 для вычисления сдвига `document.body`. +
          • +
          • `scrollWidth/scrollHeight` -- ширина/высота прокручиваемой области. Включает в себя `padding` и не включает полосы прокрутки.
          • +
          • `scrollLeft/scrollTop` -- ширина/высота прокрученной части документа, считается от верхнего левого угла.
          • +
          • `offsetWidth/offsetHeight` -- "внешняя" ширина/высота блока, не считая отступов.
          • +
          • `offsetParent` -- "родитель по дереву рендеринга" -- ближайшая ячейка таблицы, body для статического позиционирования или ближайший позиционированный элемент для других типов позиционирования.
          • +
          • `offsetLeft/offsetTop` -- позиция в пикселях левого верхнего угла блока, относительно его `offsetParent`.
          • +
          + +Все свойства, кроме `scrollLeft/scrollTop` доступны только для чтения. Изменение этих свойств заставляет браузер прокручивать элемент. + +Краткая схема: + + + +**Прокрутку *элемента* можно прочитать или изменить через свойства `scrollLeft/Top`.** + +В этой главе мы считали, что страница находится в режиме соответствия стандартам. В режиме совместимости -- всё так же, но некоторые старые браузеры требуют `document.body` вместо `documentElement`. + diff --git a/2-ui/1-document/15-metrics/clientLeft.png b/2-ui/1-document/15-metrics/clientLeft.png new file mode 100755 index 00000000..b7bf8a34 Binary files /dev/null and b/2-ui/1-document/15-metrics/clientLeft.png differ diff --git a/2-ui/1-document/15-metrics/clientWidth.png b/2-ui/1-document/15-metrics/clientWidth.png new file mode 100755 index 00000000..cfc6222e Binary files /dev/null and b/2-ui/1-document/15-metrics/clientWidth.png differ diff --git a/2-ui/1-document/15-metrics/clientWidthNoPadding.png b/2-ui/1-document/15-metrics/clientWidthNoPadding.png new file mode 100755 index 00000000..dc57a65a Binary files /dev/null and b/2-ui/1-document/15-metrics/clientWidthNoPadding.png differ diff --git a/2-ui/1-document/15-metrics/css.png b/2-ui/1-document/15-metrics/css.png new file mode 100755 index 00000000..430e08ae Binary files /dev/null and b/2-ui/1-document/15-metrics/css.png differ diff --git a/2-ui/1-document/15-metrics/cssWidthScroll.view/index.html b/2-ui/1-document/15-metrics/cssWidthScroll.view/index.html new file mode 100755 index 00000000..27019b13 --- /dev/null +++ b/2-ui/1-document/15-metrics/cssWidthScroll.view/index.html @@ -0,0 +1,29 @@ + + + + +
          +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +текст текст текст текст текст текст текст текст текст текст текст текст текст текст +
          + + + +У элемента стоит style="width:300px"
          + + + + + diff --git a/2-ui/1-document/15-metrics/metric.view/index.html b/2-ui/1-document/15-metrics/metric.view/index.html new file mode 100755 index 00000000..50972d9c --- /dev/null +++ b/2-ui/1-document/15-metrics/metric.view/index.html @@ -0,0 +1,95 @@ + + + + + + + + + + + +
          +

          Introduction

          +

          This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company's Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0.

          + +

          The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997.

          + +

          That Ecma Standard was submitted to ISO/IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.

          + +

          The third edition of the Standard introduced powerful regular expressions, better string handling, new control statements, try/catch exception handling, tighter definition of errors, formatting for numeric output and minor changes in anticipation of forthcoming internationalisation facilities and future language growth. The third edition of the ECMAScript standard was adopted by the Ecma General Assembly of December 1999 and published as ISO/IEC 16262:2002 in June 2002.

          + +
          + + +
          Координаты мыши: ...
          +
          + + + + + + diff --git a/2-ui/1-document/15-metrics/offsetLeft.png b/2-ui/1-document/15-metrics/offsetLeft.png new file mode 100755 index 00000000..c897d9cf Binary files /dev/null and b/2-ui/1-document/15-metrics/offsetLeft.png differ diff --git a/2-ui/1-document/15-metrics/offsetWidth.png b/2-ui/1-document/15-metrics/offsetWidth.png new file mode 100755 index 00000000..8ba49795 Binary files /dev/null and b/2-ui/1-document/15-metrics/offsetWidth.png differ diff --git a/2-ui/1-document/15-metrics/scrollTop.png b/2-ui/1-document/15-metrics/scrollTop.png new file mode 100755 index 00000000..3c595d89 Binary files /dev/null and b/2-ui/1-document/15-metrics/scrollTop.png differ diff --git a/2-ui/1-document/15-metrics/scrollWidth.png b/2-ui/1-document/15-metrics/scrollWidth.png new file mode 100755 index 00000000..b6fd3949 Binary files /dev/null and b/2-ui/1-document/15-metrics/scrollWidth.png differ diff --git a/2-ui/1-document/15-metrics/summary.png b/2-ui/1-document/15-metrics/summary.png new file mode 100755 index 00000000..df266417 Binary files /dev/null and b/2-ui/1-document/15-metrics/summary.png differ diff --git a/2-ui/1-document/16-metrics-window/1-get-document-scrolls/solution.md b/2-ui/1-document/16-metrics-window/1-get-document-scrolls/solution.md new file mode 100644 index 00000000..40278b0b --- /dev/null +++ b/2-ui/1-document/16-metrics-window/1-get-document-scrolls/solution.md @@ -0,0 +1,47 @@ +`top` -- можно кроссбраузерно получить, как указано в главе [](/metrics-window): + +```js +function getDocumentScrollTop() { + var html = document.documentElement; + var body = document.body; + + var scrollTop = html.scrollTop || body && body.scrollTop || 0; + scrollTop -= html.clientTop; // IE<8 + + return scrollTop; +} +``` + +`bottom` -- это `top` плюс высота видимой части: + +```js +function getDocumentScrollBottom() { + return getDocumentScrollTop() + document.documentElement.clientHeight; +} +``` + +Полная высота -- максимум двух значений, детали см. в [](/metrics-window): + +```js +function getDocumentScrollHeight() { + var scrollHeight = document.documentElement.scrollHeight; + var clientHeight = document.documentElement.clientHeight; + + scrollHeight = Math.max(scrollHeight, clientHeight); + + return scrollHeight; +} +``` + +Итого, ответ, использующий описанные выше функции: + +```js +function getDocumentScroll() { + return { + top: getDocumentScrollTop(), + bottom: getDocumentScrollBottom(), + height: getDocumentScrollHeight() + }; +} +``` + diff --git a/2-ui/1-document/16-metrics-window/1-get-document-scrolls/task.md b/2-ui/1-document/16-metrics-window/1-get-document-scrolls/task.md new file mode 100644 index 00000000..b04f07cd --- /dev/null +++ b/2-ui/1-document/16-metrics-window/1-get-document-scrolls/task.md @@ -0,0 +1,15 @@ +# Получить прокрутки документа + +[importance 5] + +Напишите функцию `getDocumentScroll()`, которая возвращает объект с координатами области видимости относительно документа. + +Свойства объекта результата: + +
            +
          • `top` -- координата верхней границы видимой части (относительно документа).
          • +
          • `bottom` -- координата нижней границы видимой части (относительно документа).
          • +
          • `height` -- полная высота документа, включая прокрутку.
          • +
          + +В задаче можно учитывать только вертикальную прокрутку (горизонтальную отдельно нет смысла разбирать, она делается аналогично, а нужна сильно реже). \ No newline at end of file diff --git a/2-ui/1-document/16-metrics-window/article.md b/2-ui/1-document/16-metrics-window/article.md new file mode 100644 index 00000000..e1d93a2b --- /dev/null +++ b/2-ui/1-document/16-metrics-window/article.md @@ -0,0 +1,209 @@ +# Размеры и прокрутка страницы + +Многие метрики для страницы работают совсем не так, как для элементов. Поэтому рассмотрим решения типичных задач для страницы отдельно. +[cut] +## Ширина/высота видимой части окна + +Свойства `clientWidth/Height` для элемента `document.documentElement` позволяют получить ширину/высоту видимой области окна. + +Например, кнопка ниже выведет размер такой области для этой страницы: + + + +Этот способ -- кросс-браузерный. + +## Ширина/высота всей страницы, с учётом прокрутки + +Если прокрутка на странице присутствует, то полные размеры страницы можно взять в `document.documentElement.scrollWidth/scrollHeight`. + +Проблемы с этими свойствами возникают, когда *прокрутка то есть, то нет*. В этом случае они работают некорректно. + +В браузерах Chrome/Safari и Opera при отсутствии прокрутки значение `document.documentElement.scrollHeight` в этом случае может быть даже меньше, чем `document.documentElement.clientHeight` (нонсенс!). Эта проблема -- именно для `document.documentElement`, то есть для всей страницы. С обычными элементами здесь всё в порядке. + +Надёжно определить размер с учетом прокрутки можно, взяв максимум из двух свойств: + +```js +//+ run +var scrollHeight = document.documentElement.scrollHeight; +var clientHeight = document.documentElement.clientHeight; + +*!* +scrollHeight = Math.max(scrollHeight, clientHeight); +*/!* + +alert('Высота с учетом прокрутки: ' + scrollHeight); +``` + +## Прокрутка страницы [#page-scroll] + +### Получение текущей прокрутки + +Значение текущей прокрутки страницы хранится в свойствах `window.pageXOffset/pageYOffset`. + +Но эти свойства: +
            +
          • Не поддерживаются IE<9
          • +
          • Их можно только читать, а менять нельзя.
          • +
          + +Поэтому для кросс-браузерности рассмотрим другой способ -- свойство `document.documentElement.scrollLeft/Top`. + +
            +
          • `document.documentElement` содержит значение прокрутки, если стоит правильный DOCTYPE. Это работает во всех браузерах, кроме Safari/Chrome.
          • +
          • Safari/Chrome используют вместо этого `document.body` (это баг в Webkit).
          • +
          • В режиме совместимости (если некорректный DOCTYPE) некоторые браузеры также используют `document.body`.
          • +
          + +Таким образом, для IE8+ и других браузеров, работающих в режиме соответствия стандартам, получить значение прокрутки можно так: + +```js +//+ run +var scrollTop = window.pageYOffset || document.documentElement.scrollTop; + +alert("Текущая прокрутка: " + scrollTop); +``` + +### С учётом IE7- и Quirks Mode [#getPageScroll] + +Если дополнительно нужна поддержка IE<8, то там тоже есть важная тонкость. Документ может быть смещен относительно начальной позиции (0,0). Это смещение хранится в `document.documentElement.clientLeft/clientTop`, и мы должны вычесть его. + +Если дополнительно добавить возможность работы браузера в Quirks Mode, то надёжный способ будет таким: + +```js +//+ run +var html = document.documentElement; +var body = document.body; + +var scrollTop = html.scrollTop || body && body.scrollTop || 0; +scrollTop -= html.clientTop; +alert("Текущая прокрутка: " + scrollTop); +``` + +Итого, можно создать кросс-браузерную функцию, которая возвращает значения прокрутки и поддерживает в том числе IE8-: + +```js +var getPageScroll = (window.pageXOffset != undefined) ? + function() { + return { + left: pageXOffset, + top: pageYOffset + }; + } : + function() { + var html = document.documentElement; + var body = document.body; + + var top = html.scrollTop || body && body.scrollTop || 0; + top -= html.clientTop; + + var left = html.scrollLeft || body && body.scrollLeft || 0; + left -= html.clientLeft; + + return { top: top, left: left }; + } +``` + +### Изменение прокрутки: scrollTo, scrollBy, scrollIntoView [#window-scroll] + +[smart] +Чтобы прокрутить страницу при помощи JavaScript, её DOM должен быть полностью загружен. +[/smart] + +На обычных элементах свойства `scrollTop/scrollLeft` можно изменять, и при этом элемент будет прокручиваться. + +Никто не мешает точно так же поступать и со страницей. Во всех браузерах, кроме Chrome/Safari можно осуществить прокрутку установкой `document.documentElement.scrollTop`, а в Chrome/Safari -- использовать для этого `document.body.scrollTop`. И будет работать. + +Но есть и другое, полностью кросс-браузерное решение -- специальные методы прокрутки страницы [window.scrollBy(x,y)](https://developer.mozilla.org/en/Window.scrollBy) и [window.scrollTo(pageX,pageY)](https://developer.mozilla.org/en/Window.scrollTo). + + + +
            +
          • **Метод `scrollBy(x,y)` прокручивает страницу относительно текущих координат.** +Например, кнопка ниже прокрутит страницу на `10px` вниз: + + +
          • +
          • **Метод `scrollTo(pageX,pageY)` прокручивает страницу к указанным координатам относительно документа.** Он эквивалентен установке свойств `scrollLeft/scrollTop`. + +Чтобы прокрутить в начало документа, достаточно указать координаты `(0,0)`: + +
          • +
          + +Для полноты картины рассмотрим также метод [elem.scrollIntoView(top)](https://developer.mozilla.org/en/DOM/element.scrollIntoView). + +Метод `elem.scrollIntoView(top)` вызывается на элементе и прокручивает страницу так, чтобы элемент оказался вверху, если параметр `top` равен `true`, и внизу, если `top` равен `false`. Причем, если параметр `top` не указан, то он считается равным `true`. + +Кнопка ниже прокрутит страницу так, чтобы кнопка оказалась вверху: + + + +А следующая кнопка прокрутит страницу так, чтобы кнопка оказалась внизу: + + + +## Запрет прокрутки + +Иногда бывает нужно временно сделать документ "непрокручиваемым". Например, при показе большого диалогового окна над документом -- чтобы посетитель мог прокручивать это окно, но не документ. + +**Чтобы запретить прокрутку страницы, достаточно поставить `document.body.style.overflow = "hidden"`.** + +При этом страница замрёт в текущем положении. Попробуйте сами: + + + + + +При нажатии на верхнюю кнопку страница замрёт на текущем положении прокрутки. После нажатия на нижнюю -- прокрутка возобновится. + +**Вместо `document.body` может быть любой элемент, прокрутку которого необходимо запретить.** + +Недостатком этого способа является то, что сама полоса прокрутки исчезает. Если она занимала некоторую ширину, то теперь эта ширина освободится, и содержимое страницы расширится, заняв её место. Такая перерисовка иногда выглядит как "прыжок" страницы. Это может быть не очень красиво, но обходится, если вычислить размер прокрутки и добавить `padding-right`. + +## Итого + +Размеры: + +
            +
          • Для получения размеров видимой части окна: `document.documentElement.clientWidth/Height` +
          • +
          • Для получения размеров страницы с учётом прокрутки: + +```js +var scrollHeight = document.documentElement.scrollHeight; +var clientHeight = document.documentElement.clientHeight; + +*!* +scrollHeight = Math.max(scrollHeight, clientHeight); +*/!* +``` + +
          • +
          + +**Прокрутка окна:** + +
            +
          • Прокрутку окна можно *получить* как `window.pageYOffset` (для горизонтальной -- `window.pageXOffset`) везде, кроме IE<9. + +Для кросс-браузерности используется другой способ: + +```js +//+ run +var html = document.documentElement; +var body = document.body; + +var scrollTop = html.scrollTop || body && body.scrollTop || 0; +scrollTop -= html.clientTop; // IE<8 +alert("Текущая прокрутка: " + scrollTop); +``` + +
          • +
          • Установить прокрутку можно при помощи специальных методов: +
              +
            • `window.scrollTo(pageX,pageY)` -- абсолютные координаты,
            • +
            • `window.scrollBy(x,y)` -- прокрутить относительно текущего места.
            • `elem.scrollIntoView(top)` -- прокрутить, чтобы элемент `elem` стал виден.
            • +
            +
          • +
          + diff --git a/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.md b/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.md new file mode 100644 index 00000000..ceae552e --- /dev/null +++ b/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.md @@ -0,0 +1,42 @@ +# Координаты внешних углов + +Координаты элемента возвращаются функцией [elem.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/DOM/element.getBoundingClientRect). Она возвращает все координаты относительно окна в виде объекта со свойствами `left`, `top`, `right`, `bottom`. Некоторые браузеры также добавляют `width`, `height`. + +Так что координаты верхнего-левого `coords1` и правого-нижнего `coords4` внешних углов: + +```js +var coords = elem.getBoundingClientRect(); + +var coords1 = [coords.left, coords.top]; +var coords2 = [coords.right, coords.bottom]; +``` + +# Левый-верхний угол внутри + +Этот угол отстоит от наружных границ на размер рамки, который доступен через `clientLeft/clientTop`: + +```js +var coords3 = [coords.left + field.clientLeft, coords.top + field.clientTop]; +``` + +# Правый-нижний угол внутри + +Этот угол отстоит от правой-нижней наружной границы на размер рамки. Так как нужная рамка находится справа-внизу, то специальных свойств для нее нет, но мы можем получить этот размер из CSS: + +```js +var coords4 = [ + coords.right - parseInt(getComputedStyle(field).borderRightWidth) , + coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth) +] +``` + +Можно получить их альтернативным путем, прибавив `clientWidth/clientHeight` к координатам левого-верхнего внутреннего угла. Получится то же самое, пожалуй даже быстрее и изящнее. + +```js +var coords4 = [ + coords.left + elem.clientLeft + elem.clientWidth , + coords.top + elem.clientTop + elem.clientHeight +] +``` + +[edit src="solution"]Полный код решения[/edit] \ No newline at end of file diff --git a/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.view/index.css b/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.view/index.css new file mode 100755 index 00000000..9d4f4a6e --- /dev/null +++ b/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.view/index.css @@ -0,0 +1,27 @@ +body { + padding: 20px 0 0 20px; + cursor: pointer; +} + +#field { + overflow: hidden; + width: 200px; + height: 150px; + border-top: 10px solid black; + border-right: 10px solid gray; + border-bottom: 10px solid gray; + border-left: 10px solid black; + background-color: #00FF00; + font: 10px/1.2 monospace; +} + +.triangle-right { + position: relative; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 20px solid red; + text-indent: -20px; + font: 12px/1 monospace; +} diff --git a/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.view/index.html b/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.view/index.html new file mode 100755 index 00000000..f4c46101 --- /dev/null +++ b/2-ui/1-document/17-coordinates/1-find-point-coordinates/solution.view/index.html @@ -0,0 +1,62 @@ + + + + + + + + + + Кликните на любое место, чтобы получить координаты относительно окна.
          + Это для удобства тестирования, чтобы проверить результат, который вы получите из DOM.
          +
          (координаты появятся тут)
          + + +
          + . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
          + + +
          1
          +
          3
          +
          4
          +
          2
          + + + + + + diff --git a/2-ui/1-document/17-coordinates/1-find-point-coordinates/source.view/index.css b/2-ui/1-document/17-coordinates/1-find-point-coordinates/source.view/index.css new file mode 100755 index 00000000..9d4f4a6e --- /dev/null +++ b/2-ui/1-document/17-coordinates/1-find-point-coordinates/source.view/index.css @@ -0,0 +1,27 @@ +body { + padding: 20px 0 0 20px; + cursor: pointer; +} + +#field { + overflow: hidden; + width: 200px; + height: 150px; + border-top: 10px solid black; + border-right: 10px solid gray; + border-bottom: 10px solid gray; + border-left: 10px solid black; + background-color: #00FF00; + font: 10px/1.2 monospace; +} + +.triangle-right { + position: relative; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 20px solid red; + text-indent: -20px; + font: 12px/1 monospace; +} diff --git a/2-ui/1-document/17-coordinates/1-find-point-coordinates/source.view/index.html b/2-ui/1-document/17-coordinates/1-find-point-coordinates/source.view/index.html new file mode 100755 index 00000000..cd09f1f8 --- /dev/null +++ b/2-ui/1-document/17-coordinates/1-find-point-coordinates/source.view/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + Кликните на любое место, чтобы получить координаты относительно окна.
          + Это для удобства тестирования, чтобы проверить результат, который вы получите из DOM.
          +
          (координаты появятся тут)
          + + +
          + . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
          + + +
          1
          +
          3
          +
          4
          +
          2
          + + + + + + diff --git a/2-ui/1-document/17-coordinates/1-find-point-coordinates/task.md b/2-ui/1-document/17-coordinates/1-find-point-coordinates/task.md new file mode 100644 index 00000000..052ebcb9 --- /dev/null +++ b/2-ui/1-document/17-coordinates/1-find-point-coordinates/task.md @@ -0,0 +1,25 @@ +# Найдите координаты точки в документе + +[importance 5] + +В ифрейме ниже вы видите документ с зеленым "полем". + +При помощи JavaScript найдите координаты указанных стрелками углов относительно окна браузера. + +Для тестирования в документ добавлено удобство: клик в любом месте отображает координаты мыши относительно окна. + +[iframe border=1 height=360 src="source" link edit] + +Ваш код должен при помощи DOM получить четыре пары координат: +
            +
          1. Левый-верхний угол снаружи, это просто.
          2. +
          3. Правый-нижний угол снаружи, это тоже просто.
          4. +
          5. Левый-верхний угол внутри, это чуть сложнее.
          6. +
          7. Правый-нижний угол внутри, это ещё сложнее, но можно сделать даже несколькими способами.
          8. +
          + +Они должны совпадать с координатами, которые вы получите кликом по полю. + +P.S. Код не должен быть как-то привязан к конкретным размерам элемента, стилям, наличию или отсутствию рамки. + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/1-document/17-coordinates/2-position-at/solution.md b/2-ui/1-document/17-coordinates/2-position-at/solution.md new file mode 100644 index 00000000..099f5e81 --- /dev/null +++ b/2-ui/1-document/17-coordinates/2-position-at/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/1-document/17-coordinates/2-position-at/solution.view/index.css b/2-ui/1-document/17-coordinates/2-position-at/solution.view/index.css new file mode 100755 index 00000000..c3749b0d --- /dev/null +++ b/2-ui/1-document/17-coordinates/2-position-at/solution.view/index.css @@ -0,0 +1,28 @@ +.note { + position: fixed; + z-index: 1000; + padding: 5px; + border: 1px solid black; + background: white; + text-align: center; + font: italic 14px Georgia; +} + +blockquote { + background:#f9f9f9; + border-left:10px solid #ccc; + margin: 0 0 0 100px; + padding:.5em 10px; + quotes:"\201C""\201D""\2018""\2019"; + display: inline-block; + white-space: pre; +} + +blockquote:before { + color: #ccc; + content: open-quote; + font-size:4em; + line-height:.1em; + margin-right:.25em; + vertical-align:-.4em; +} diff --git a/2-ui/1-document/17-coordinates/2-position-at/solution.view/index.html b/2-ui/1-document/17-coordinates/2-position-at/solution.view/index.html new file mode 100755 index 00000000..4726fcc2 --- /dev/null +++ b/2-ui/1-document/17-coordinates/2-position-at/solution.view/index.html @@ -0,0 +1,85 @@ + + + + + + + + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + +
          +- Что на завтрак, Бэрримор? +- Овсянка, сэр. +- А на обед? +- Овсянка, сэр. +- Ну а на ужин? +- Котлеты, сэр. +- Уррра!!! +- Из овсянки, сэр!!! +
          + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + + + + + + diff --git a/2-ui/1-document/17-coordinates/2-position-at/source.view/index.css b/2-ui/1-document/17-coordinates/2-position-at/source.view/index.css new file mode 100755 index 00000000..c3749b0d --- /dev/null +++ b/2-ui/1-document/17-coordinates/2-position-at/source.view/index.css @@ -0,0 +1,28 @@ +.note { + position: fixed; + z-index: 1000; + padding: 5px; + border: 1px solid black; + background: white; + text-align: center; + font: italic 14px Georgia; +} + +blockquote { + background:#f9f9f9; + border-left:10px solid #ccc; + margin: 0 0 0 100px; + padding:.5em 10px; + quotes:"\201C""\201D""\2018""\2019"; + display: inline-block; + white-space: pre; +} + +blockquote:before { + color: #ccc; + content: open-quote; + font-size:4em; + line-height:.1em; + margin-right:.25em; + vertical-align:-.4em; +} diff --git a/2-ui/1-document/17-coordinates/2-position-at/source.view/index.html b/2-ui/1-document/17-coordinates/2-position-at/source.view/index.html new file mode 100755 index 00000000..f9e156cb --- /dev/null +++ b/2-ui/1-document/17-coordinates/2-position-at/source.view/index.html @@ -0,0 +1,58 @@ + + + + + + + + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + +
          +- Что на завтрак, Бэрримор? +- Овсянка, сэр. +- А на обед? +- Овсянка, сэр. +- Ну а на ужин? +- Котлеты, сэр. +- Уррра!!! +- Из овсянки, сэр!!! +
          + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + + + + + + diff --git a/2-ui/1-document/17-coordinates/2-position-at/task.md b/2-ui/1-document/17-coordinates/2-position-at/task.md new file mode 100644 index 00000000..43617adf --- /dev/null +++ b/2-ui/1-document/17-coordinates/2-position-at/task.md @@ -0,0 +1,13 @@ +# Разместить заметку рядом с элементом + +[importance 5] + +Создайте функцию `positionAt(anchor, position, elem)`, которая позиционирует элемент `elem`, в зависимости от `position`, сверху (`"top"`), справа (`"right"`) или снизу (`"bottom"`) от элемента `anchor`. + +Используйте её, чтобы сделать функцию `showNote(anchor, position, html)`, которая показывает элемент с классом `note` и текстом `html` на позиции `position` рядом с элементом `anchor`. + +Выведите заметки как здесь: + +[iframe src="solution" height="450" border="1" link] + +[edit src="source" task/] diff --git a/2-ui/1-document/17-coordinates/article.md b/2-ui/1-document/17-coordinates/article.md new file mode 100644 index 00000000..fe9364eb --- /dev/null +++ b/2-ui/1-document/17-coordinates/article.md @@ -0,0 +1,176 @@ +# Координаты в окне + +Для того, чтобы поместить один элемент рядом с другим на странице, а также двигать его произвольным образом, к примеру, рядом с указателем мыши -- используются координаты. + +Первая координатная система, которую мы посмотрим, начинается в левом-верхнем углу текущей видимой области окна. + +Мы будем называть координаты в ней `clientX/clientY`. + + +## getBoundingClientRect() + +Синтаксис: + +```js +var coords = elem.getBoundingClientRect(); +``` + +Возвращает координаты элемента, а точнее -- размеры прямоугольника, который охватывает элемент, в виде объекта со свойствами: `top`, `left`, `right` и `bottom`: +
            +
          • `top` -- Y-координата верхней границы элемента,
          • +
          • `left` -- X-координата левой границы,
          • +
          • `right` -- X-координата правой границы,
          • +
          • `bottom` -- Y-координата нижней границы.
          • +
          + +Например: + + + +Обратите внимание: страница в этом примере прокручена, её часть осталась сверху. + +**Координаты относительно окна не учитывают прокрутку, они высчитываются от границ текущей видимой области.** + +Например, кликните на кнопку, чтобы увидеть её координаты: + + + + + +Если вы прокрутите эту страницу, то положение кнопки в окне изменится, и её координаты, соответственно, тоже. + +
            +
          • Координаты могут быть дробными -- это нормально, так как они возвращаются из внутренних структур браузера.
          • +
          • Координаты могут быть и отрицательными, например если прокрутить страницу так, что часть кнопки будет выходить за верхнуюю границу окна, то её `top`-координата будет меньше нуля.
          • +
          • Некоторые современные браузеры также добавляют к объекту свойства для ширины и высоты: `width/height`, но их можно получить и простым вычитанием: `height = bottom - top`, `width = right - left`.
          • +
          + + +[smart header="Метод `elem.getBoundingClientRect()` изнутри"] + +Браузер отображает любое содержимое, используя прямоугольники. + +В случае с блочным элементом, таким как `DIV`, элемент сам по себе образует прямоугольник. Но если элемент строчный и содержит в себе длинный текст, то каждая строка будет отдельным прямоугольником, с одинаковой высотой но разной длиной (у каждой строки -- своя длина). + +Более подробно это описано в: спецификации. + +Если обобщить, содержимое элемента может отображаться в одном прямоугольнике или в нескольких. + +Все эти прямоугольники можно получить с помощью [elem.getClientRects()](https://developer.mozilla.org/en/DOM/element.getClientRects). А метод [elem.getBoundingClientRect()](https://developer.mozilla.org/en/DOM/element.getBoundingClientRect) возвращает один охватывающий прямоугольник для всех `getClientRects()`. +[/smart] + + +## elementFromPoint(x, y) [#elementFromPoint] + +Возвращает элемент, который находится на координатах `(x, y)` относительно окна. + +Синтаксис: + +```js +var elem = document.elementFromPoint(x, y); +``` + +Например, код ниже ниже выделяет и выводит тег у элемента, который сейчас в середине окна: + +```js +//+ run +var centerX = document.documentElement.clientWidth / 2; +var centerY = document.documentElement.clientHeight / 2; + +var elem = document.elementFromPoint(centerX, centerY); + +elem.style.background = "red"; +alert( elem.tagName ); +elem.style.background = ""; +``` + +Аналогично предыдущему методу, используются координаты относительно окна. В зависимости от прокрутки страницы, от размеров окна браузера, в центре может быть разный элемент. + +## position:fixed + +Координаты обычно требуются не просто так, а, например, чтобы переместить элемент на них. + +В CSS для позиционирования элемента относительно окна используется свойство `position:fixed`. Как правило, вместе с ним идут и координаты, например `left/top`. + +Например, этот код покажет сообщение под элементом с `id="coords-show-mark"`: + +```js +var elem = document.getElementById("coords-show-mark"); + +// получить координаты +var coords = elem.getBoundingClientRect(); + +// создать элемент для сообщения +var message = document.createElement('div'); + +// эти свойства можно было бы задать классом +message.style.position = "fixed"; +message.style.background = "red"; +message.style.color = "yellow"; + +*!* +// к координатам обязательно добавляем "px"! +message.style.left = coords.left + "px"; +message.style.top = coords.bottom + "px"; +*/!* + +message.innerHTML = "Привет, мир!"; + +// добавить на 10 сек в документ +document.body.appendChild(message); +setTimeout(function() { + document.body.removeChild(message); +}, 10000); +``` + +Нажмите на кнопку, чтобы запустить его: + + + +Этот код можно модифицировать, чтобы показывать сообщение слева, справа, сверху, делать это вместе с CSS-анимацией и так далее. Для этого нужно всего лишь понимать, как получить координаты. + +**Заметим, однако, важную деталь: при прокрутке страницы сообщение отделяется от кнопки.** + +Причина очевидна, ведь оно использует `position: fixed`. Как это обойти, мы посмотрим в следующей главе. + + +[head] + +[/head] \ No newline at end of file diff --git a/2-ui/1-document/17-coordinates/transitions-bare.png b/2-ui/1-document/17-coordinates/transitions-bare.png new file mode 100755 index 00000000..346c57d2 Binary files /dev/null and b/2-ui/1-document/17-coordinates/transitions-bare.png differ diff --git a/2-ui/1-document/17-coordinates/transitions-bare@2x.png b/2-ui/1-document/17-coordinates/transitions-bare@2x.png new file mode 100755 index 00000000..172ad1e6 Binary files /dev/null and b/2-ui/1-document/17-coordinates/transitions-bare@2x.png differ diff --git a/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.md b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.md new file mode 100644 index 00000000..099f5e81 --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.view/index.css b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.view/index.css new file mode 100755 index 00000000..0e0b7ec9 --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.view/index.css @@ -0,0 +1,28 @@ +.note { + position: absolute; + z-index: 1000; + padding: 5px; + border: 1px solid black; + background: white; + text-align: center; + font: italic 14px Georgia; +} + +blockquote { + background:#f9f9f9; + border-left:10px solid #ccc; + margin: 0 0 0 100px; + padding:.5em 10px; + quotes:"\201C""\201D""\2018""\2019"; + display: inline-block; + white-space: pre; +} + +blockquote:before { + color: #ccc; + content: open-quote; + font-size:4em; + line-height:.1em; + margin-right:.25em; + vertical-align:-.4em; +} diff --git a/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.view/index.html b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.view/index.html new file mode 100755 index 00000000..4ead72fc --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/solution.view/index.html @@ -0,0 +1,101 @@ + + + + + + + + +

          Исправления два:

          + +
            +
          1. Использование функции getCoords() из учебника для получения абсолютных координат.
          2. +
          3. Изменение position:fixed на position:absolute в стилях.
          4. +
          + +
          +- Что на завтрак, Бэрримор? +- Овсянка, сэр. +- А на обед? +- Овсянка, сэр. +- Ну а на ужин? +- Котлеты, сэр. +- Уррра!!! +- Из овсянки, сэр!!! +
          + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + + + + + + diff --git a/2-ui/1-document/18-coordinates-document/1-position-at-absolute/task.md b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/task.md new file mode 100644 index 00000000..5e229a89 --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/1-position-at-absolute/task.md @@ -0,0 +1,9 @@ +# Разместить заметку рядом с элементом (absolute) + +[importance 5] + +Модифицируйте решение задачи [](/task/position-at), чтобы при прокрутке страницы заметка не убегала от элемента. + +Используйте для этого координаты относительно документа и `position:absolute` вместо `position:fixed`. + +В качестве исходного документа используйте решение задачи [](/task/position-at), для тестирования прокрутки добавьте стиль ``. \ No newline at end of file diff --git a/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.md b/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.md new file mode 100644 index 00000000..33000522 --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.md @@ -0,0 +1 @@ +[edit src="solution"/] \ No newline at end of file diff --git a/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.view/index.css b/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.view/index.css new file mode 100755 index 00000000..588db26b --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.view/index.css @@ -0,0 +1,29 @@ +.note { + position: absolute; + z-index: 1000; + padding: 5px; + border: 1px solid black; + background: white; + text-align: center; + font: italic 14px Georgia; + opacity: .8; +} + +blockquote { + background: #f9f9f9; + border-left: 10px solid #ccc; + margin: 0 0 0 100px; + padding: .5em 10px; + quotes: "\201C""\201D""\2018""\2019"; + display: inline-block; + white-space: pre; +} + +blockquote:before { + color: #ccc; + content: open-quote; + font-size: 4em; + line-height: .1em; + margin-right: .25em; + vertical-align: -.4em; +} diff --git a/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.view/index.html b/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.view/index.html new file mode 100755 index 00000000..0a8fce0e --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/2-position-at-2/solution.view/index.html @@ -0,0 +1,115 @@ + + + + + + + + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + +
          +- Что на завтрак, Бэрримор? +- Овсянка, сэр. +- А на обед? +- Овсянка, сэр. +- Ну а на ужин? +- Котлеты, сэр. +- Уррра!!! +- Из овсянки, сэр!!! +
          + +

          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reprehenderit sint atque dolorum fuga ad incidunt voluptatum error fugiat animi amet! Odio temporibus nulla id unde quaerat dignissimos enim nisi rem provident molestias sit tempore omnis recusandae esse sequi officia sapiente.

          + + + + + + + diff --git a/2-ui/1-document/18-coordinates-document/2-position-at-2/task.md b/2-ui/1-document/18-coordinates-document/2-position-at-2/task.md new file mode 100644 index 00000000..f81c127f --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/2-position-at-2/task.md @@ -0,0 +1,27 @@ +# Разместить заметку внутри элемента + +[importance 5] + +Расширьте предыдущую задачу [](/task/position-at-absolute): научите функцию `positionAt(anchor, position, elem)` вставлять `elem` внутрь `anchor`. + +Новые значения `position`: +
            +
          • `top-out`, `right-out`, `bottom-out` -- работают так же, как раньше, то есть вставляют `elem` над/справа/под `anchor`.
          • +
          • `top-in`, `right-in`, `bottom-in` -- вставляют `elem` внутрь `anchor`: к верхней границе/правой/нижней.
          • +
          + +Например: + +```js +// покажет note сверху blockquote +positionAt(blockquote, "top-out", note); + +// покажет note сверху-внутри blockquote +positionAt(blockquote, "top-in", note); +``` + +Пример результата: + +[iframe src="solution" height="500" border="1" link] + +В качестве исходного документа возьмите решение задачи [](/task/position-at-absolute). \ No newline at end of file diff --git a/2-ui/1-document/18-coordinates-document/article.md b/2-ui/1-document/18-coordinates-document/article.md new file mode 100644 index 00000000..90b8ceac --- /dev/null +++ b/2-ui/1-document/18-coordinates-document/article.md @@ -0,0 +1,266 @@ +# Координаты в документе + +Система координат относительно всей страницы или, иначе говоря, относительно *документа*, тоже начинается в левом-верхнем углу. + +Мы будем называть координаты в ней `pageX/pageY`. + +[cut] + +Зачем нужны ещё какие-то координаты, кроме рассмотренных ранее? + +Как мы видели в конце предыдущей главы, позиционирование через `position: fixed` привязывает элемент не к месту на странице, а к окну. Поэтому при прокрутке страница под таким элементом двигается, а сам элемент -- нет. + +**Как правило, мы хотим показать элемент в определённом месте страницы, а не окна.** + +Для этого используют `position: absolute` и координаты `left/top`, которые заданы относительно документа. + +## Сравнение систем координат + +Когда страница не прокручена, точки начала координат относительно окна `(clientX,clientY)` и документа `(pageX,pageY)` совпадают: + + + +Например, координаты элемента с надписью "STANDARDS" равны расстоянию от верхней/левой границы окна: + + + +**Прокрутим страницу, чтобы элемент был на самом верху:** + +Посмотрите на рисунок ниже, на нём -- та же страница, только прокрученная, и тот же элемент "STANDARDS". + +
            +
          • Координата `clientY` изменилась. Она теперь равна `0`, так как элемент находится вверху окна.
          • +
          • Координата `pageY` осталась такой же, так как отсчитывается от левого-верхнего угла *документа*.
          • +
          + + + +**Итак, координаты `pageX/pageY` не меняются при прокрутке, в отличие от `clientX/clientY`.** + +Технически, координаты относительно страницы включают в себя текущую прокрутку. Эти две системы координат жёстко связаны, их разность `pageY-clientY` -- в точности размер текущей прокрученной области. + +## Получение координат [#getCoords] + +К сожалению, готовой функции для получения координат элемента относительно страницы нет. Но её можно легко написать самим. + +Наша функция `getCoords(elem)` будет брать результат `elem.getBoundingClientRect()` и прибавлять текущую прокрутку документа. + +Результат: объект с координатами `{left: .., top: ..}` + +```js +//+ autorun +function getCoords(elem) { + // (1) + var box = elem.getBoundingClientRect(); + + var body = document.body; + var docEl = document.documentElement; + + // (2) + var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; + + // (3) + var clientTop = docEl.clientTop || body.clientTop || 0; + var clientLeft = docEl.clientLeft || body.clientLeft || 0; + + // (4) + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + + return { top: top, left: left }; +} +``` + +Разберем что и зачем, по шагам: + +
            +
          1. Получаем прямоугольник.
          2. +
          3. Считаем прокрутку страницы. Все браузеры, кроме IE8- поддерживают свойство `pageXOffset/pageYOffset`. В более старых IE, когда установлен DOCTYPE, прокрутку можно получить из `documentElement`, ну и наконец если DOCTYPE некорректен -- использовать `body`.
          4. +
          5. В IE документ может быть смещен относительно левого верхнего угла. Получим это смещение.
          6. +
          7. Добавим прокрутку к координатам окна и вычтем смещение `html/body`, чтобы получить координаты всего документа.
          8. +
          + +### Устаревший метод: offset* + +Есть альтернативный способ нахождения координат -- это пройти всю цепочку `offsetParent` от элемента вверх и сложить отступы `offsetLeft/offsetTop`. + +Мы разбираем его здесь с учебной целью, так как он используется лишь в старых браузерах. + +Вот функция, реализующая такой подход. + +```js +//+ autorun +function getOffsetSum(elem) { + var top = 0, left = 0; + + while(elem) { + top = top + parseInt(elem.offsetTop); + left = left + parseInt(elem.offsetLeft); + elem = elem.offsetParent; + } + + return {top: top, left: left}; +} +``` + +Казалось бы, код нормальный. И он как-то работает, но разные браузеры преподносят "сюрпризы", включая или выключая размер рамок и прокруток из `offsetTop/Left`, некорректно учитывая позиционирование. В итоге результат не всегда верен. Можно, конечно, разобрать эти проблемы и посчитать действительно аккуратно и правильно этим способом, но зачем? Ведь есть `getBoundingClientRect`. + +### Сравнение offset* с getBoundingClientRect + +Посмотрим разницу между описанными способами вычисления координат на примере. + +В прямоугольнике ниже есть 3 вложенных `DIV`. Все они имеют `border`, кое-кто из них имеет `position/margin/padding`. + +Кликните по внутреннему (жёлтому) элементу, чтобы увидеть результаты обоих методов: `getOffsetSum` и `getCoords`, а также реальные координаты курсора -- `event.pageX/pageY` (мы обсудим их позже в статье [](/fixevent)). + +[pre] +
          +
          +
          Кликните, чтобы получить координаты getOffsetSum и getCoords
          +
          +
          +
          +
          getOffsetSum:значение getOffsetSum()
          +
          getCoords:значение getCoords()
          +
          mouse:координаты мыши
          +
          + + +[/pre] + +**При клике на любом месте желтого блока вы легко увидите разницу между `getOffsetSum(elem)` и `getCoords(elem)`.** + +Для того, чтобы узнать, какой же результат верный, кликните в левом-верхнем углу жёлтого блока, причём обратите внимание -- кликать нужно не на жёлтом, а на чёрном, это рамка, она тоже входит в элемент. Будут видны точные координаты мыши, так что вы можете сравнить их с `getOffsetSum/getCoords`. + +Пример клика в правильном месте (обратите внимание на разницу координат): + + + +**Именно `getCoords` всегда возвращает верное значение :).** + + + + +### Комбинированный подход + +Фреймворки, которые хотят быть совместимыми со старыми браузерами, используют комбинированный подход: + +```js +function getOffset(elem) { + if (elem.getBoundingClientRect) { + return getCoords(elem); + } else { // старый браузер + return getOffsetSum(elem); + } +} +``` + +[js hide="Открыть полный код getCoords/getOffsetSum"] +function getOffsetSum(elem) { + var top=0, left=0 + while(elem) { + top = top + parseInt(elem.offsetTop) + left = left + parseInt(elem.offsetLeft) + elem = elem.offsetParent + } + + return {top: top, left: left} +} + + +function getCoords(elem) { + var box = elem.getBoundingClientRect() + + var body = document.body; + var docEl = document.documentElement; + + var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; + + var clientTop = docEl.clientTop || body.clientTop || 0; + var clientLeft = docEl.clientLeft || body.clientLeft || 0; + + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + + return { top: Math.round(top), left: Math.round(left) }; +} + + +function getOffset(elem) { + if (elem.getBoundingClientRect) { + return getCoords(elem) + } else { + return getOffsetSum(elem) + } +} +[/js] + + +## Координаты на экране screenX/screenY + +Есть ещё одна система координат, которая используется очень редко, но для полноты картины необходимо её упомянуть. + +Координаты относительно *экрана* `screenX/screenY` отсчитываются от его левого-верхнего угла. Имеется в виду именно *весь экран*, а не окно браузера. + + + +Такие координаты могут быть полезны, например, при работе с мобильными устройствами или для открытия нового окна посередине экрана вызовом [window.open](https://developer.mozilla.org/en-US/docs/DOM/window.open). + +
            +
          • **Общая информация об экране хранится в глобальной переменной [screen](https://developer.mozilla.org/en/DOM/window.screen):** + +```js +//+ run +// общая ширина/высота +alert( screen.width + ' x ' + screen.height ); + +// доступная ширина/высота (за вычетом таскбара и т.п.) +alert( screen.availWidth + ' x ' + screen.availHeight); + +// есть и ряд других свойств screen (см. документацию) +``` + +
          • +
          • **Координаты левого-верхнего угла браузера на экране хранятся в `window.screenX,` `window.screenY`** (не поддерживаются IE8-): + +```js +//+ run +alert("Браузер находится на " + window.screenX + "," + window.screenY); +``` + +Они могут быть и меньше нуля, если окно частично вне экрана.
          • +
          • **Координаты *DOM-элемента* на экране получить нельзя, браузер не предоставляет свойств и методов для этого.**
          • +
          + +## Итого + +У любой точки в браузере есть координаты: +
            +
          1. Относительно окна `window` -- `elem.getBoundingClientRect()`.
          2. +
          3. Относительно документа `document` -- добавляем прокрутку, во всех фреймворках есть готовая функция.
          4. +
          5. Относительно экрана `screen` -- можно узнать координаты браузера, но не элемента.
          6. +
          + +Метод `elem.getBoundingClientRect()` поддерживается IE очень давно, с версии 6, а вот версии других браузеров старше чем 2010 года (примерно), могут не иметь его. Для них (и только для них) используется подсчёт координат суммированием `offsetTop/Left`. + +Координаты будут нужны нам далее, при работе с событиями мыши (координаты клика) и элементами (перемещение). + diff --git a/2-ui/1-document/18-coordinates-document/getcoords-compare.png b/2-ui/1-document/18-coordinates-document/getcoords-compare.png new file mode 100755 index 00000000..8459a297 Binary files /dev/null and b/2-ui/1-document/18-coordinates-document/getcoords-compare.png differ diff --git a/2-ui/1-document/18-coordinates-document/getcoords-compare@2x.png b/2-ui/1-document/18-coordinates-document/getcoords-compare@2x.png new file mode 100755 index 00000000..51f50217 Binary files /dev/null and b/2-ui/1-document/18-coordinates-document/getcoords-compare@2x.png differ diff --git a/2-ui/1-document/18-coordinates-document/pagewindow0.png b/2-ui/1-document/18-coordinates-document/pagewindow0.png new file mode 100755 index 00000000..83672bfb Binary files /dev/null and b/2-ui/1-document/18-coordinates-document/pagewindow0.png differ diff --git a/2-ui/1-document/18-coordinates-document/screen.png b/2-ui/1-document/18-coordinates-document/screen.png new file mode 100755 index 00000000..40905f92 Binary files /dev/null and b/2-ui/1-document/18-coordinates-document/screen.png differ diff --git a/2-ui/1-document/18-coordinates-document/standards-scroll.png b/2-ui/1-document/18-coordinates-document/standards-scroll.png new file mode 100755 index 00000000..0ab7389b Binary files /dev/null and b/2-ui/1-document/18-coordinates-document/standards-scroll.png differ diff --git a/2-ui/1-document/18-coordinates-document/standards.png b/2-ui/1-document/18-coordinates-document/standards.png new file mode 100755 index 00000000..f0e49efa Binary files /dev/null and b/2-ui/1-document/18-coordinates-document/standards.png differ diff --git a/2-ui/1-document/19-support-polyfill/1-polyfill-textcontent-ie8/solution.md b/2-ui/1-document/19-support-polyfill/1-polyfill-textcontent-ie8/solution.md new file mode 100644 index 00000000..af305164 --- /dev/null +++ b/2-ui/1-document/19-support-polyfill/1-polyfill-textcontent-ie8/solution.md @@ -0,0 +1,30 @@ +Код для полифилла здесь имеет стандартный вид: + +```js +(function() { + + // проверяем поддержку + if (document.createElement('div').textContent === undefined) { + + // определяем свойство + Object.defineProperty(Element.prototype, "textContent", { + get : function() { + return this.innerText; + }, + set : function(value) { + this.innerText = value; + } + } + ); + } + +})(); +``` + +Единственный тонкий момент -- в проверке поддержки. + +Мы часто можем использовать уже существующий элемент. В частности, при проверке `firstElementChild` мы можем проверить его наличие в `document.documentElement`. + +Однако, в данном случае попытка получить `document.documentElement.textContent` при поддержке этого свойства приведёт к совершенно лишним затратам времени и памяти на генерацию текстового содержимого всего документа. + +Поэтому мы создаём временный пустой элемент и проверяем наличие свойства у него. diff --git a/2-ui/1-document/19-support-polyfill/1-polyfill-textcontent-ie8/task.md b/2-ui/1-document/19-support-polyfill/1-polyfill-textcontent-ie8/task.md new file mode 100644 index 00000000..aaad6bbd --- /dev/null +++ b/2-ui/1-document/19-support-polyfill/1-polyfill-textcontent-ie8/task.md @@ -0,0 +1,9 @@ +# Полифилл для textContent + +[importance 5] + +Свойство `textContent` не поддерживается IE8. Однако, там есть свойство `innerText`. + +Создаёте полифилл для него, который использует `innerText`. При наличии полифилла в IE8 свойство `textContent` должно стать "псевдонимом" для `innerText`. + +Хотя свойство `innerText` и работает по-иному, нежели `textContent`, но в некоторых ситуациях они могут быть взаимозаменимы. Именно на них направлен полифилл. diff --git a/2-ui/1-document/19-support-polyfill/article.md b/2-ui/1-document/19-support-polyfill/article.md new file mode 100644 index 00000000..fd56413e --- /dev/null +++ b/2-ui/1-document/19-support-polyfill/article.md @@ -0,0 +1,232 @@ +# Современный DOM: полифиллы + +В старых IE, особенно в IE8 и ниже, ряд стандартных DOM-свойств не поддерживаются или поддерживаются плохо. + +Но это не значит, что, поддерживая IE8-, мы должны о них забыть! + +Их можно использовать, необходимо только поставить нужный полифилл. +[cut] + +## Полифиллы + +"Полифилл" (англ. polyfill) -- это библиотека, которая добавляет в старые браузеры поддержку возможностей, которые в современных браузерах являются встроенными. + +Один полифилл мы уже видели, когда изучали собственно JavaScript -- это библиотека [ES5 shim](https://github.com/es-shims/es5-shim). Если её подключить, то в IE8- начинают работать многие возможности ES5. Работает она через модификацию стандартных объектов и их прототипов. Это типично для полифиллов. + +В работе с DOM несовместимостей гораздо больше, как и способов их обхода. + +Как правило, полифиллы организованы в виде коллекции, из которой можно как выбрать отдельные свойства и функции, так и подключить всё вместе, пачкой. + +Примеры полифиллов: +
            +
          • [](https://github.com/jonathantneal/polyfill) -- ES5 вместе с DOM
          • +
          • [](https://github.com/termi/ES5-DOM-SHIM) -- ES5 вместе с DOM
          • +
          • [](https://github.com/inexorabletash/polyfill) -- ES5+ вместе с DOM
          • +
          + +Есть и более мелкие библиотеки, а также коллекции ссылок на них: + +
            +
          • [](http://compatibility.shwups-cms.ch/en/polyfills/)
          • +
          • [](http://html5please.com/#polyfill)
          • +
          • [](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills)
          • +
          + +Например, мы хотим в браузере IE8 использовать свойство `firstElementChild`, которое по умолчанию отсутствует. + +Для этого мы либо смотрим свою любимую коллекцию полифиллов на предмет его поддержки, либо набираем в Google: ["\"polyfill firstElementChild\""](https://www.google.ru/search?q=polyfill+firstElementChild). Одна из ссылок укажет на [](http://compatibility.shwups-cms.ch/en/polyfills/)... + +И вот, пожалуйста, код, который нужно подключить к IE8, чтобы получить `firstElementChild` (и не только): +
            +
          • [](http://compatibility.shwups-cms.ch/en/polyfills?&id=81)
          • +
          + +## Что делает полифилл? + +Как работает полифилл для `firstElementChild`? Посмотрим внимательнее на его JS: + +```js +//+ run +*!* +if( document.createElement('div').firstElementChild === undefined ) { // (1) +*/!* + +*!* + Object.defineProperty(Element.prototype, 'firstElementChild', { // (2) +*/!* + get: function () { + var el = this.firstChild; + do { + if( el.nodeType === 1 ) { + return el; + } + el = el.nextSibling; + } while(el); + + return null; + } + }); +} +``` + +Если этот код запустить, то `firstElementChild` появится у всех элементов в IE8. + +Общий вид этого полифилла довольно типичен. Обычно полифилл состоит из двух частей: +
            +
          1. Проверка, есть ли встроенная возможность.
          2. +
          3. Эмуляция, если её нет.
          4. +
          + +### Проверка встроенного свойства + +Для проверки встроенной поддержки `firstElementChild` создаём элемент и смотрим, есть ли у него это свойство. + +**"Фишка" заключается в том, что если бы DOM-свойство поддерживалось, то его значение никогда не было бы `undefined`. Если детей нет -- свойство было бы `null`.** + +Сравните: + +```js +//+ run +var div = document.createElement('div'); + +alert( div.firstChild ); // null, поддержка есть +alert( div.blabla ); // undefined, поддержки нет +``` + +**Важная тонкость -- элемент, который мы тестируем, должен *по стандарту* поддерживать такое свойство.** + +Попытаемся, к примеру, проверить "поддержку" свойства `value`. У `input` оно есть, у `div` такого свойства нет: + +```js +//+ run +var div = document.createElement('div'); +var input = document.createElement('input'); + +alert( input.value ); // пустая строка, поддержка есть +alert( div.value ); // undefined, поддержки нет +``` + +[smart header="Поддержка значений свойств"] +**Если мы хотим проверить поддержку не свойства целиком, а некоторых его значений, то ситуация сложнее.** + +Например, нам интересно, поддерживает ли браузер ``. То есть, понятно, что свойство `type` у `input`, в целом, поддерживается, а вот конкретный тип ``? + +**Для этого можно присвоить такой атрибут и посмотреть, подействовал ли он.** + +Например, сохранился ли он в свойстве: + +```js +//+ run +var input = document.createElement("input"); +*!* +input.setAttribute("type", "range"); +*/!* + +var support = (input.type == "range"); + +alert('Поддержка: ' + support); +``` + +Эта проверка работает, так как хоть в атрибут `type` и можно присвоить любую строку, но в DOM-свойство `type` [по стандарту](http://www.w3.org/TR/html-markup/input.html) хранит реальный тип `input'а`. Если присвоить неподдерживаемый тип, то свойство `type` не изменится. + +Ниже вы можете увидеть, верно ли сработал код определения поддержки. Если он вывел `true`, то HTML ниже выведет красивый "слайдер", иначе -- текстовое поле. + +```html + + +``` + +[/smart] + +### Ещё проверки: Modernizr + +Существует целый фреймворк [Modernizr](http://modernizr.com/), посвящённый разнообразным проверкам. Использовать его очень просто. + +Можно либо скачать сборку, которая проверяет поддержку именно того, что интересно, со страницы [](http://modernizr.com/download/), либо подключить вообще все проверки файлом [](http://modernizr.com/downloads/modernizr.js). + +Пример использования: + +```html + + + + +``` + +### Добавляем поддержку + +Если мы осуществили проверку и видим, что встроенной поддержки нет -- полифилл должен её добавить. + +Для этого вспомним, что DOM элементы описываются соответствующими JS-классами. + +Например: + + +Они наследуют, как указано в секции DOM Interface по ссылкам выше, от [HTMLElement](http://www.w3.org/TR/html5/dom.html#htmlelement), который является общим родительским классом для HTML-элементов. + +А `HTMLElement`, в свою очередь, наследует от [Element](http://www.w3.org/TR/dom/#interface-element), который является общим родителем не только для HTML, но и для других DOM-структур, например для XML и SVG. + +**Для добавления нужной возможности берётся правильный класс и модифицируется его `prototype`.** + +Самое простое -- это добавить всем элементам в прототип функцию, например: + +```js +//+ run +Element.prototype.sayHi = function() { + alert("Привет от " + this); +} + +document.body.sayHi(); // Привет от [object HTMLBodyElement] +``` + +**Сложнее -- добавить свойство, но это тоже возможно, через `Object.defineProperty`:** + +```js +//+ run +Object.defineProperty(Element.prototype, 'lowerTag', { + get: function() { + return this.tagName.toLowerCase(); + } +}); + +alert( document.body.lowerTag ); // body +``` + +[warn header="Геттер-сеттер и IE8"] +В IE8 современные методы для работы со свойствами, такие как [Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), [Object.getOwnPropertyDescriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) и другие не поддерживаются для произвольных объектов, но отлично работают для DOM-элементов. + +Чем полифиллы и пользуются. +[/warn] + +В итоге получается, что если в браузере нет нужного свойства -- оно появляется. + +Эта техника почти не работает в совсем старых IE6,7. Когда-то для них использовалась особая "IE-магия" при помощи `.htc`-файлов, которые [более не поддерживаются](http://msdn.microsoft.com/en-us/library/ie/hh801216.aspx). + +Если нужно поддерживать и эти версии, то рекомендуется воспользоваться фреймворками. К счастью, для большинства проектов эти браузеры уже стали историей. + +## Итого + +
            +
          • Для того, чтобы спокойно и без оглядки на старые браузеры использовать современные возможности DOM, используются особые библиотеки, которые называют "полифиллами" (polyfill).
          • +
          • Для поиска полифилла обычно достаточно ввести в поисковике `"polyfill"`, и нужное свойство либо метод. Как правило, полифиллы идут в виде коллекций скриптов.
          • +
          • Полифиллы хороши тем, что мы просто подключаем их и используем везде современный DOM/JS, а когда старые браузеры окончательно отомрут -- просто выкинем полифилл, без изменения кода.
          • +
          + +Для создания полифилла DOM-свойства или метода: +
            +
          • Создаётся элемент, который его, в теории, должен поддерживать.
          • +
          • Соответствующее свойство сравнивается с `undefined`.
          • +
          • Если его нет -- модифицируется прототип, обычно это `Element.prototype` -- в него дописываются новые геттеры и функции.
          • +
          + +Другие полифиллы сделать сложнее. Например, для добавления поддержки `` полифилл должен искать все такие элементы на странице и обрабатывать их. Возможности такого полифилла ограничены -- если уже существующему `` поменять `type` на `range` -- полифилл не "подхватит" его. + +И это нормальная ситуация, что полифилл не обеспечивает 100% совместимости. Скорее всего, мы не собираемся так делать, а значит -- полифилл вполне подойдёт. + + diff --git a/2-ui/1-document/2-dom-nodes/1-body-from-head/solution.md b/2-ui/1-document/2-dom-nodes/1-body-from-head/solution.md new file mode 100644 index 00000000..4fd0177f --- /dev/null +++ b/2-ui/1-document/2-dom-nodes/1-body-from-head/solution.md @@ -0,0 +1,18 @@ +Выведет `null`, так как на момент выполнения скрипта тег `` ещё не обработан браузером. + +Попробуйте в действии: + +```html + + + + + + + Привет, мир! + + +``` + diff --git a/2-ui/1-document/2-dom-nodes/1-body-from-head/task.md b/2-ui/1-document/2-dom-nodes/1-body-from-head/task.md new file mode 100644 index 00000000..9303132d --- /dev/null +++ b/2-ui/1-document/2-dom-nodes/1-body-from-head/task.md @@ -0,0 +1,21 @@ +# Что выведет этот alert? + +[importance 5] + +Что выведет `alert`? + +```html + + +*!* + +*/!* + + + Привет, мир! + + +``` + diff --git a/2-ui/1-document/2-dom-nodes/article.md b/2-ui/1-document/2-dom-nodes/article.md new file mode 100644 index 00000000..f7e53cac --- /dev/null +++ b/2-ui/1-document/2-dom-nodes/article.md @@ -0,0 +1,250 @@ +# Дерево DOM + +Основным инструментом работы и динамических изменений на странице является DOM (Document Object Model) -- объектная модель, используемая для XML/HTML-документов. + + +[cut] +Согласно DOM-модели, документ является иерархией, деревом. Каждый HTML-тег образует узел дерева с типом "элемент". Вложенные в него теги становятся дочерними узлами. Для представления текста создаются узлы с типом "текст". + +**DOM -- это представление документа в виде дерева объектов, доступное для изменения через JavaScript.** + +## Пример DOM + +Построим, для начала, дерево DOM для следующего документа. + +```html + + + + + О лосях + + + Правда о лосях + + +``` + +
          + + +В этом дереве выделено два типа узлов. + +
            +
          1. Теги образуют синие узлы-элементы (element node). Естественным образом одни узлы вложены в другие. Структура дерева образована за счет синих элементов-узлов -- тегов HTML.
          2. +
          3. Текст внутри элементов образует **текстовые узлы** (text node), обозначенные как `#text`. Текстовый узел содержит исключительно строку текста и не может иметь потомков.
          4. +
          + +Например, у корневого элемента `` есть три узла-потомка: ``, текстовый узел и `` + +**На рисунке выше синие узлы-элементы можно кликать, при этом их дети будут скрываться-раскрываться.** + +Обратите внимание на специальные символы в текстовых узлах: +
            +
          • перевод строки: `↵`
          • +
          • пробел: `␣`
          • +
          + +**Пробелы и переводы строки -- это тоже текст, полноправные символы, которые учитываются в DOM!** + +В частности, в примере выше тег `` содержит не только узел-элемент ``, но и два текстовых узла до и после него, состоящие из пробелов и переводов строк. Они не видны, но они существуют! + +Исключения из этого правила только на самом верхнем уровне -- пробелы до `<head>` по стандарту игнорируются, а любое содержимое после `</body>` не создаёт узла, браузер переносит его в конец `body`. + +В остальных случаях всё честно -- если пробелы есть в документе, то они есть и в DOM, а если их убрать, то и в DOM их не будет, получится так: + +```html +<!DOCTYPE HTML> +<html><head><title>О лосяхПравда о лосях +``` + +
          + + +## Автоисправление + +При чтении неверного HTML браузер автоматически корректирует его для показа и при построении DOM. + +В частности, всегда будет верхний тег ``. Даже если в тексте нет -- в DOM он будет, браузер создаст его самостоятельно. + +То же самое касается и тега ``. + +Например, если файл состоит из одного слова `"Привет"`, то браузер автоматически обернёт его в `` и ``. + +**При генерации DOM браузер самостоятельно обрабатывает ошибки в документе, закрывает теги и так далее.** + +Такой документ: + +```html +

          Привет +

        • Мама +
        • и +
        • Папа +``` + +...Превратится вот во вполне респектабельный DOM, браузер сам закроет теги: + +
          + + +[warn header="Таблицы всегда содержат ``"] +Важный "особый случай" при работе с DOM -- таблицы. По стандарту DOM они обязаны иметь ``, однако по стандарту HTML их можно написать без него. + +В этом случае браузер добавляет `` самостоятельно. + +Например, для такого HTML: + +```html + + +
          1
          +``` + +DOM-структура будет такой: +
          + + +Вы видите? Появился ``, как будто документ был таким: + +```html + +*!* + +*/!* + +*!* + +*/!* +
          1
          +``` + +Важно знать об этом, иначе при работе с таблицами возможны сюрпризы. +[/warn] + + +## Другие типы узлов + +Дополним страницу новыми тегами и комментарием: + +```html + + + + Правда о лосях +
            +
          1. Лось — животное хитрое
          2. +*!* + +*/!* +
          3. ..и коварное!
          4. +
          + + +``` + +
          + + +**В этом примере тегов уже больше, и даже появился узел нового типа -- *комментарий*.** + +Казалось бы, зачем комментарий в DOM? На отображение-то он всё равно не влияет. Но так как он есть в HTML -- обязан присутствовать в DOM-дереве. + +**Всё, что есть в HTML, находится и в DOM.** + +Вообще-то это секрет, но и директива `` тоже является DOM-узлом, и находится в дереве DOM непосредственно перед HTML. На иллюстрациях выше этот факт скрыт, поскольку здесь и далее мы с этим узлом работать не будем. + +Даже сам объект `document`, формально, является DOM-узлом, самым-самым корневым. + +Всего различают 12 типов узлов, но на практике мы работаем с тремя из них: элемент, текст и, редко, комментарии (если они есть в HTML). + +## Возможности, которые дает DOM + +Зачем, кроме красивых рисунков, нужна иерархическая модель DOM? + +**DOM нужен для того, чтобы манипулировать страницей -- читать информацию из HTML, создавать и изменять элементы.** + +Узел `HTML` можно получить как `document.documentElement`, а `BODY` -- как `document.body`. Получив узел, мы можем что-то сделать с ним. + +Например, можно поменять цвет `BODY` и вернуть обратно: + +```js +//+ run +document.body.style.backgroundColor = 'red'; +alert('Поменяли цвет BODY'); + +document.body.style.backgroundColor = ''; +alert('Сбросили цвет BODY'); +``` + +Фактически, DOM предоставляет возможность делать со страницей всё, что угодно, и далее мы этому научимся. + +## Особенности IE8- + +IE8- не генерирует текстовые узлы, если они состоят только из пробелов. + + То есть, такие два документа дадут идентичный DOM: + +```html + +О лосяхПравда о лосях +``` + +И такой: + +```html + + + + О лосях + + + Правда о лосях + + +``` + +Эта, с позволения сказать, "оптимизация" не соответствует стандарту и IE9+ уже работает как нужно, то есть как описано ранее. + +Но, по большому счёту, для нас должно быть без разницы, старый или новый IE. + +**При работе с DOM/HTML мы в любом случае не должны быть завязаны на то, есть пробел между тегами или его нет. Мало ли, сегодня он есть, а завтра решили переформатировать HTML и его не стало.** + +К счастью, свойства и методы DOM, которые мы пройдём далее, вполне позволяют писать код, который будет работать корректно во всех версиях браузеров. + +## Итого + +
            +
          • DOM-модель -- это внутреннее представление HTML-страницы в виде дерева.
          • +
          • Все элементы страницы, включая теги, текст, комментарии, являются узлами DOM.
          • +
          • У элементов DOM есть свойства и методы, которые позволяют изменять их.
          • +
          • IE8- не генерирует пробельные узлы.
          • +
          + +Кстати, DOM-модель используется не только в JavaScript, это известный способ представления XML-документов. + +В следующих главах мы лучше познакомимся с DOM и увидим, какие свойства и методы нужны, чтобы творить красивые штуки со страницей. + +[libs] +d3 +domtree +[/libs] \ No newline at end of file diff --git a/2-ui/1-document/20-dom-cheatsheet/article.md b/2-ui/1-document/20-dom-cheatsheet/article.md new file mode 100644 index 00000000..c893a1a8 --- /dev/null +++ b/2-ui/1-document/20-dom-cheatsheet/article.md @@ -0,0 +1,157 @@ +# Итого: DOM-шпаргалка + +В этой статье перечислены основные свойства и методы DOM, которые мы изучили. + +Используйте её, чтобы быстро подглядеть то, что изучали ранее. + +[cut] + +## Создание + +
          +
          `document.createElement(tag)`
          создать элемент с тегом `tag`
          +
          `document.createTextNode(txt)`
          создать текстовый узел с текстом `txt`
          +
          `node.cloneNode(deep)`
          клонировать существующий узел, если `deep=false`, то без потомков.
          +
          + +## Свойства узлов + +
          +
          `node.nodeType`
          тип узла: 1(элемент) / 3(текст) / другие.
          +
          `elem.tagName`
          тег элемента.
          +
          `elem.innerHTML`
          HTML внутри элемента.
          +
          `node.data`
          содержимое любого узла любого типа, кроме элемента.
          +
          + +## Ссылки + +
          +
          `document.documentElement`
          +
          элемент ``
          +
          `document.body`
          +
          элемент ``
          +
          + +По всем узлам: +
            +
          • `parentNode`
          • +
          • `nextSibling` `previousSibling`
          • +
          • `childNodes` `firstChild` `lastChild`
          • +
          + +Только по элементам: + +
            +
          • `children`
          • +
          • `nextElementSibling` `previousElementSibling`
          • +
          • `firstElementChild` `lastElementChild`
          • +
          + +В IE8- из них работает только `children`, причём содержит не только элементы, но и комментарии (ошибка в браузере). + +### Таблицы + +
          +
          `table.rows[N]`
          +
          строка `TR` номер `N`.
          +
          `tr.cells[N]`
          +
          ячейка `TH/TD` номер `N`.
          +
          `tr.sectionRowIndex`
          +
          номер строки в таблице в секции `THEAD/TBODY`.
          +
          `td.cellIndex`
          +
          номер ячейки в строке.
          +
          + +### Формы + +
          +
          `document.forms[N/name]`
          +
          форма по номеру/имени.
          +
          `form.elements[N/name]`
          +
          элемент формы по номеру/имени
          +
          `element.form`
          +
          форма для элемента.
          +
          + +## Поиск + + +
          +
          `*.querySelector(css)`
          +
          По селектору, только первый элемент
          +
          `*.querySelectorAll(css)`
          +
          По селектору CSS3, в IE8 по CSS 2.1
          +
          `document.getElementById(id)`
          +
          По уникальному `id`
          +
          `document.getElementsByName(name)`
          +
          По атрибуту `name`, в IE<10 работает только для элементов, где `name` предусмотрен стандартом.
          +
          `*.getElementsByTagName(tag)`
          +
          По тегу `tag`
          +
          `*.getElementsByClassName(class)`
          +
          По классу, IE9+, корректно работает с элементами, у которых несколько классов.
          +
          + +При поддержки IE только версии 8 и выше, можно использовать только `querySelector/querySelectorAll`. + +Для более старых IE нужен либо фреймворк, который сам умеет искать узлы по селектору, наподобие jQuery, либо пользоваться методами `get*`, все из которых, кроме `...ByClassName`, поддерживаются с древних времён. + +## Изменение + +
            +
          • `parent.appendChild(newChild)`
          • +
          • `parent.removeChild(child)`
          • +
          • `parent.insertBefore(newChild, refNode)`
          • +
          • `parent.insertAdjacentHTML("beforeBegin|afterBegin|beforeEnd|afterEnd", html)`
          • +
          + +## Классы и стили + +
          +
          `elem.className`
          +
          Атрибут `class` +
          `elem.classList.add(class) remove(class) toggle(class) contains(class)`
          +
          Управление классами в HTML5, для IE8+ есть [эмуляция](https://github.com/eligrey/classList.js/blob/master/classList.js).
          +
          `elem.style`
          +
          Стили в атрибуте `style` элемента
          +
          `getComputedStyle(elem, "")` +
          Стиль, с учётом всего каскада, вычисленный и применённый (только чтение)
          +
          + +## Размеры и прокрутка элемента + +
          +
          `clientLeft/Top`
          +
          Ширина левой/верхней рамки `border`
          +
          `clientWidth/Height`
          +
          Ширина/высота внутренней части элемента, включая содержимое и `padding`, не включая полосу прокрутки (если есть).
          +
          `scrollWidth/Height`
          +
          Ширина/высота внутренней части элемента, с учетом прокрутки.
          +
          `scrollLeft/Top`
          +
          Ширина/высота прокрученной области.
          +
          `offsetWidth/Height`
          +
          Полный размер элемента: ширина/высота, включая `border`.
          +
          + +## Размеры и прокрутка страницы + +
            +
          • ширина/высота видимой области: `document.documentElement.clientHeight`
          • +
          • прокрутка(чтение): ` window.pageYOffset || document.documentElement.scrollTop`
          • +
          • прокрутка(изменение): +
              +
            • `window.scrollBy(x,y)`: на x,y относительно текущей позиции.
            • +
            • `window.scrollTo(pageX, pageY)`: на координаты в документе.
            • +
            • `elem.scrollIntoView(true/false)`: прокрутить, чтобы `elem` стал видимым и оказался вверху окна(`true`) или внизу(`false`)
            • +
            +
          • +
          + +## Координаты + +
            +
          • относительно окна: `elem.getBoundingClientRect()`
          • +
          • относительно документа: `elem.getBoundingClientRect()` + прокрутка страницы
          • +
          • получить элемент по координатам: `document.elementFromPoint(clientX, clientY)`
          • +
          + +Список намеренно сокращён, чтобы было проще найти то, что нужно. diff --git a/2-ui/1-document/3-dom-console/1.png b/2-ui/1-document/3-dom-console/1.png new file mode 100755 index 00000000..864a95c9 Binary files /dev/null and b/2-ui/1-document/3-dom-console/1.png differ diff --git a/2-ui/1-document/3-dom-console/1@2x.png b/2-ui/1-document/3-dom-console/1@2x.png new file mode 100755 index 00000000..64d717a2 Binary files /dev/null and b/2-ui/1-document/3-dom-console/1@2x.png differ diff --git a/2-ui/1-document/3-dom-console/2.png b/2-ui/1-document/3-dom-console/2.png new file mode 100755 index 00000000..91dac732 Binary files /dev/null and b/2-ui/1-document/3-dom-console/2.png differ diff --git a/2-ui/1-document/3-dom-console/2@2x.png b/2-ui/1-document/3-dom-console/2@2x.png new file mode 100755 index 00000000..cfa45822 Binary files /dev/null and b/2-ui/1-document/3-dom-console/2@2x.png differ diff --git a/2-ui/1-document/3-dom-console/3.png b/2-ui/1-document/3-dom-console/3.png new file mode 100755 index 00000000..4ccaa0b2 Binary files /dev/null and b/2-ui/1-document/3-dom-console/3.png differ diff --git a/2-ui/1-document/3-dom-console/3@2x.png b/2-ui/1-document/3-dom-console/3@2x.png new file mode 100755 index 00000000..bc8a66d6 Binary files /dev/null and b/2-ui/1-document/3-dom-console/3@2x.png differ diff --git a/2-ui/1-document/3-dom-console/article.md b/2-ui/1-document/3-dom-console/article.md new file mode 100644 index 00000000..526bc747 --- /dev/null +++ b/2-ui/1-document/3-dom-console/article.md @@ -0,0 +1,83 @@ +# Работа с DOM из консоли + +Исследовать и изменять DOM можно с помощью инструментов разработки, встроенных в браузер. Посмотрим средства для этого на примере Google Chrome. +[cut] + +## Доступ к элементу + +Откройте документ [losi.html](/files/tutorial/browser/dom/dom-tree/losi.html) и, в инструментах разработчика, перейдите во вкладку Elements. + +Чтобы проанализировать любой элемент: +
            +
          • Выберите его во вкладке Elements.
          • +
          • ...Либо внизу вкладки Elements есть лупа, при нажатии на которую можно выбрать элемент кликом.
          • +
          • ...Либо, что обычно удобнее всего, просто кликните на нужном месте на странице правой кнопкой и выберите в меню "Проверить Элемент".
          • +
          + + + +**В более новой версии Chrome расположение элементов управления может отличаться, но происходящее всё равно должно быть понятно.** + +Справа будет различная информация об элементе: +
          +
          Computed Style
          +
          Итоговые свойства CSS элемента, которые он приобрёл в результате применения всего каскада стилей, включая внешние CSS-файлы и атрибут `style`.
          +
          Style
          +
          Каскад стилей, применённый к элементу. Каждое стилевое правило отдельно, здесь же можно менять стили кликом.
          +
          Metrics
          +
          Размеры элемента.
          +
          ...
          +
          И еще некоторые реже используемые вкладки, которые станут понятны по мере изучения DOM.
          +
          + +[warn header="DOM не полностью отображается в инструментах!"] +Во вкладке Elements не отображаются пробельные узлы. Это сделано для удобства просмотра. На самом-то деле они есть. + +Зато в Elements могут быть видны псевдоэлементы, такие как `::before`, `::after`. Это также сделано для удобства, в DOM их нет. +[/warn] + + +## Ссылки $0 $1... + +Зачастую бывает нужно выбрать элемент DOM и сделать с ним что-то на JavaScript. + +Находясь во вкладке Elements, откройте консоль нажатием Esc (или перейдите на вкладку Console). + +**Последний элемент, выбранный во вкладке Elements, доступен в консоли как `$0`, предыдущий -- `$1` и так далее.** + +Запустите на элементе команду, которая делает его красным: + +```js +$0.style.backgroundColor = 'red'; +``` + +Пример, как это может выглядеть в браузере: + + + +**Итак, можно выделить элемент и редактировать его в консоли. Есть и обратный путь: любой элемент из консоли можно открыть во вкладке Elements.** + +Чтобы показать элемент из JS-переменной во вкладке Elements: +
            +
          1. Выведите эту переменную в консоли, например при помощи `console.log`.
          2. +
          3. Кликните на нём правой кнопкой мыши.
          4. +
          5. Выберите соответствующий пункт меню.
          6. +
          + + + +**Таким образом можно легко перемещаться из Elements в консоль и обратно.** + +Выбрал элемент -- попробовал на нём JavaScript -- посмотрел что получилось в DOM. + +## Ещё методы консоли + +Для поиска элементов в консоли есть два специальных метода: +
            +
          • `$$("div.my")` -- возвращает все элементы по CSS-селектору.
          • +
          • `$("div.my")` -- возвращает один элемент по CSS-селектору, если их много, то только первый.
          • +
          + +Более полная документация по методам консоли доступна здесь: [Console API Reference для Chrome](https://developers.google.com/chrome-developer-tools/docs/console-api) и здесь: [Command Line API для Firebug](https://getfirebug.com/wiki/index.php/Command_Line_API), а также на [firebug.ru](http://firebug.ru). + +Другие браузеры реализуют почти такой же функционал, как и Chrome/Firebug. diff --git a/2-ui/1-document/4-traversing-dom/1-dom-children/index.html b/2-ui/1-document/4-traversing-dom/1-dom-children/index.html new file mode 100755 index 00000000..e8e9d60f --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/1-dom-children/index.html @@ -0,0 +1,18 @@ + + + + +
          Пользователи:
          +
            +
          • Маша
          • +
          • Вовочка
          • +
          + + + + + + + diff --git a/2-ui/1-document/4-traversing-dom/1-dom-children/solution.md b/2-ui/1-document/4-traversing-dom/1-dom-children/solution.md new file mode 100644 index 00000000..e04fff6a --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/1-dom-children/solution.md @@ -0,0 +1,34 @@ +# HEAD + +Два способа: + +```js +document.documentElement.children[0] +document.documentElement.firstChild +``` + +Второй способ работает, так как пробелы перед `` игнорируются. + +# UL + +Два варианта: + +```js +document.getElementById('user-list') +document.body.children[1] +``` + +Предпочтителен второй вариант, так как он не зависит от положении узла в документе. Мало ли, вдруг мы решим вставить какую-то ещё информацию перед ним. + +# LI + +Два варианта: + +```js +document.getElementById('user-list').children[1]; +document.body.children[1].children[1]; // LI +``` + +Если комментарий переместить между элементами списка, то в IE8- он станет одним из `children`, в результате последний код станет работать некорректно. + +Чтобы это обойти, нужно либо не ставить комментарии в те места HTML, где планируются такие выборки, либо использовать другие методы поиска в HTML, которые мы рассмотрим [далее](/searching-elements-dom). \ No newline at end of file diff --git a/2-ui/1-document/4-traversing-dom/1-dom-children/solution.view/index.html b/2-ui/1-document/4-traversing-dom/1-dom-children/solution.view/index.html new file mode 100755 index 00000000..e8e9d60f --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/1-dom-children/solution.view/index.html @@ -0,0 +1,18 @@ + + + + +
          Пользователи:
          +
            +
          • Маша
          • +
          • Вовочка
          • +
          + + + + + + + diff --git a/2-ui/1-document/4-traversing-dom/1-dom-children/task.md b/2-ui/1-document/4-traversing-dom/1-dom-children/task.md new file mode 100644 index 00000000..92c74b0a --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/1-dom-children/task.md @@ -0,0 +1,17 @@ +# DOM children + +[importance 5] + +Для страницы: + +```html + +``` + +
            +
          • Напишите код, который получит элемент `HEAD`.
          • +
          • Напишите код, который получит `UL`.
          • +
          • Напишите код, который получит второй `LI`. Будет ли ваш код работать в IE8-, если комментарий переместить *между* элементами `LI`?
          • +
          + +[edit src="solution" task/] \ No newline at end of file diff --git a/2-ui/1-document/4-traversing-dom/2-has-childnodes/solution.md b/2-ui/1-document/4-traversing-dom/2-has-childnodes/solution.md new file mode 100644 index 00000000..73176af3 --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/2-has-childnodes/solution.md @@ -0,0 +1,27 @@ +Вначале нерабочие способы, которые могут прийти на ум: + +```js +if (!elem) { .. } +``` + +Это не работает, так как `elem` всегда есть, и является объектом. Так что проверка `if (elem)` всегда верна, вне зависимости от того, есть ли у `elem` потомки. + +```js +if (!elem.childNodes) { ... } +``` + +Тоже не работает, так как псевдо-массив `childNodes` всегда существует. Он может быть пуст или непуст, но он всегда является объектом, так что проверка `if (elem.childNodes)` всегда верна. + +Несколько рабочих способов: + +```js +if (!elem.childNodes.length) { ... } + +if (!elem.firstChild) { ... } + +if (!elem.lastChild) { ... } +``` + +Последний - самый короткий. + +P.S. Также есть метод [hasChildNodes](https://developer.mozilla.org/en-US/docs/Web/API/Node.hasChildNodes), который позволяет вызовом `elem.hasChildNodes()` определить наличие детей. Он работает так же, как проверка `elem.childNodes.length != 0`. \ No newline at end of file diff --git a/2-ui/1-document/4-traversing-dom/2-has-childnodes/task.md b/2-ui/1-document/4-traversing-dom/2-has-childnodes/task.md new file mode 100644 index 00000000..d7aa46f6 --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/2-has-childnodes/task.md @@ -0,0 +1,13 @@ +# Проверка существования детей + +[importance 5] + +Придумайте самый короткий код для проверки, пуст ли элемент `elem`. + +"Пустой" -- значит нет дочерних узлов, даже текстовых. + +```js +if (/*...ваш код проверки elem... */) { узел elem пуст } +``` + +Что написать в условии `if` ? \ No newline at end of file diff --git a/2-ui/1-document/4-traversing-dom/3-navigation-links-which-null/solution.md b/2-ui/1-document/4-traversing-dom/3-navigation-links-which-null/solution.md new file mode 100644 index 00000000..ca09870b --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/3-navigation-links-which-null/solution.md @@ -0,0 +1,10 @@ +
            +
          1. Да, верно, с оговоркой. Элемент `elem.lastChild` последний, у него нет правого соседа. + +**Оговорка:** `elem.lastChild.nextSibling` выдаст ошибку если `elem` не имеет детей. +
          2. +
          3. Нет, неверно, это может быть текстовый узел. Значением `elem.children[0]` является первый узел-элемент, перед ним может быть текст. + +Аналогично предыдущему случаю, если у `elem` нет детей-элементов -- будет ошибка. +
          4. + diff --git a/2-ui/1-document/4-traversing-dom/3-navigation-links-which-null/task.md b/2-ui/1-document/4-traversing-dom/3-navigation-links-which-null/task.md new file mode 100644 index 00000000..993d7d8b --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/3-navigation-links-which-null/task.md @@ -0,0 +1,9 @@ +# Вопрос по навигационным ссылкам + +[importance 5] + +Если `elem` -- это произвольный узел DOM... + +Верно ли, что `elem.lastChild.nextSibling` всегда `null`? + +Верно ли, что `elem.children[0].previousSibling` всегда `null` ? \ No newline at end of file diff --git a/2-ui/1-document/4-traversing-dom/article.md b/2-ui/1-document/4-traversing-dom/article.md new file mode 100644 index 00000000..6cc5884b --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/article.md @@ -0,0 +1,374 @@ +# Ссылки между DOM-элементами + +Для того, чтобы изменить узел DOM или его содержимое, нужно сначала его получить. + +Доступ к DOM начинается с объекта `document`. Из него можно добраться до любых узлов. + +[cut] + +Вот картина, которая, с небольшими дополниниями, будет обсуждаться в этой главе: + + + +## Корень: documentElement и body + +Войти в "корень" дерева можно двумя путями. + +
            +
            `` = `document.documentElement`
            +
            Первая точка входа -- `document.documentElement`. Это свойство ссылается на DOM-объект для тега `HTML`.
            +
            `` = `document.body`
            +
            Вторая точка входа -- `document.body`, который соответствует тегу `BODY`.
            +
            + +Оба варианта отлично работают. Но есть одна тонкость: **`document.body` может быть равен `null`**. + +Например, при доступе к `document.body` в момент обработки тега `HEAD`, то `document.body = null`. Это вполне логично, потому что `BODY` еще не существует. + +**Нельзя получить доступ к элементу, которого еще не существует в момент выполнения скрипта.** + +В следующем примере, первый `alert` выведет `null`: + +```html + + + + + + + + + + + + +``` + +[smart header="В DOM активно используется `null`"] + +В мире DOM в качестве значения "нет такого элемента" или "узел не найден" используется не `undefined`, а `null`. +[/smart] + +## document.getElementById или просто id + +Если элементу назначен специальный атрибут `id`, то можно получить его прямо по переменной с именем из значения `id`. + +Например: + +```html + +
            +
            Элемент
            +
            + + +``` + +По стандарту, значение `id` должно быть уникально, то есть в документе может быть только один элемент с данным `id`. + +Это поведение соответствует [стандарту](http://www.whatwg.org/specs/web-apps/current-work/#dom-window-nameditem). Оно существует, в первую очередь, для совместимости, как осколок далёкого прошлого и не очень приветствуется, поскольку использует глобальные переменные. Браузер пытается помочь нам, смешивая пространства имён JS и DOM, но при этом возможны конфликты. + +**Более правильной и общепринятой практикой является доступ к элементу вызовом `document.getElementById("идентификатор")`.** + +Например: + +```html + +
            Выделим этот элемент
            + + +``` + +**Далее я изредка буду использовать прямое обращение через переменную в примерах, чтобы было меньше букв и проще было понять происходящее. Но предпочтительным методом является `document.getElementById`.** + +## Дочерние элементы + +Здесь и далее мы будем использовать два принципиально разных термина. + +
              +
            • **Дочерние элементы (или дети)** -- элементы, которые лежат *непосредственно* внутри данного. Например, внутри `` обычно лежат `` и ``.
            • +
            • **Потомки** -- все элементы, которые лежат внутри данного, вместе с их детьми, детьми их детей и так далее. То есть, всё поддерево.
            • +
            + +### childNodes + +Псевдо-массив `childNodes` хранит все дочерние элементы, включая текстовые. + +Пример ниже последовательно выведет дочерние элементы `document.body`: + +```html + +``` + +Во всех браузерах, кроме старых IE, `document.body.childNodes[0]` это текстовый узел из пробелов, а `DIV` -- второй потомок: `document.body.childNodes[1]`. + +В IE8- не создаются пустые текстовые узлы, поэтому там дети начнутся с `DIV`. + +Как вы думаете, почему перечисление узлов в примере выше заканчивается на `SCRIPT` ? Неужели под скриптом нет пробельного узла? + +[hide] +Конечно, потому что пробельный узел будет в итоговом документе, но его еще нет на момент выполнения скрипта. +[/hide] + +[warn header="Коллекция только для чтения!"] +Все навигационные свойства, которые перечислены в этой главе -- только для чтения. Нельзя просто заменить элемент присвоением `childNodes[i] = ...`. В частности, методы массива для `childNodes` тоже не поддерживаются, поэтому это свойство и называют "коллекцией". + +Изменения DOM осуществляется другими методами, которые мы рассмотрим далее, все навигационные ссылки при этом обновляются автоматически. +[/warn] + + + +### children + +А что если текстовые узлы нам не интересны? + +**Свойство `children`, перечисляет только дочерние узлы-элементы, соответствующие тегам.** + +Модифицируем предыдущий пример, применив `children` вместо `childNodes`. + +Теперь он будет выводить не все узлы, а только узлы-элементы: + +```html + +``` + +[warn header="В IE8- в `children` присутствуют узлы-комментарии"] +С точки зрения стандарта это ошибка, но IE8- также включает в `children` узлы, соответствующие HTML-комментариям. + +Это может привести к сюрпризам при использовании свойства `children`, поэтому HTML-комментарии либо убирают либо используют функцию (или фреймворк, к примеру, jQuery), который автоматически офильтрует их. +[/warn] + +### Коллекции -- не массивы + +Коллекции, которые возвращают методы поиска, не являются массивами. + +У них нет методов массива, таких как `join`, `pop`, `forEach` и т.п. + +Например, этот пример выполнится с ошибкой: + +```js +//+ run +var elems = document.documentElement.childNodes; + +*!* +elems.forEach(function(elem) { // нет такого метода! +*/!* + /* ... */ +}); +``` + +Можно для перебора коллекции использовать обычный цикл `for(var i=0; i +
          5. Применить метод массива через `call/apply`: + +```js +//+ run +var elems = document.documentElement.childNodes; + +*!* +[].forEach.call(elems, function(elem) { +*/!* + alert(elem); // HEAD, текст, BODY +}); +``` + +
          6. +
          7. При помощи [Array.prototype.slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) сделать из коллекции массив. + +Обычно вызов `arr.slice(a, b)` делает новый массив и копирует туда элементы `arr` с индексами от `a` до `b-1` включительно. Если же вызвать его без аргументов `arr.slice()`, то он делает новый массив и копирует туда все элементы `arr`. + +Это работает и для коллекции: + +```js +//+ run +var elems = document.documentElement.childNodes; +*!* +elems = Array.prototype.slice.call(elems); // теперь elems - массив +*/!* + +elems.forEach(function(elem) { + alert(elem.tagName); // HEAD, текст, BODY +}); +``` + +
          8. +
          + +[warn header="Нельзя перебирать коллекцию через `for..in`"] +Ранее мы говорили, что не рекомендуется использовать для перебора массива цикл `for..in`. + +**Коллекции -- наглядный пример, почему нельзя. Они похожи на массивы, но у них есть свои свойства и методы, которых в массивах нет.** + +При запуске этого кода вы увидите, что `alert` сработает не 3, а целых 5 раз! + +```js +//+ run +var elems = document.documentElement.childNodes; + +for(var key in elems) { + alert(key); // 0, 1, 2, length, item +} +``` + +Цикл `for..in` вывел не только ожидаемые индексы `0`, `1`, `2`, по которым лежат узлы в коллекции, но и свойства `length` (в коллекции оно enumerable), а также функцию `item(n)` -- она никогда не используется, возвращает `n-й` элемент коллекции, старый аналог обращения по индексу `[n]`. + +В реальном коде мы хотим перебирать только элементы, поэтому желательно использовать `for(var i=0; i +
          ...
          +
            ...
          +
          ...
          + +``` + +DOM-дерево будет таким (внутренности `div` и `ul` скрыты): + +
          + + +Если бы пробельных узлов не было, например, в IE8, то была бы такая картина ссылок: + + + +С другой стороны, так как пробельные узлы, всё же, есть, то `body.firstChild` и `body.lastChild` будут указывать как раз на них, то есть на первый и последний `#text`. + +Всегда верны равенства: + +```js +body.firstChild === body.childNodes[0] +body.lastChild === body.childNodes[body.childNodes.length-1] +``` + +## parentNode, previousSibling и nextSibling + +Ранее мы смотрели свойства для доступа к детям. Теперь рассмотрим ссылки для доступа вверх и в стороны от узла. + +
            +
          • Свойство `parentNode` ссылается на родительский узел.
          • +
          • Свойства `previousSibling` и `nextSibling` дают доступ к левому и правому соседу.
          • +
          + +Ниже изображены ссылки между `BODY` и его потомками для документа: + +```html + +``` + +Ссылки (пробельные узлы обозначены решеткой `#`): + + + +## Ссылки для элементов (IE9+) + +Все современные браузеры, включая IE9+, поддерживают дополнительные ссылки: + +
            +
          • `firstElementChild` -- первый потомок-элемент (`=children[0]`)
          • +
          • `lastElementChild` -- последний потомок-элемент (`=children[children.length-1]`)
          • +
          • `nextElementSibling` -- правый брат-элемент
          • +
          • `previousElementSibling` -- левый брат-элемент
          • +
          • `parentElement` -- родительский узел-элемент, по факту отличается от `parentNode` только тем, что `document.documentElement.parentElement = null`, то есть `document` он элементом не считает.
          • +
          + +Любые другие узлы, кроме элементов, просто игнорируются. + +Например: + +```html + + + firstElementChild:
          ...
          + + lastElementChild: ... + + + + +``` + +Современные браузеры также поддерживают дополнительные интерфейсы для обхода DOM c фильтром по узлам: `NodeIterator`, `TreeFilter` и `TreeWalker`. Они были утверждены аж в 2000-м году, однако на практике оказались неудобными, и потому практически не применяются. Вы можете почитать о них в стандарте [DOM 2 Traversal](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-Filters). + + +## Итого + +Сверху в DOM можно войти либо через `document.documentElement` (тег `HTML`), либо через `document.body` (тег `BODY`). + +По элементу DOM можно получить всех соседей через ссылки: + +
          +
          `childNodes`, `children`
          +
          Список дочерних узлов.
          +
          `firstChild`, `lastChild`
          +
          Первый и последний потомки
          +
          `parentNode`
          +
          Родительский узел
          +
          `previousSibling`, `nextSibling`
          +
          Соседи влево-вправо
          +
          + +Все навигационные ссылки доступны только для чтения и поддерживаются автоматически. + +Свойства-коллекции, хотя и имеют индексы, а также `length`, не являются массивами. Поэтому их и называют "псевдомассивами", "коллекциями" или "списками". Далее мы встретимся с кучей других полезных коллекций, которые тоже не будут массивами. + +В современных браузерах, включая IE9+, реализованы дополнительные свойства, работающие только для элементов: + +
            +
          • `firstElementChild` -- первый потомок-элемент
          • +
          • `lastElementChild` -- последний потомок-элемент
          • +
          • `nextElementSibling` -- правый брат-элемент
          • +
          • `previousElementSibling` -- левый брат-элемент
          • +
          + +Картинка только со ссылками для элементов: + + + +
            +
          • `children*` -- единственное свойство из списка, поддерживаемое IE8-.
          • +
          + +[libs] +d3 +domtree +[/libs] \ No newline at end of file diff --git a/2-ui/1-document/4-traversing-dom/children.png b/2-ui/1-document/4-traversing-dom/children.png new file mode 100755 index 00000000..1feaf4d5 Binary files /dev/null and b/2-ui/1-document/4-traversing-dom/children.png differ diff --git a/2-ui/1-document/4-traversing-dom/index.html b/2-ui/1-document/4-traversing-dom/index.html new file mode 100755 index 00000000..7f5319b2 --- /dev/null +++ b/2-ui/1-document/4-traversing-dom/index.html @@ -0,0 +1,13 @@ + + + + +
          Начало
          + +
            +
          • Содержание
          • +
          + +
          Конец
          + + diff --git a/2-ui/1-document/4-traversing-dom/navigation-elements.png b/2-ui/1-document/4-traversing-dom/navigation-elements.png new file mode 100755 index 00000000..59e387ae Binary files /dev/null and b/2-ui/1-document/4-traversing-dom/navigation-elements.png differ diff --git a/2-ui/1-document/4-traversing-dom/navigation-elements@2x.png b/2-ui/1-document/4-traversing-dom/navigation-elements@2x.png new file mode 100755 index 00000000..bca8a83d Binary files /dev/null and b/2-ui/1-document/4-traversing-dom/navigation-elements@2x.png differ diff --git a/2-ui/1-document/4-traversing-dom/navigation.png b/2-ui/1-document/4-traversing-dom/navigation.png new file mode 100755 index 00000000..1f4d6a46 Binary files /dev/null and b/2-ui/1-document/4-traversing-dom/navigation.png differ diff --git a/2-ui/1-document/4-traversing-dom/navigation@2x.png b/2-ui/1-document/4-traversing-dom/navigation@2x.png new file mode 100755 index 00000000..d55257de Binary files /dev/null and b/2-ui/1-document/4-traversing-dom/navigation@2x.png differ diff --git a/2-ui/1-document/4-traversing-dom/siblings2.png b/2-ui/1-document/4-traversing-dom/siblings2.png new file mode 100755 index 00000000..a9a529ab Binary files /dev/null and b/2-ui/1-document/4-traversing-dom/siblings2.png differ diff --git a/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/solution.md b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/solution.md new file mode 100644 index 00000000..099f5e81 --- /dev/null +++ b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/solution.view/index.html b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/solution.view/index.html new file mode 100755 index 00000000..8b548755 --- /dev/null +++ b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/solution.view/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + +
          1:12:13:14:15:1
          1:22:23:24:25:2
          1:32:33:34:35:3
          1:42:43:44:45:4
          1:52:53:54:55:5
          + + + diff --git a/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/source.view/index.html b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/source.view/index.html new file mode 100755 index 00000000..b1fa9345 --- /dev/null +++ b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/source.view/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + +
          1:12:13:14:15:1
          1:22:23:24:25:2
          1:32:33:34:35:3
          1:42:43:44:45:4
          1:52:53:54:55:5
          + + + diff --git a/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/task.md b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/task.md new file mode 100644 index 00000000..7a1235ce --- /dev/null +++ b/2-ui/1-document/5-traversing-tables/1-select-diagonal-cells/task.md @@ -0,0 +1,16 @@ +# Выделите ячейки по диагонали + +[importance 5] + +Напишите код, который выделит все ячейки в таблице по диагонали. + +Вам нужно будет получить из таблицы `table` все диагональные `td` и выделить их, используя код: + +```js +td.style.backgroundColor = 'red'; +``` + +Должно получиться так: +[iframe src="solution"] + +[edit src="source" task/] diff --git a/2-ui/1-document/5-traversing-tables/article.md b/2-ui/1-document/5-traversing-tables/article.md new file mode 100644 index 00000000..c3d84b5e --- /dev/null +++ b/2-ui/1-document/5-traversing-tables/article.md @@ -0,0 +1,58 @@ +# Особые ссылки для таблиц + +У конкретных элементов DOM могут быть свои дополнительные ссылки для большего удобства навигации. + +В этой главе мы рассмотрим таблицу, так как это важный частный случай и просто для примера. Также дополнительные ссылки есть у форм, но работу с ними лучше осваивать отдельно. +[cut] + +В списке ниже выделены наиболее полезные: + +
          +
          `TABLE`
          +
          +
            +
          • **`table.rows`** -- список строк `TR` таблицы.
          • +
          • `table.caption/tHead/tFoot` -- ссылки на элементы таблицы `CAPTION`, `THEAD`, `TFOOT`.
          • +
          • `table.tBodies` -- список элементов таблицы `TBODY`, по спецификации их может быть несколько.
          • +
          +
          `THEAD/TFOOT/TBODY`
          +
          +
            +
          • `tbody.rows` -- список строк `TR` секции.
          • +
          +
          `TR`
          +
          +
            +
          • **`tr.cells`** -- список ячеек `TD/TH`
          • +
          • **`tr.sectionRowIndex`** -- номер строки в текущей секции `THEAD/TBODY`
          • +
          • `tr.rowIndex` -- номер строки в таблице
          • +
          +
          +
          `TD/TH`
          +
          +
            +
          • **`td.cellIndex`** -- номер ячейки в строке
          • +
          +
          +
          + +Пример использования: + +```html + + + + +
          один два
          три четыре
          + + +``` + +Спецификация: [HTML5: tabular data](http://www.w3.org/TR/html5/tabular-data.html). + +Даже если эти свойства не нужны вам прямо сейчас, имейте их в виду на будущее, когда понадобится пройтись по таблице. + diff --git a/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/console-innerhtml.png b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/console-innerhtml.png new file mode 100755 index 00000000..0f58e790 Binary files /dev/null and b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/console-innerhtml.png differ diff --git a/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/console-innerhtml@2x.png b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/console-innerhtml@2x.png new file mode 100755 index 00000000..a7b48c6c Binary files /dev/null and b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/console-innerhtml@2x.png differ diff --git a/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/solution.md b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/solution.md new file mode 100644 index 00000000..87aba7d3 --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/solution.md @@ -0,0 +1,18 @@ +**Однозначно правильный ответ невозможен.** + +В консоли не выводятся пробельные узлы. Если перед `

          ` находится пробельный узел, то будет `undefined`, а если нет -- то текст внутри `

          `. + +Пример с `undefined`: + +```html + + +

          Привет, мир!

          + + + +``` + +Если убрать из него перевод строки перед `

          `, то было бы `"Привет, мир!"`. \ No newline at end of file diff --git a/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/task.md b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/task.md new file mode 100644 index 00000000..e5bab48c --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/1-console-firstchild-innerhtml/task.md @@ -0,0 +1,12 @@ +# Что выведет код в консоли? + +[importance 5] + +В браузере Chrome открыт HTML-документ. + +Вы зашли во вкладку Elements и видите такую картинку: + + +В настоящий момент выбран элемент ``. + +Что выведет код `$0.firstChild.innerHTML` в консоли? \ No newline at end of file diff --git a/2-ui/1-document/6-basic-dom-node-properties/2-lastchild-nodetype-inline/solution.md b/2-ui/1-document/6-basic-dom-node-properties/2-lastchild-nodetype-inline/solution.md new file mode 100644 index 00000000..2a0e3976 --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/2-lastchild-nodetype-inline/solution.md @@ -0,0 +1,16 @@ +Небольшой подвох -- в том, что во время выполнения скрипта последним тегом является `SCRIPT`. Браузер не может обработать страницу дальше, пока не выполнит скрипт. + +Так что результат будет `1` (узел-элемент). + +```html + + + + + + + +``` + diff --git a/2-ui/1-document/6-basic-dom-node-properties/2-lastchild-nodetype-inline/task.md b/2-ui/1-document/6-basic-dom-node-properties/2-lastchild-nodetype-inline/task.md new file mode 100644 index 00000000..481d07f0 --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/2-lastchild-nodetype-inline/task.md @@ -0,0 +1,17 @@ +# В инлайн скрипте lastChild.nodeType + +[importance 5] + +Что выведет скрипт на этой странице? + +```html + + + + + + +``` + diff --git a/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/solution.md b/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/solution.md new file mode 100644 index 00000000..4fb95abf --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/solution.md @@ -0,0 +1,101 @@ +# Менее эффективный вариант + +Цикл по правым соседям в поисках узла-элемента: + +```html + +
          Первый
          + +

          Второй

          + + +``` + +# Более эффективный вариант + +Все браузеры, кроме IE<9, поддерживают свойство `nextElementSibling`. Воспользуемся этим. + +```html + +
          Первый
          + +

          Второй

          + + +``` + +В выделенном фрагменте мы проверяем поддержку этого свойства. Если оно поддерживается, то равно либо элементу-соседу, либо `null`, если такого соседа нет. В любом случае это не `undefined`. + +Если же оно не поддерживается, то производим те же вычисления, что в предыдущем решении. + +# Ещё более эффективный вариант + +Поддержка свойства `nextElementSibling` в браузере либо есть, либо её нет. Зачем проверять её для каждого элемента? Можно сделать это один раз и запомнить результат. А можно поступить ещё лучше -- определить функцию по-разному, в зависимости от того, поддерживается это свойство или нет. + +```html + +
          Первый
          + +

          Второй

          + + +``` + diff --git a/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/source.view/index.html b/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/source.view/index.html new file mode 100755 index 00000000..c3b7fc56 --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/source.view/index.html @@ -0,0 +1,20 @@ + + + + + + + +
          Первый
          + +

          Второй

          + + + + + \ No newline at end of file diff --git a/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/task.md b/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/task.md new file mode 100644 index 00000000..c8b1777e --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/3-find-next-element/task.md @@ -0,0 +1,27 @@ +# Найти следующий элемент + +[importance 5] + +Напишите функцию `getNextElement(elem)`, которая возвращает следующий за `elem` узел-элемент (игнорирует остальные узлы). + + +Пример: + +```html +
          Первый
          + +

          Второй

          + + +``` + +[edit src="source" /] + +P.S. Функция должна работать максимально эффективно и учитывать возможности современных браузеров. \ No newline at end of file diff --git a/2-ui/1-document/6-basic-dom-node-properties/4-tag-in-comment/solution.md b/2-ui/1-document/6-basic-dom-node-properties/4-tag-in-comment/solution.md new file mode 100644 index 00000000..f791b12f --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/4-tag-in-comment/solution.md @@ -0,0 +1,19 @@ +Ответ: **`BODY`**. + +```html + + +``` + +Происходящее по шагам: +
            +
          1. Заменяем содержимое `` на комментарий. Он будет иметь вид <!--BODY-->, так как `body.tagName == "BODY"`. Как мы помним, свойство `tagName` в HTML всегда находится в верхнем регистре.
          2. +
          3. Этот комментарий теперь является первым и единственным потомком `body.firstChild`.
          4. +
          5. Получим значение `data` для комментария `body.firstChild`. Оно равно содержимому узла для всех узлов, кроме элементов. Содержимое комментария: `"BODY"`.
          6. +
          \ No newline at end of file diff --git a/2-ui/1-document/6-basic-dom-node-properties/4-tag-in-comment/task.md b/2-ui/1-document/6-basic-dom-node-properties/4-tag-in-comment/task.md new file mode 100644 index 00000000..2e42bbc4 --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/4-tag-in-comment/task.md @@ -0,0 +1,16 @@ +# Тег в комментарии + +[importance 3] + +Что выведет этот код? + +```html + +``` + diff --git a/2-ui/1-document/6-basic-dom-node-properties/article.md b/2-ui/1-document/6-basic-dom-node-properties/article.md new file mode 100644 index 00000000..35c86c74 --- /dev/null +++ b/2-ui/1-document/6-basic-dom-node-properties/article.md @@ -0,0 +1,503 @@ +# Свойства узлов: тип, тег и содержимое + +Для того, чтобы работать с DOM-узлами, нужно знать, какие свойства у них есть, и как изменение тех или иных свойств повлияет на документ. + +В этой главе мы познакомимся с основными, самыми важными свойствами, которые отвечают за тип DOM-узла, тег и содержимое. + +[cut] + +## Классы, иерархия DOM + +Самое главное различие между DOM-узлами -- разные узлы являются объектами различных классов. + +Основной объект в ней: [Node](http://dom.spec.whatwg.org/#interface-node), от которого наследуют остальные: + + + +На рисунке выше изображены основные объекты иерархии: +
          + +Узнать класс узла очень просто -- достаточно привести его к строке, к примеру, вывести: + +```js +//+ run +alert( document.body ); // [object HTMLBodyElement] +``` + +**Разные классы могут иметь разные свойства и методы, детальное описание которых можно найти в [спецификации](http://www.whatwg.org/specs/web-apps/current-work/multipage/).** + +Например, [The input element](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#the-input-element) описывает класс, соответствующий ``, включая [interface HTMLInputElement](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#htmlinputelement), который нас как раз и интересует. + +Вот из него выдержка: + +```js +interface HTMLInputElement : HTMLElement { + attribute DOMString accept; + attribute DOMString alt; + attribute DOMString autocomplete; + attribute boolean autofocus; + ... + attribute DOMString value; + ... + void select(); + ... +} +``` + +Здесь мы видим, что у всех `` есть свойства `accept`, `alt`, `autocomplete`, `autofocus`, `value`, метод `select()` и так далее. В зависимости от типа `` чаще используются те или иные свойства. Позже мы к ним вернёмся. + +**Далее в этом разделе мы сосредоточимся на самых главных свойствах узлов DOM, без разницы какого типа.** + +## Тип: nodeType + +Тип узла содержится в его свойстве `nodeType`. + +Как правило, мы работаем всего с двумя типами узлов: +
            +
          • Элемент.
          • +
          • Текстовый узел.
          • +
          + +На самом деле типов узлов гораздо больше. Строго говоря, их 12, и они описаны в спецификации с древнейших времён, см.
          DOM Уровень 1: + +```js +interface Node { + // NodeType + const unsigned short ELEMENT_NODE = 1; + const unsigned short ATTRIBUTE_NODE = 2; + const unsigned short TEXT_NODE = 3; + const unsigned short CDATA_SECTION_NODE = 4; + const unsigned short ENTITY_REFERENCE_NODE = 5; + const unsigned short ENTITY_NODE = 6; + const unsigned short PROCESSING_INSTRUCTION_NODE = 7; + const unsigned short COMMENT_NODE = 8; + const unsigned short DOCUMENT_NODE = 9; + const unsigned short DOCUMENT_TYPE_NODE = 10; + const unsigned short DOCUMENT_FRAGMENT_NODE = 11; + const unsigned short NOTATION_NODE = 12; + ... +} +``` + +Нам важны номера основных типов. + +**Самые важные -- это `ELEMENT_NODE` под номером 1 и `TEXT_NODE` под номером 3.** + +Например, выведем все узлы-потомки `document.body`, *являющиеся элементами*: + +```html + + +
          Читатели:
          +
            +
          • Вася
          • +
          • Петя
          • +
          + + + + + +``` + +Это свойство можно только читать, изменить его невозможно. + + +## Тег: nodeName и tagName + +Существует целых два свойства: `nodeName` и `tagName`, которые содержат название(тег) элемента узла. + +**Название HTML-тега всегда находится в верхнем регистре.** + +Например, для `document.body`: + +```js +//+ run +alert( document.body.nodeName ); // BODY +alert( document.body.tagName ); // BODY +``` + +[smart header="Когда `nodeName` не в верхнем регистре?"] +У браузера есть два режима обработки документа: HTML и XML-режим. Обычно используется режим HTML, XML-режим включается, когда браузер получает XML-документ через `XMLHttpRequest`(технология AJAX) или при наличии заголовка `Content-Type: application/xml+xhtml`. + +В XML-режиме сохраняется регистр и `nodeName` может выдать "body" или даже "bOdY" -- в точности как указано в документе. +[/smart] + +### Какая разница между tagName и nodeName ? + +Разница отражена в названиях свойств, но неочевидна. + +
            +
          • Свойство `tagName` есть только у элементов `Element` (в IE8- также у комментариев, но это ошибка в браузере).
          • +
          • Свойство `nodeName` определено для любых узлов `Node`, для элементов оно равно `tagName`, а для не-элементов обычно содержит строку с типом узла.
          • +
          + +Таким образом, при помощи `tagName` мы можем работать только с элементами, а `nodeName` может что-то сказать и о других типах узлов. + +Например, сравним `tagName` и `nodeName` на примере узла-комментария и объекта `document`: + +```html + + + + + +``` + +При работе только с узлами элементов имеет смысл использовать `tagName` -- так короче :). + +## innerHTML: содержимое элемента + +Свойство `innerHTML` описано в спецификации HTML 5 -- embedded content. + +Оно позволяет получить HTML-содержимое элемента в виде строки. В `innerHTML` можно и читать и писать. + +Пример выведет на экран все содержимое `document.body`, а затем заменит его на другое: + +```html + + +

          Параграф

          +
          Div
          + + + + +``` + +Значение, возвращаемое `innerHTML` -- всегда валидный HTML-код. При записи можно попробовать записать что угодно, но браузер исправит ошибки: + +```html + + + + + + +``` + +Свойство `innerHTML` -- одно из самых часто используемых. + +### Тонкости innerHTML + +`innerHTML` не так прост, как может показаться, и таит в себе некоторые тонкости, которые могут сбить с толку новичка, а иногда и опытного программиста. + +Ознакомьтесь с ними. Даже если этих сложностей у вас *пока* нет, эта информация отложится где-то в мозге и поможет, когда проблема появится. + +[warn header="Для таблиц в IE9- -- `innerHTML` только для чтения"] +В Internet Explorer версии 9 и ранее, innerHTML доступно только для чтения для элементов `COL`, `COLGROUP`, `FRAMESET`, `HEAD`, `HTML`, `STYLE`, `TABLE`, `TBODY`, `TFOOT`, `THEAD`, `TITLE`, `TR`. + +В частности, **в IE9- нельзя присвоить `innerHTML` табличным элементам, кроме ячеек (`TD/TH`).** +[/warn] + +[warn header="Добавление `innerHTML+=` осуществляет перезапись"] +Синтаксически, можно добавить текст к `innerHTML` через `+=`: + +```js +chatDiv.innerHTML += "
          Привет !
          "; +chatDiv.innerHTML += "Как дела?"; +``` + +На практике этим следует пользоваться с большой осторожностью, так как фактически происходит не добавление, а перезапись: +
            +
          1. Удаляется старое содержание
          2. +
          3. На его место становится новое значение `innerHTML`.
          4. +
          + +Так как новое значение записывается с нуля, то **все изображения и другие ресурсы будут перезагружены**. В примере выше вторая строчка перезагрузит `smile.gif`, который был до неё. Если в `chatDiv` много текста, то эта перезагрузка будет очень заметна. + +Есть и другие побочные эффекты, например если существующий текст был выделен мышкой, то в большинстве браузеров это выделение пропадёт. + +К счастью, есть и другие способы добавить содержимое, не использующие `innerHTML`. +[/warn] + +[warn header="Скрипты не выполняются"] +**Если в `innerHTML` есть тег `script` -- он не будет выполнен.** + +Пример -- ниже: + +```html + +
          + + +``` + +В примере закрывающий тег `` разбит на две строки, т.к. иначе браузер подумает, что это конец скрипта. Вставленный скрипт не выполнится. + +Исключение -- IE9-, в нем вставляемый скрипт выполняются, если у него есть атрибут `defer` (это нестандартная возможность): + +```html + +
          + + +``` + +[/warn] + +[warn header="IE8- обрезает `style` и `script` в начале `innerHTML`"] +Если в начале `innerHTML` находятся стили ` + + + + + + + + + + + diff --git a/2-ui/1-document/8-searching-elements-dom/3-set-class-links/task.md b/2-ui/1-document/8-searching-elements-dom/3-set-class-links/task.md new file mode 100644 index 00000000..feb2bfca --- /dev/null +++ b/2-ui/1-document/8-searching-elements-dom/3-set-class-links/task.md @@ -0,0 +1,26 @@ +# Поставьте класс ссылкам + +[importance 3] + +Сделайте желтыми внешние ссылки, добавив им класс `external`. + +Все относительные ссылки и абсолютные с доменом `javascript.ru` считаются внутренними. + +```html + + + +``` + +Результат: +[iframe border=1 height=180 src="solution"] + diff --git a/2-ui/1-document/8-searching-elements-dom/article.md b/2-ui/1-document/8-searching-elements-dom/article.md new file mode 100644 index 00000000..dc2eeead --- /dev/null +++ b/2-ui/1-document/8-searching-elements-dom/article.md @@ -0,0 +1,303 @@ +# Поиск: getElement* и querySelector* + +Прямая навигация от родителя к потомку удобна, если элементы рядом. А если нет? + +Для этого в DOM есть дополнительные методы поиска. +[cut] +## getElementById + +Мы рассматривали этот способ ранее. Он работает, если у DOM-элемента есть атрибут `id`. + +**В тех случаях, когда `id` не уникален, вызов `document.getElementById(id)` может возвратить любой из элементов с данным `id`.** + +## getElementsByTagName + +Метод `elem.getElementsByTagName(tag)` ищет все элементы с заданным тегом `tag` внутри элемента `elem` и возвращает их в виде списка. + +Регистр тега не имеет значения. + +Можно искать и в элементе и в документе: + +```js +// получить все div-элементы +var elements = document.getElementsByTagName('div'); +``` + +Найдём все элементы `input` внутри таблицы: + +```html + + + + + + + + +
          Ваш возраст: + + + +
          + + +``` + +**Можно получить все элементы, передав звездочку `'*'` вместо тега:** + +```js +// получить все элементы документа +var allElems = document.getElementsByTagName('*'); +``` + +Если хочется получить только один элемент -- можно указать индекс сразу же: + +```js +var element = document.getElementsByTagName('input')*!*[0]*/!* +``` + +[warn header="Не забываем про букву `\"s\"`!"] +Одна из самых частых ошибок начинающих (впрочем, иногда и не только) -- это забыть букву `"s", то есть пробовать вызывать метод `getElementByTagName` вместо getElement**s**ByTagName. + +Буква `"s"` не нужна там, где элемент только один, то есть в `getElementById`, в остальных методах она обязательна. +[/warn] + +[warn header="Возвращается коллекция, а не элемент"] +Другая частая ошибка -- это код вида: + +```js +document.getElementsByTagName('input').value = 5; +``` + +То есть, вместо элемента присваивают значение коллекции. Работать такое не будет. + +Коллекцию нужно или перебрать в цикле или получить элемент по номеру и уже ему присваивать `value`, например так: + +```js +document.getElementsByTagName('input')[0].value = 5; +``` + +[/warn] + +## getElementsByName + +Вызов `document.getElementsByName(name)` позволяет получить все элементы с данным атрибутом `name`. + +Например, все элементы с именем `age`: + +```js +var elems = document.getElementsByName('age'); +``` + +До появления стандарта HTML5 этот метод возвращал только те элементы, в которых предусмотрена поддержка атрибута `name`, в частности: `iframe`, `a`, `input` и другими. + +В современных браузерах тег не имеет значения, но старое поведение можно увидеть, попробовав пример ниже в IE10 и до версии 10: + +```html + + + +
          + + +``` + +**В IE9- метод не найдёт элементы, для которых в стандарте нет атрибута `name`.** + +## getElementsByClassName + +Вызов `elem.getElementsByClassName(className)` возвращает коллекцию элементов с классом `className`. Находит элемент и в том случае, если у него несколько классов, а искомый - один из них. + +Поддерживается всеми современными браузерами, кроме IE8-. + +Например: + +```html + +
          Статья
          +
          Длинная статья
          + + +``` + +Как и `getElementsByTagName`, этот метод может быть вызван и в контексте DOM-элемента и в контексте документа. + + +## querySelectorAll [#querySelectorAll] + +Вызов `elem.querySelectorAll(cssQuery)` возвращает все элементы внутри `elem`, удовлетворяющие CSS-селектору `cssQuery`. + +Он работает во всех современных браузерах, включая IE9+. Также работает и в IE8, но с некоторыми ограничениями: +
            +
          1. IE8 должен быть именно в режиме IE8, а не в режиме совместимости.
          2. +
          3. В IE8 синтаксис `cssQuery` должен соответствовать не CSS 3, а CSS 2.1. Не так мощно, конечно, но этого хватает для большинства случаев.
          4. +
          + +Следующий запрос получает все элементы `LI`, которые являются последними потомками своих `UL`. Это будет работать и в IE8. + +```html + +
            +
          • Этот
          • +
          • тест
          • +
          +
            +
          • полностью
          • +
          • пройден
          • +
          + +``` + +## querySelector [#querySelector] + +То же самое, что `elem.querySelectorAll(cssQuery)`, но возвращает только первый элемент. + +Фактически, эквивалентен `elem.querySelectorAll(cssQuery)[0]`, но быстрее, так как ищутся не все элементы, а только первый. + +## matches + +Вызов [elem.matches(css)](http://dom.spec.whatwg.org/#dom-element-matches) проверяет, удовлетворяет ли `elem` селектору `css`. + +Он возвращает `true` либо `false`. + +Ранее в спецификации он назывался `matchesSelector`, и большинство браузеров поддерживают его под этим старым именем, либо с префиксами. + +Например: + +```html + +
          + ... +
          + + +``` + +Не поддерживается в IE8-. + +## XPath в современных браузерах + +Для поиска в XML-документах существует язык запросов XPath. + +Он очень мощный, во многом мощнее CSS, но сложнее. Например, запрос для поиска элементов `H2`, содержащих текст `"XPath"`, будет выглядеть так: `//h2[contains(., "XPath")]`. + +**Все современные браузеры, кроме IE, поддерживают XPath с синтаксисом, близким к [описанному в MDN](https://developer.mozilla.org/en/XPath).** + +Найдем заголовки с текстом `XPath` в текущем документе: + +```js +//+ run +var result = document.evaluate("//h2[contains(., 'XPath')]", document.documentElement, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + +for (var i=0; i + +Метод +Ищет по.. +Ищет внутри элемента? +Поддержка + +`getElementById` +`id` +- +везде + + +`getElementsByName` +`name` +- +везде + + +`getElementsByTagName` +тег или `'*'` +✔ +везде + + +`getElementsByClassName` +классу +✔ +кроме IE8- + + +`querySelector` +CSS-селектор +✔ +кроме IE7- + + +`querySelectorAll` +CSS-селектор +✔ +кроме IE7- + + + +Если браузеры IE7- не нужны, то в 95% ситуаций достаточно и одного `querySelector(All)`. + +Кроме того: +
            +
          • Есть метод `elem.matchesSelector(css)`, который проверяет, удовлетворяет ли элемент CSS-селектору. Он поддерживается большинством браузеров в префиксной форме (`ms`, `moz`, `webkit`).
          • +
          • XPath поддерживается большинством браузеров, кроме IE, даже 9й версии. Кроме того, как правило, `querySelector` удобнее. Поэтому он используется редко.
          • +
          + + + + + + diff --git a/2-ui/1-document/9-searching-elements-internals/1-collection-length-after-remove/solution.md b/2-ui/1-document/9-searching-elements-internals/1-collection-length-after-remove/solution.md new file mode 100644 index 00000000..730cb5d7 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/1-collection-length-after-remove/solution.md @@ -0,0 +1,48 @@ +# Ответ на первый вопрос + +Ответ: 0, пустая коллекция. + +```html + + + +``` + +Это потому, что все элементы из `BODY` удаляются, а коллекция - *живая*. + +# Ответ на второй вопрос + +Ответ на второй вопрос зависит от браузера. В большинстве браузеров будет 3, коллекция не изменилась, так как она теперь привязана не к `BODY`, а к элементу, на котором идёт поиск, т.е. к `menu`. + +Но элемент `menu` находится в переменной, и поэтому должен быть жив, а значит и его дети тоже. Но некоторые браузеры (IE10) используют агрессивный подход при работе с памятью и очищают все элементы, кроме тех, которые непосредственно хранятся в переменных. + +Поэтому результат кода ниже в большинстве браузеров: `3`, а в IE10: `0`. + +```html + + + +``` + diff --git a/2-ui/1-document/9-searching-elements-internals/1-collection-length-after-remove/task.md b/2-ui/1-document/9-searching-elements-internals/1-collection-length-after-remove/task.md new file mode 100644 index 00000000..ff9eeb93 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/1-collection-length-after-remove/task.md @@ -0,0 +1,35 @@ +# Длина коллекции после удаления элементов + +[importance 5] + +Вот небольшой документ: + +```html + +``` + +1. Что выведет следующий код (простой вопрос)? + +```js +var lis = document.body.getElementsByTagName('li'); + +document.body.innerHTML = ""; + +alert(lis.length); +``` + +2. А такой код (вопрос посложнее)? + +```js +var menu = document.getElementById('menu'); +var lis = menu.getElementsByTagName('li'); + +document.body.innerHTML = ""; + +alert(lis.length); +``` + diff --git a/2-ui/1-document/9-searching-elements-internals/2-compare-elements-count/solution.md b/2-ui/1-document/9-searching-elements-internals/2-compare-elements-count/solution.md new file mode 100644 index 00000000..8a39f575 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/2-compare-elements-count/solution.md @@ -0,0 +1,5 @@ +Значение `aList1` изменится, потому что `getElementsByTagName` - *живая* коллекция. Она автоматически дополнится новым элементом `a` и ее длина увеличится на 1. + +А вот `querySelector`, наоборот, возвращает статичный список узлов. Он ссылается на те же самые элементы, что бы не происходило с документом. Поэтому длина `aList2.length` останется неизменной. + + diff --git a/2-ui/1-document/9-searching-elements-internals/2-compare-elements-count/task.md b/2-ui/1-document/9-searching-elements-internals/2-compare-elements-count/task.md new file mode 100644 index 00000000..fd311082 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/2-compare-elements-count/task.md @@ -0,0 +1,13 @@ +# Сравнение количества элементов + +[importance 5] + +Для любого документа сделаем следующее: + +```js +var aList1 = document.getElementsByTagName('a'), +var aList2 = document.querySelectorAll('a'); +``` + +Что произойдёт со значениями `aList1.length`, `aList2.length`, если в документе вдруг появится ещё одна ссылка `...`? + diff --git a/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/solution.md b/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/solution.md new file mode 100644 index 00000000..76d0cd59 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/solution.md @@ -0,0 +1,32 @@ +Для бенчмаркинга будем использовать функцию `bench(f, times)`, которая запускает функцию `f` `times` раз и возвращает разницу во времени: + +```js +function bench(f, times) { + var d = new Date(); + for(var i=0; i + + + + + + + +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          + + + + + + + diff --git a/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/source.view/index.html b/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/source.view/index.html new file mode 100755 index 00000000..3e989571 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/source.view/index.html @@ -0,0 +1,32 @@ + + + + + + + +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          +

          1

          2

          3

          4

          5

          6

          7

          8

          9

          + + + + + + diff --git a/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/task.md b/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/task.md new file mode 100644 index 00000000..aa952850 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/3-benchmark-search-dom/task.md @@ -0,0 +1,11 @@ +# Бенчмаркинг методов поиска в DOM + +[importance 2] + +Какой метод поиска элементов работает быстрее: `getElementsByTagName(tag)` или `querySelectorAll(tag)`? + +Напишите код, который измеряет разницу между ними. + +[edit src="source" task/] + +*P.S. В задаче есть подвох, все не так просто. Если разница больше 10 раз -- вы решили ее неверно. Тогда подумайте, почему такое может быть.* diff --git a/2-ui/1-document/9-searching-elements-internals/4-get-second-li/solution.md b/2-ui/1-document/9-searching-elements-internals/4-get-second-li/solution.md new file mode 100644 index 00000000..930f4120 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/4-get-second-li/solution.md @@ -0,0 +1,21 @@ +Можно так: + +```js +var li = ul.getElementsByTagName('li')[1]; +``` + +Или так: + +```js +var li = ul.querySelector('li:nth-child(2)'); +``` + +Оба этих вызова будут перебирать детей `UL` и остановят перебор на найденном элементе. + +А вот так -- браузер найдет все элементы, а затем выберет второй. Это дольше: + +```js +var li = ul.querySelectorAll('li')[1]; +``` + +На практике разница в производительности будет видна только для действительно больших списков, либо при частом выполнении запроса. Браузер перебирает элементы весьма шустро. diff --git a/2-ui/1-document/9-searching-elements-internals/4-get-second-li/task.md b/2-ui/1-document/9-searching-elements-internals/4-get-second-li/task.md new file mode 100644 index 00000000..6dbecfe4 --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/4-get-second-li/task.md @@ -0,0 +1,16 @@ +# Получить второй LI + +[importance 5] + +Есть длинный список `ul`: + +```html +
            +
          • ...
          • +
          • ...
          • +
          • ...
          • + ... +
          +``` + +Как наиболее эффективно получить второй `LI`? diff --git a/2-ui/1-document/9-searching-elements-internals/article.md b/2-ui/1-document/9-searching-elements-internals/article.md new file mode 100644 index 00000000..d82f25bd --- /dev/null +++ b/2-ui/1-document/9-searching-elements-internals/article.md @@ -0,0 +1,168 @@ +# Внутреннее устройство поисковых методов + +Несмотря на схожесть в синтаксисе, поисковые методы `get*` и `querySelector*` внутри устроены очень по-разному. + +Если вы хотите действительно глубоко понимать, что происходит, то посмотрите эту главу. Если нет -- её можно пропустить. + +## document.getElementById(id) + +Браузер поддерживает у себя внутреннее соответствие `id -> элемент`. Поэтому нужный элемент возвращается сразу. Это очень быстро. + +## elem.querySelector(query), elem.querySelectorAll(query) + +Чтобы найти элементы, удовлетворяющие поисковому запросу, браузер не использует никаких сложных структур данных. + +Он просто перебирает все подэлементы внутри элемента `elem`(или по всему документу, если вызов в контексте документа) и проверяет каждый элемент на соответствие запросу `query`. + +Вызов `querySelector` прекращает перебор после первого же найденного элемента, а `querySelectorAll` собирает найденные элементы в "псевдомассив": внутреннюю структуру данных, по сути аналогичную массиву JavaScript. + +Этот перебор происходит очень быстро, так как осуществляется непосредственно движком браузера, а не JavaScript-кодом. + +Оптимизации: +
            +
          • В случае поиска по ID: `elem.querySelector('#id')`, большинство браузеров оптимизируют поиск, используя вызов `getElementById`.
          • +
          • Последние результаты поиска сохраняются в кеше. Но это до тех пор, пока документ как-нибудь не изменится.
          • +
          + + +## elem.getElementsBy*(...) + +Результаты поиска `getElementsBy*` -- живые! При изменении документа -- изменяется и результат запроса. + +Например, найдём все `div` при помощи `querySelectorAll` и `getElementsByTagName`, а потом изменим документ: + +```html + +
          + +``` + +Как видно, длина коллекции, найденной через `querySelectorAll`, осталась прежней. А длина списка, возвращённого `getElementsByTagName`, изменилась. + +Дело в том, что результат запросов `getElementsBy*` -- это не массив, а специальный объект, имеющий тип NodeList или HTMLCollection. Он похож на массив, так как имеет нумерованные элементы и длину, но внутри это не готовый список, а "живой поисковой запрос". + +Собственно поиск выполняется только при обращении к элементам списка или к его длине. + +## Алгоритмы getElementsBy* + +Поиск `getElementsBy*` наиболее сложно сделать эффективно, так как его результат -- "живая" коллекция, она должна быть всегда актуальной для текущего состояния документа. + +```js +var elems = document.getElementsByTagName('div'); +alert( elems[0] ); +*!* +// изменили документ +*/!* +alert( elems[0] ); // результат может быть уже другой +``` + +Можно искать заново при каждой попытке получить элемент из `elems`. Тогда результат будет всегда актуален, но поиск будет работать уж слишком медленно. Да и зачем? Ведь, скорее всего, документ не поменялся. + +**Чтобы производительность `getElementsBy*` была достаточно хорошей, активно используется кеширование результатов поиска.** + +Для этого есть два основных способа: назовём их условно "Способ Firefox" (Firefox, IE) и "Способ WebKit" (Chrome, Safari, Opera). + +Для примера, рассмотрим поиск в произвольном документе, в котором есть 1000 элементов `div`. + +Посмотрим, как будут работать браузеры, если нужно выполнить следующий код: + +```js +// вместо document может быть любой элемент +var elems = document.getElementsByTagName('div'); +alert( elems[0] ); +alert( elems[995] ); +alert( elems[500] ); +alert( elems.length ); +``` + +
          +
          Способ Firefox
          +
          Перебрать подэлементы `document.body` в порядке их появления в поддереве. Запоминать *все найденные элементы* во внутренней структуре данных, чтобы при повторном обращении обойтись без поиска. + +Разбор действий браузера при выполнении кода выше: +
          1. Браузер создаёт пустую "живую коллекцию" `elems`. Пока ничего не ищет.
          2. +
          3. Перебирает элементы, пока не найдёт первый `div`. Запоминает его и возвращает.
          4. +
          5. Перебирает элементы дальше, пока не найдёт элемент с индексом `995`. Запоминает все найденные.
          6. +
          7. Возвращает ранее запомненный элемент с индексом `500`, без дополнительного поиска!
          8. +
          9. Продолжает обход поддерева с элемента, на котором остановился (`995`) и до конца. Запоминает найденные элементы и возвращает их количество.
          10. +
          +
          +
          Способ WebKit
          +
          Перебирать подэлементы `document.body`. Запоминать только один, *последний найденный*, элемент, а также, по окончании перебора -- длину коллекции. + +Здесь кеширование используется меньше. + +Разбор действий браузера по строкам: +
          1. Браузер создаёт пустую "живую коллекцию" `elems`. Пока ничего не ищет.
          2. +
          3. Перебирает элементы, пока не найдёт первый `div`. Запоминает его и возвращает.
          4. +
          5. Перебирает элементы дальше, пока не найдёт элемент с индексом `995`. Запоминает его и возвращает.
          6. +
          7. Браузер запоминает только последний найденный, поэтому не помнит об элементе `500`. Нужно найти его перебором поддерева. Этот перебор можно начать либо с начала -- вперед по поддереву, 500й по счету) либо с элемента `995` -- назад по поддереву, 495й по счету. Так как назад разница в индексах меньше, то браузер выбирает второй путь и идёт от 995го назад 495 раз. Запоминает теперь уже 500й элемент и возвращает его.
          8. +
          9. Продолжает обход поддерева с 500го (не 995го!) элемента и до конца. Запоминает число найденных элементов и возвращает его.
          10. +
          +
          +
          + +Основное различие -- в том, что Firefox запоминает все найденные, а Webkit -- только последний. Таким образом, "метод Firefox" требует больше памяти, но гораздо эффективнее при повторном доступе к предыдущим элементам. + +А "метод Webkit" ест меньше памяти и при этом работает не хуже в самом важном и частом случае -- последовательном переборе коллекции, без возврата к ранее выбранным. + +**Запомненные элементы сбрасываются при изменениях DOM.** + +Документ может меняться. При этом, если изменение может повлиять на результаты поиска, то запомненные элементы необходимо сбросить. Например, добавление нового узла `div` сбросит запомненные элементы коллекции `elem.getElementsByTagName('div')`. + +Сбрасывание запомненных элементов при изменении документа выполняется интеллектуально. +
            +
          1. Во-первых, при добавлении элемента будут сброшены только те коллекции, которые могли быть затронуты обновлением. Например, если в документе есть два независимых раздела `
            `, и поисковая коллекция привязана к первому из них, то при добавлении во второй -- она сброшена не будет. + +Если точнее -- будут сброшены все коллекции, привязанные к элементам вверх по иерархии от непосредственного родителя нового `div` и выше, то есть такие, которые потенциально могли измениться. И только они. +
          2. +
          3. Во-вторых, если добавлен только `div`, то не будут сброшены запомненные элементы для поиска по другим тегам, например `elem.getElementsByTagName('a')`.
          4. +
          5. ...И, конечно же, не любые изменения DOM приводят к сбросу кешей, а только те, которые могут повлиять на список. Если где-то добавлен новый атрибут -- с поиском по тегу ничего не произойдёт.
          6. +
          + +Прочие поисковые методы, такие как `getElementsByClassName` тоже сбрасывают кеш при изменениях интеллектуально. + +Разницу в алгоритмах поиска легко "пощупать". Посмотрите сами: + +```html + + +``` + +В примере выше первый цикл проходит элементы последовательно. А второй -- идет по шагам: один с начала, потом один с конца, потом ещё один с начала, ещё один -- с конца, и так далее. + +Количество обращений к элементам одинаково. + +
            +
          • В браузерах, которые запоминают все найденные (Firefox, IE) -- скорость будет одинаковой.
          • +
          • В браузерах, которые запоминают только последний (Webkit) -- разница будет порядка 100 и более раз, так как браузер вынужден бегать по дереву при каждом запросе заново.
          • +
          + diff --git a/2-ui/1-document/index.md b/2-ui/1-document/index.md new file mode 100644 index 00000000..ebc4cf6f --- /dev/null +++ b/2-ui/1-document/index.md @@ -0,0 +1,3 @@ +# Документ и объекты страницы + +При помощи JavaScript получаем и меняем существующие элементы на странице, а также создаём новые. \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.md new file mode 100644 index 00000000..ed38f26c --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Решение задачи[/edit] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.view/index.html new file mode 100755 index 00000000..4f218abe --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/solution.view/index.html @@ -0,0 +1,18 @@ + + + + + + + + + +
          Текст
          + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/source.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/source.view/index.html new file mode 100755 index 00000000..97a6221e --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/source.view/index.html @@ -0,0 +1,16 @@ + + + + + + + + + +
          Текст
          + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/task.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/task.md new file mode 100644 index 00000000..26da5029 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/1-hide-other/task.md @@ -0,0 +1,10 @@ +# Спрятать при клике + +[importance 5] + +Используя JavaScript, сделайте так, чтобы при клике на кнопку исчезал элемент с `id="hide"`. + +Демо: +[iframe border=1 src="solution"] + +[edit src="source" task/] diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/2-hide-self-onclick/solution.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/2-hide-self-onclick/solution.md new file mode 100644 index 00000000..4a7904c0 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/2-hide-self-onclick/solution.md @@ -0,0 +1,7 @@ +Решение задачи заключается в использовании `this` в обработчике. + +```html + + +``` + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/2-hide-self-onclick/task.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/2-hide-self-onclick/task.md new file mode 100644 index 00000000..685e1092 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/2-hide-self-onclick/task.md @@ -0,0 +1,8 @@ +# Спрятаться + +[importance 5] + +Создайте кнопку, при клике на которую, она будет скрывать сама себя. + +Как эта: + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/2.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/2.html new file mode 100755 index 00000000..a12e5cc4 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/2.html @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/3-which-handlers-run/solution.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/3-which-handlers-run/solution.md new file mode 100644 index 00000000..7e8e00fa --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/3-which-handlers-run/solution.md @@ -0,0 +1,16 @@ +Ответ: будет выведено `1` и `2`. + +Первый обработчик сработает, так как он не убран вызовом `removeEventListener`. Для удаления обработчика нужно передать в точности ту же функцию (ссылку на нее), что была назначена, а в коде передается такая же с виду функция, но, тем не менее, это другой объект. + +Для того, чтобы удалить функцию-обработчик, нужно где-то сохранить ссылку на неё, например так: + +```js +function handler() { + alert("1"); +} + +button.addEventListener("click", handler, false); +button.removeEventListener("click", handler, false); +``` + +Обработчик `button.onclick` сработает независимо и в дополнение к назначенному в `addEventListener`. diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/3-which-handlers-run/task.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/3-which-handlers-run/task.md new file mode 100644 index 00000000..ebe89653 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/3-which-handlers-run/task.md @@ -0,0 +1,18 @@ +# Какие обработчики сработают? + +[importance 5] + +В переменной `button` находится кнопка. + +Изначально обработчиков на ней нет. + +Что будет выведено при клике после выполнения кода? + +```js +button.addEventListener("click", function() { alert("1"); }, false); + +button.removeEventListener("click", function() { alert("1"); }, false); + +button.onclick = function() { alert(2); }; +``` + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/solution.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/solution.md new file mode 100644 index 00000000..85a53f25 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/solution.md @@ -0,0 +1,74 @@ +# Структура HTML/CSS + +Для начала, зададим структуру HTML/CSS. + +Меню является отдельным графическим компонентом, его лучше поместить в единый DOM-элемент. + +Элементы меню с точки зрения семантики являются списком `UL/LI`. Заголовок должен быть отдельным кликабельным элементом. + +Получаем структуру: + +```html + +``` + +Для заголовка лучше использовать именно `SPAN`, а не `DIV`, так как `DIV` постарается занять 100% ширины, и мы не сможем ловить `click` только на тексте: + +```html + +
          [Сладости (нажми меня)!]
          +``` + +...А `SPAN` -- это элемент с `display: inline`, поэтому он занимает ровно столько места, сколько занимает текст внутри него: + +```html + +[Сладости (нажми меня)!] +``` + +Раскрытие/закрытие делайте путём добавления/удаления класса `.menu-open` к меню, которые отвечает за стрелочку и отображение `UL`. + +# CSS + +CSS для меню: + +```css +.menu ul { + margin: 0; + list-style: none; + padding-left: 20px; + + display: none; +} + +.menu .title { + padding-left: 16px; + font-size: 18px; + cursor: pointer; + + background: url(...arrow-right.png) left center no-repeat; +} +``` + +Если же меню раскрыто, то есть имеет класс `.menu-open`, то стрелочка слева заголовка меняется и список детей показывается: + +```css +.menu-open .title { + background: url(...arrow-down.png) left center no-repeat; +} + +.menu-open ul { + display: block; +} +``` + +Теперь сделайте JavaScript. + +[edit src="solution"]Полное решение в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/solution.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/solution.view/index.html new file mode 100755 index 00000000..6c707e44 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/solution.view/index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/source.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/source.view/index.html new file mode 100755 index 00000000..d8b1bc96 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/source.view/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + +Сладости (нажми меня)! +
            +
          • Торт
          • +
          • Пончик
          • +
          • Пирожное
          • +
          + + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/task.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/task.md new file mode 100644 index 00000000..b67dda4e --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/4-sliding-menu/task.md @@ -0,0 +1,11 @@ +# Раскрывающееся меню + +[importance 5] + +Создайте меню, которое раскрывается/сворачивается при клике: + +[iframe border=1 height=100 src="solution"] + +HTML/CSS исходного документа, возможно, понадобится изменить. + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.md new file mode 100644 index 00000000..acead631 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.md @@ -0,0 +1,26 @@ +# Алгоритм решения + +
            +
          1. Разработать структуру HTML/CSS. Позиционировать кнопку внутри сообщения.
          2. +
          3. Найти все кнопки
          4. +
          5. Присвоить им обработчики
          6. +
          7. Обработчик будет ловить событие на кнопке и удалять соответствующий элемент.
          8. +
              + +# Вёрстка + +Исправьте HTML/CSS, чтобы кнопка была в нужном месте сообщения. Кнопку лучше сделать как `div`, а картинка --- будет его `background`. Это более правильно, чем `img`, т.к. в данном случае картинка является *оформлением кнопки*, а оформление должно быть в CSS. + +Расположить кнопку справа можно при помощи `position: relative` для `pane`, а для кнопки `position: absolute + right/top`. Так как `position: absolute` вынимает элемент из потока, то кнопка может перекрыть текст заголовка. Чтобы этого не произошло, можно добавить `padding-right` к заголовку. + +Потенциальным преимуществом способа с `position` по сравнению с `float` в данном случае является возможность поместить элемент кнопки в HTML *после текста*, а не до него. + +# Обработчики + +Для того, чтобы получить кнопку из контейнера, можно найти все `IMG` в нём и выбрать из них кнопку по `className`. На каждую кнопку можно повесить обработчик. + +# Решение + +[edit src="solution"]Решение в песочнице[/edit] + +Для поиска элементов `span` с нужным классом в нём используется `getElementsByTagName` с фильтрацией. К сожалению, это единственный способ, доступный в IE 6,7. Если же эти браузеры вам не нужны, то гораздо лучше -- искать элементы при помощи `querySelector` или `getElementsByClassName`. \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.view/index.html new file mode 100755 index 00000000..6fe31c05 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.view/index.html @@ -0,0 +1,67 @@ + + + + + + + + + + +
              +
              +

              Лошадь

              +

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

              + +
              +
              +

              Осёл

              +

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

              + +
              +
              +

              Корова, а также пара слов о диком быке, о волах и о тёлках.

              +

              Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.

              + +
              +
              + + + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.view/messages.css b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.view/messages.css new file mode 100755 index 00000000..696daed0 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/solution.view/messages.css @@ -0,0 +1,32 @@ +body { + margin: 10px auto; + width: 470px; +} +h3 { + margin: 0; + padding-bottom: .3em; + padding-right: 20px; + font-size: 1.1em; +} +p { + margin: 0; + padding: 0 0 .5em; +} +.pane { + background: #edf5e1; + padding: 10px 20px 10px; + border-top: solid 2px #c4df9b; + position: relative; +} + +.remove-button { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + display: block; + background: url(delete.gif) no-repeat; + width: 16px; + height: 16px; +} + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/source.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/source.view/index.html new file mode 100755 index 00000000..830beb0b --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/source.view/index.html @@ -0,0 +1,28 @@ + + + + + + + + + +Картинка для кнопки удаления: + +
              +
              +

              Лошадь

              +

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

              +
              +
              +

              Осёл

              +

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

              +
              +
              +

              Корова, а также пара слов о диком быке, о волах и о тёлках.

              +

              Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.

              +
              +
              + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/source.view/messages.css b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/source.view/messages.css new file mode 100755 index 00000000..d459af80 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/source.view/messages.css @@ -0,0 +1,18 @@ +body { + margin: 10px auto; + width: 470px; +} +h3 { + margin: 0; + padding-bottom: .3em; + font-size: 1.1em; +} +p { + margin: 0; + padding: 0 0 .5em; +} +.pane { + background: #edf5e1; + padding: 10px 20px 10px; + border-top: solid 2px #c4df9b; +} diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/task.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/task.md new file mode 100644 index 00000000..a62cc2bf --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/5-hide-message/task.md @@ -0,0 +1,12 @@ +# Спрятать сообщение + +[importance 5] + +Есть список сообщений. Добавьте каждому сообщению по кнопке для его скрытия. + +Результат: +[iframe src="solution"] + +Как лучше отобразить кнопку справа-сверху: через `position:absolute` или `float`? + +[edit src="source" task/] diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/carousel1.png b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/carousel1.png new file mode 100755 index 00000000..6520a756 Binary files /dev/null and b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/carousel1.png differ diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/carousel2.png b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/carousel2.png new file mode 100755 index 00000000..fba478e2 Binary files /dev/null and b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/carousel2.png differ diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.md new file mode 100644 index 00000000..55c01c24 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.md @@ -0,0 +1,28 @@ +# HTML/CSS +Лента изображений должна быть оформлена как список, согласно принципам семантической вёрстки. + +Нужно стилизовать его так, чтобы он был длинной лентой, из которой внешний `DIV` вырезает нужную часть для просмотра: + + + +Чтобы список был длинный и элементы не переходили вниз, ему ставится `width: 9999px`, а элементам, соответственно, `float:left`. + +[warn header="Не используйте display:inline"] +Элементы с `display:inline` имеют дополнительные отступы для возможных "хвостов букв". + +В частности, для `img` нужно поставить в стилях явно `display:block`, чтобы пространства под ними не оставалось. +[/warn] + +При прокрутке UL сдвигается назначением `margin-left`: + + + +У внешнего `DIV` фиксированная ширина, поэтому "лишние" изображения обрезаются. + +Снаружи окошка находятся стрелки и внешний контейнер. + +Реализуйте эту структуру, и к ней прикручивайте обработчики, которые меняют `ul.style.marginLeft`. + +# Полное решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.view/index.html new file mode 100755 index 00000000..b248fbd3 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.view/index.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.view/style.css b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.view/style.css new file mode 100755 index 00000000..d430e776 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/solution.view/style.css @@ -0,0 +1,52 @@ +body { + padding: 10px +} + +.carousel { + position: relative; + width: 398px; + padding: 10px 40px; + border: 1px solid #CCC; + border-radius: 15px; + background: #eee; +} +.carousel img { + width: 130px; + height: 130px; + display: block; /* если не поставить block, в ряде браузеров будет inline -> лишнее пространтсво вокруг элементов */ +} +.carousel .arrow { + position: absolute; + top: 57px; + padding: 3px 8px 8px 9px; + background: #ddd; + border-radius: 15px; + font-size: 24px; + color: #444; + text-decoration: none; +} + +.carousel .arrow:hover { + background: #ccc; +} +.carousel .left-arrow { + left: 7px; +} +.carousel .right-arrow { + right: 7px; +} +.gallery { + width: 390px; + overflow: hidden; +} +.gallery ul { + height: 130px; + width: 9999px; + margin: 0; + padding: 0; + list-style: none; +} + +.gallery ul li { + float: left; +} diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/source.view/index.html b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/source.view/index.html new file mode 100755 index 00000000..31a895a4 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/source.view/index.html @@ -0,0 +1,42 @@ + + + + + + + + +
                +
              • +
              • +
              • +
              • +
              • +
              • +
              • +
              • +
              • +
              • +
              + + + + + + + diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/task.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/task.md new file mode 100644 index 00000000..8a236e81 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/6-carousel/task.md @@ -0,0 +1,13 @@ +# Карусель + +[importance 5] + +Напишите "Карусель" -- ленту изображений, которую можно листать влево-вправо нажатием на стрелочки. + +[iframe height=200 src="solution"] + +В дальнейшем к ней можно легко добавить анимацию, динамическую подгрузку и другие возможности. + +В этой задаче разработка HTML/CSS-структуры составляет 90% решения. + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/1-introduction-browser-events/article.md b/2-ui/2-events-and-interfaces/1-introduction-browser-events/article.md new file mode 100644 index 00000000..9ca06ad5 --- /dev/null +++ b/2-ui/2-events-and-interfaces/1-introduction-browser-events/article.md @@ -0,0 +1,526 @@ +# Введение в браузерные события + +Для реакции на действия посетителя и внутреннего взаимодействия скриптов существуют *события*. + +*Событие* - это сигнал от браузера о том, что что-то произошло. +[cut] +Существует много видов событий. + +Посмотрим список самых часто используемых, пока просто для ознакомления: + +
              +
              События мыши
              +
              +
                +
              • `click` -- происходит, когда кликнули на элемент левой кнопкой мыши
              • +
              • `contextmenu` -- происходит, когда кликнули на элемент правой кнопкой мыши
              • +
              • `mouseover` -- возникает, когда на элемент наводится мышь
              • +
              • `mousedown` и `mouseup` -- когда кнопку мыши нажали или отжали
              • +
              • `mousemove` -- при движении мыши
              • +
              +
              +
              События на элементах управления
              +
              +
                +
              • `submit` -- посетитель отправил форму `
                `
              • +
              • `focus` -- посетитель фокусируется на элементе, например нажимает на ``
              • +
              +
              Клавиатурные события
              +
              +
                +
              • `keydown` -- когда посетитель нажимает клавишу
              • +
              • `keyup` -- когда посетитель отпускает клавишу
              • +
              +
              +
              События документа
              +
              +
                +
              • `DOMContentLoaded` -- когда HTML загружен и обработан, DOM документа полностью построен и доступен.
              • +
              +
              События CSS
              +
              +
                +
              • `transitionend` -- когда CSS-анимация завершена.
              • +
              +
              + +Также есть и много других событий. + +## Назначение обработчиков событий + +Событию можно назначить обработчик, то есть функцию, которая сработает, как только событие произошло. + +Именно благодаря событиям JavaScript-код может реагировать на действия посетителя. + +Есть несколько способов назначить событию обработчик. Сейчас мы их рассмотрим, начиная от самого простого. + +### Использование атрибута HTML + +Обработчик может быть назначен прямо в разметке, в атрибуте, который называется `on<событие>`. + +Например, чтобы прикрепить `click`-событие к `input` кнопке, можно присвоить обработчик `onclick`, вот так: + +```html + +``` + +При клике мышкой на кнопке выполнится код, указанный в атрибуте `onclick`. + +В действии: + + + +Обратите внимание, для строки *внутри* `alert('Клик!')` используются *одиночные кавычки*, так как сам атрибут находится в двойных. + +Частая ошибка новичков в том, что они забывают, что код находится внутри атрибута. Запись вида `onclick="alert("Клик!")"` не будет работать. Если вам действительно нужно использовать именно двойные кавычки, то это можно сделать, заменив их на `"`: onclick="alert(&quot;Клик!&quot;)". + +Однако, обычно этого не требуется, так как в разметке пишутся только очень простые обработчики. Если нужно сделать что-то сложное, то имеет смысл описать это в функции, и в обработчике вызвать уже её. + +Следующий пример по клику запускает функцию `countRabbits()`. + +```html + + +``` + +Как мы помним, атрибут HTML-тега не чувствителен к регистру, поэтому `ONCLICK` будет работать так же, как `onClick` или `onclick`... Но, как правило, атрибуты пишут в нижнем регистре: `onclick`. + +### Использование свойства DOM-объекта + +Можно назначать обработчик, используя свойство DOM-элемента `on<событие>`. + +Пример установки обработчика `click`: + +```html + + +``` + +В действии: + + + +Если обработчик задан через атрибут, то браузер читает HTML-разметку, создаёт новую функцию из содержимого атрибута и записывает в свойство `onclick`. + +**Обработчик хранится именно в свойстве, а атрибут -- лишь один из способов его инициализации.** + +Эти два примера кода работают одинаково: + +
                +
              1. Только HTML: + +```html + + +``` + +
              2. +
              3. HTML + JS: + +```html + + + +``` + +
              4. +
              + +**Так как свойство, в итоге, одно, то назначить более одного обработчика так нельзя.** + +В примере ниже назначение через JavaScript перезапишет обработчик из атрибута: + +```html + + + +``` + +Кстати, обработчиком можно назначить и уже существующую функцию: + +```js +function sayThanks() { + alert('Спасибо!'); +} + +elem.onclick = sayThanks; +``` + +Если обработчик надоел -- его всегда можно убрать назначением `elem.onclick = null`. + +### Доступ к элементу через this + +Внутри обработчика события `this` ссылается на текущий элемент, то есть на тот, на котором он сработал. + +Это можно использовать, чтобы получить свойства или изменить элемент. + +В коде ниже `button` выводит свое содержимое, используя `this.innerHTML`: + +```html + +``` + +В действии: + + + +### Частые ошибки + +Если вы только начинаете работать с событиями -- обратите внимание на следующие особенности. + +
              +
              Функция должна быть присвоена как `sayThanks`, а не `sayThanks()`.
              +
              + +```js +button.onclick = sayThanks; +``` + +Если добавить скобки, то `sayThanks()` -- будет уже *результат* выполнения функции (а так как в ней нет `return`, то в `onclick` попадёт `undefined`). Нам же нужна именно функция. + +...А вот в разметке как раз скобки нужны: + +```html + +``` + +Это различие просто объяснить. При создании обработчика браузером по разметке, он автоматически создает функцию из его содержимого. Поэтому последний пример -- фактически то же самое, что: + +```js +button.onclick = function() { +*!* + sayThanks(); // содержимое атрибута +*/!* +}; +``` + +
              +
              Используйте именно функции, а не строки.
              +
              +Назначение обработчика строкой `elem.onclick = 'alert(1)'` будет работать, но не рекомендуется, могут быть проблемы при сжатии JavaScript. + +Передавать код в виде строки по меньшей мере странно в языке, который поддерживает Function Expressions, оно здесь доступно только по соображениям совместимости с древними временами. +
              +
              Не используйте `setAttribute`.
              +
              +Такой вызов работать не будет: + +```js +//+ run +// при нажатии на body будут ошибки +// потому что при назначении в атрибут функция будет преобразована в строку +document.body.setAttribute('onclick', function() { alert(1) }); +``` + +
              +
              Регистр свойства имеет значение.
              +
              Свойство называется `onclick`, а не `ONCLICK`.
              +
              + + + + +## Специальные методы + +Фундаментальный недостаток описанных выше способов назначения обработчика -- невозможность повесить *несколько* обработчиков на одно событие. + +Например, одна часть кода хочет при клике на кнопку делать ее подсвеченной, а другая -- выдавать сообщение. Нужно в разных местах два обработчика повесить. + +При этом новый обработчик будет затирать предыдущий. Например, следующий код на самом деле назначает один обработчик -- последний: + +```js +input.onclick = function() { alert(1); } +// ... +input.onclick = function() { alert(2); } // заменит предыдущий обработчик +``` + +Разработчики стандартов достаточно давно это поняли и предложили альтернативный способ назначения обработчиков при помощи специальных методов, который свободен от указанного недостатка. + +### addEventListener и removeEventListener + +Методы `addEventListener` и `removeEventListener` являются современным способом назначить или удалить обработчик, и при этом позволяют использовать сколько угодно любых обработчиков. + +Назначение обработчика осуществляется вызовом `addEventListener` с тремя аргументами: + +```js +element.addEventListener( event, handler, phase ); +``` + +
              +
              `event`
              +
              Имя события, например `click`
              +
              `handler`
              +
              Ссылка на функцию, которую надо поставить обработчиком.
              +
              `phase`
              +
              Фаза, на которой обработчик должен сработать. Этот аргумент мы рассмотрим далее в учебнике. Пока что будем использовать значение `phase = false`, которое нужно в 99% случаев.
              +
              + +Удаление обработчика осуществляется вызовом `removeEventListener`: + +```js +element.removeEventListener( event, handler, phase ); +``` + +[warn header="Удаление требует ту же функцию"] +Для удаления нужно передать именно ту функцию-обработчик которая была назначена. + +Вот так `removeEventListener` не сработает: + +```js +input.addEventListener( "click" , function() {alert('Спасибо!')}, false); +// .... +input.removeEventListener( "click", function() {alert('Спасибо!')}, false); +``` + +Это не одна и та же функция, а две независимо созданные (с одинаковым кодом, но это не важно). + +Вот так правильно: + +```js +function handler() { + alert('Спасибо!'); +} + +input.addEventListener( "click" , handler, false); +// .... +input.removeEventListener( "click", handler, false); +``` + +[/warn] + +**Использование `addEventListener` позволяет добавлять несколько обработчиков на одно событие одного элемента:** + +```html + + + + +``` + +Как видно из примера выше, можно одновременно назначать обработчики и через `onсвойство` (только один) и через `addEventListener`. Однако, во избежание путаницы обычно рекомендуется выбрать один способ. + +[warn header="`addEventListener` работает всегда, а `onсвойство` -- нет"] +У специальных методов есть ещё одно перимущество перед "старой школой". + +Есть некоторые события, которые нельзя назначить через `onсвойство`, но можно через `addEventListener`. + +Например, таково событие `transitionend`, то есть окончание CSS-анимации. В большинстве браузеров оно требует назначения через `addEventListener`. + +При нажатии на кнопку в примере ниже сработает второй обработчик, но не первый. + +```html + + + + + + +``` + +[/warn] + + +## Отличия IE8- + +При работе с событиями в IE8- есть много отличий. Как правило, они формальны -- некое свойство или метод называются по-другому. Начиная с версии 9, также работают и стандартные свойства и методы, которые предпочтительны. + +**В IE8- вместо `addEventListener/removeEventListener` используются свои методы.** + +Назначение обработчика осуществляется вызовом `attachEvent`: + +```js +element.attachEvent( "on"+event, handler); +``` + +Удаление обработчика -- вызовом `detachEvent`: + +```js +element.detachEvent( "on"+event, handler); +``` + +Например: + +```js +function handler() { + alert('Спасибо!'); +} +button.attachEvent( "onclick" , handler) // Назначение обработчика +// .... +button.detachEvent( "onclick", handler) // Удаление обработчика +``` + +Как видите, почти то же самое, только событие должно включать префикс `on` и нет третьего аргумента, который нам пока не нужен. + + +[warn header="У обработчиков, назначенных с `attachEvent`, нет `this`"] +Обработчики, назначенные с `attachEvent` не получают `this`! + +Это важная особенность и подводный камень старых IE. +[/warn] + +### Кроссбраузерный способ назначения обработчиков + +Можно объединить способы для IE<9 и современных браузеров, создав свои методы `addEvent(elem, type, handler)` и `removeEvent(elem, type, handler)`: + +```js +var addEvent, removeEvent; + +if (document.addEventListener) { // проверка существования метода + addEvent = function(elem, type, handler) { + elem.addEventListener(type, handler, false); + }; + removeEvent = function(elem, type, handler) { + elem.removeEventListener(type, handler, false); + }; +} else { + addEvent = function(elem, type, handler) { + elem.attachEvent("on" + type, handler); + }; + removeEvent = function(elem, type, handler) { + elem.detachEvent("on" + type, handler); + }; +} + +... +// использование: +addEvent(elem, "click", function() { alert("Привет"); }); +``` + +Это хорошо работает в большинстве случаев, но у обработчика не будет `this` в IE, потому что `attachEvent` не поддерживает `this`. + +Кроме того, в IE7- есть проблемы с утечками памяти... Но если вам не нужно `this`, и вы не боитесь утечек (как вариант -- не поддерживаете IE7-), то это решение может подойти. + + +## Итого + +Есть три способа назначения обработчиков событий: + +
                +
              1. Атрибут HTML: `onclick="..."`.
              2. +
              3. Свойство: elem.onclick = function.
              4. +
              5. Специальные методы: +
                  +
                • Для IE8-: `elem.attachEvent( on+событие, handler )` (удаление через `detachEvent`).
                • +
                • Для остальных: `elem.addEventListener( событие, handler, false )` (удаление через `removeEventListener`).
                • +
                +
              6. +
              + +Сравнение `addEventListener` и `onclick`: +[compare] ++Некоторые события можно назначить только через `addEventListener`. ++Метод `addEventListener` позволяет назначить много обработчиков на одно событие. +-Обработчик, назначенный через `onclick`, проще удалить или заменить. +-Метод `onclick` кросс-браузерный. +[/compare] + +**Этим введением мы только начинаем работу с событиями, но вы уже можете решать разнообразные задачи с их использованием.** + + +[head] + + +[/head] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/2-events-and-timing-depth/article.md b/2-ui/2-events-and-interfaces/2-events-and-timing-depth/article.md new file mode 100644 index 00000000..7d9265ac --- /dev/null +++ b/2-ui/2-events-and-interfaces/2-events-and-timing-depth/article.md @@ -0,0 +1,175 @@ +# Порядок обработки событий + +События могут возникать не только по очереди, но и пачкой, по многу сразу. Возможно и такое, что во время обработки одного события возникают другие. + +Здесь и далее, очень важно понимать, как браузер обычно работает с событиями и важные исключения из этого правила. Это мы и разберём в этой главе. + +[cut] +## Главный поток + +В каждом окне выполняется только один *главный* поток, который занимается выполнением JavaScript, отрисовкой и работой с DOM. + +Он выполняет команды последовательно и блокируется при выводе модальных окон, таких как `alert`. + + +[smart header="Дополнительные потоки тоже есть"] +Есть и другие, служебные потоки, например, для сетевых коммуникаций. + +Поэтому скачивание файлов может продолжаться пока главный поток ждёт реакции на `alert`. Но управлять служебными потоками мы не можем. +[/smart] + +[smart header="Web Workers"] +Существует спецификация Web Workers, которая позволяет запускать дополнительные JavaScript-процессы(workers). + +Они могут обмениваться сообщениями с главным процессом, но их переменные полностью независимы. + +В частности, дополнительные процессы не имеют доступа к DOM, поэтому они полезны, преимущественно, при вычислениях, чтобы загрузить несколько ядер/процессоров одновременно. +[/smart] + +## Очередь событий + +Произошло одновременно несколько событий или во время работы одного случилось другое -- как главному потоку обработать это? + +Если главный поток прямо сейчас занят, то он не может срочно выйти из середины одной функции и прыгнуть в другую. А потом третью. Отладка при этом могла бы превратиться в кошмар, потому что пришлось бы разбираться с совместным состоянием нескольких функций сразу. + +Поэтому используется альтернативный подход. + +**Когда происходит событие, оно попадает в очередь.** + +Внутри браузера существует главный внутренний цикл, который проверяет очередь и обрабатывает события, запускает соответствующие обработчики и т.п. + +**Иногда события добавляются в очередь сразу пачкой.** + +Например, при клике на элементе генерируется несколько событий: +
                +
              1. Сначала `mousedown` -- нажата кнопка мыши.
              2. +
              3. Затем `mouseup` -- кнопка мыши отпущена.
              4. +
              5. Так как это было над одним элементом, то дополнительно генерируется `click`
              6. +
              + + +В действии: + +```html + + + + +``` + +Таким образом, при нажатии кнопки мыши в очередь попадёт событие `mousedown`, а при отпускании -- сразу два события: `mouseup` и `click`. Браузер сначала обработает первое, а потом -- второе. + +**При этом каждое событие из очереди обрабатывается полностью отдельно от других.** + +## Вложенные (синхронные) события + +В тех случаях, когда событие инициируется не посетителем, а кодом, то оно, как правило, обрабатывается синхронно, то есть прямо сейчас. + +Рассмотрим в качестве примера событие `onfocus`. + +### Пример: событие onfocus + +Когда посетитель фокусируется на элементе, возникает событие `onfocus`. Обычно оно происходит, когда посетитель кликает на поле ввода, например: + +```html + +

              При фокусе на поле оно изменит значение.

              + +``` + +Но ту же фокусировку можно вызвать и явно, вызовом метода `elem.focus()`: + +```html + + + + +``` + +В главе [](/focus-blur) мы познакомимся с этим событием подробнее, а пока -- нажмите на кнопку в примере ниже. При этом обработчик `onclick` вызовет метод `focus()` на текстовом поле `text`. + +**Событие `onfocus`, инициированное вызовом `text.focus()`, будет обработано синхронно, прямо сейчас, до завершения `onclick`.** + +```html + + + + + +``` + +При клике на кнопке в примере выше будет видно, что управление вошло в `onclick`, затем перешло в `onfocus`, затем вышло из `onclick`. + +**Так ведут себя все браузеры, кроме IE.** + +В нём событие `onfocus` -- всегда асинхронное, так что будет сначала полностью обработан клик, а потом -- фокус. В остальных -- фокус вызовется посередине клика. Попробуйте кликнуть в IE и в другом браузере, чтобы увидеть разницу. + + +## Делаем события асинхронными через setTimeout(...,0) + +А что, если мы хотим, чтобы *сначала* закончилась обработка `onclick`, а потом уже произошла обработка `onfocus` и связанные с ней действия? + +Можно добиться и этого. + +Один вариант -- просто переместить строку `text.focus()` вниз кода обработчика. + +Если это неудобно, можно запланировать `text.focus()` чуть позже через `setTimeout(..., 0)`, вот так + +```html + + + + + +``` + +Такой вызов обеспечит фокусировку через минимальный "тик" таймера, по стандарту равный 4мс. Обычно такая задержка не играет роли, а необходимую асинхронность мы получили. + +## Итого + +
                +
              • JavaScript выполняется в едином потоке. Современные браузеры позволяют порождать подпроцессы Web Workers, они выполняются параллельно и могут отправлять/принимать сообщения, но не имеют доступа к DOM.
              • +
              • Обычно события становятся в очередь и обрабатываются в порядке поступления, асинхронно, независимо друг от друга.
              • +
              • Синхронными являются вложенные события, инициированные из кода.
              • +
              • Чтобы сделать событие гарантированно асинхронным, используется вызов через `setTimeout(func, 0)`.
              • +
              diff --git a/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/solution.md b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/solution.md new file mode 100644 index 00000000..30031d16 --- /dev/null +++ b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/solution.md @@ -0,0 +1,35 @@ +# Мяч под курсор мыши + +Основная сложность первого этапа -- сдвинуть мяч под курсор, т.к. координаты клика `e.clientX/Y` -- относительно окна, а мяч позиционирован абсолютно внутри поля, его координаты `left/top` нужно ставить относительно левого-верхнего внутреннего (внутри рамки!) угла поля. + +Чтобы правильно вычислить координаты мяча, нужно получить координаты угла поля и вычесть их из `clientX/Y`: + +```js +var field = document.getElementById('field'); +var ball = document.getElementById('ball'); + +field.onclick = function(e) { + +*!* + var fieldCoords = field.getBoundingClientRect(); + var fieldInnerCoords = { + top: fieldCoords.top + field.clientTop, + left: fieldCoords.left + field.clientLeft + }; + + ball.style.left = e.clientX - fieldInnerCoords.left + 'px'; + ball.style.top = e.clientY - fieldInnerCoords.top + 'px'; +*/!* + +}; +``` + +Далее мяч нужно сдвинуть на половину его ширины и высоты `ball.clientWidth/clientHeight`, чтобы он оказался центром под курсором. + +Здесь есть важный "подводный камень" -- размеры мяча в исходном документе не прописаны. Там просто стоит ``. Но на момент выполнения JavaScript картинка, возможно, ещё не загрузилась, так что высота и ширина мяча будут неизвестны (а они необходимы для центрирования). + +Нужно добавить `width/height` в тег `` или задать размеры в CSS, тогда на момент выполнения JavaScript будет знать их и передвинет мяч правильно. + +Код, который полностью центрирует мяч, вы найдете в полном решении: + +[iframe border="1" src="solution" height="260" link edit] diff --git a/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/solution.view/index.html b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/solution.view/index.html new file mode 100755 index 00000000..9d09b8fd --- /dev/null +++ b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/solution.view/index.html @@ -0,0 +1,93 @@ + + + + + + + + + Кликните на любое место поля, чтобы мяч перелетел туда.
              + + +
              + + . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
              + + + + + diff --git a/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/source.view/index.html b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/source.view/index.html new file mode 100755 index 00000000..5db0fe17 --- /dev/null +++ b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/source.view/index.html @@ -0,0 +1,37 @@ + + + + + + + + +Кликните на любое место поля, чтобы мяч перелетел туда.
              +Мяч никогда не вылетит за границы поля. + + +
              + +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
              + + + + diff --git a/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/task.md b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/task.md new file mode 100644 index 00000000..4a56e13c --- /dev/null +++ b/2-ui/2-events-and-interfaces/3-obtaining-event-object/1-move-ball-field/task.md @@ -0,0 +1,26 @@ +# Передвигать мяч по полю + +[importance 5] + +Сделайте так, что при клике по полю мяч перемещался на место клика. + +[iframe src="solution" height="260" link] + +Требования: +
                +
              • Мяч после перелёта должен становиться центром ровно под курсор мыши, если это возможно без вылета за край поля.
              • +
              • CSS-анимация не обязательна, но желательна.
              • +
              • Мяч должен останавливаться у границ поля, ни в коем случае не вылетать за них.
              • +
              • При прокрутке страницы ничего не должно ломаться.
              • +
              + +Замечания: +
                +
              • Код не должен зависеть от конкретных размеров мяча и поля.
              • +
              • Текущий HTML/CSS нельзя менять, можно лишь "украшать" (анимация).
              • +
              • Вам пригодятся свойства `event.clientX/event.clientY`
              • +
              + +[edit src="source" task/] + +P.S. Центрировать мяч можно и при помощи CSS, но JavaScript в перспективе позволит делать это гибче и определять позицию более динамически. \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/3-obtaining-event-object/article.md b/2-ui/2-events-and-interfaces/3-obtaining-event-object/article.md new file mode 100644 index 00000000..b7ee8645 --- /dev/null +++ b/2-ui/2-events-and-interfaces/3-obtaining-event-object/article.md @@ -0,0 +1,109 @@ +# Объект события + +Чтобы хорошо обработать событие, недостаточно знать о том, что это -- "клик" или "нажатие клавиши". Могут понадобиться детали: координаты курсора, введённый символ и другие, в зависимости от события. + +**Детали произошедшего браузер записывает в "объект события", который передаётся первым аргументом в обработчик.** +[cut] + +## Получение объекта события + +Пример ниже демонстрирует использования объекта события: + +```html + + + + +``` + +Свойства объекта `event`: +
              +
              `event.type`
              +
              Тип события, в данном случае `click`
              +
              `event.currentTarget`
              +
              Элемент, на котором сработал обработчик -- то же, что и `this`, но бывают ситуации, когда обработчик является методом объекта и его `this` при помощи `bind` привязан к объекту, тогда `event.currentTarget` полезен.
              +
              `event.clientX / event.clientY`
              +
              Координаты курсора в момент клика (относительно окна)
              +
              + +Есть также и ряд других свойств, в зависимости от событий, которые мы разберём в дальнейших главах, когда будем подробно знакомиться с событиями мыши, клавиатуры и так далее. + +[smart header="Объект события доступен и в HTML"] +При назначении обработчика в HTML, тоже можно использовать переменную `event`, это будет работать кросс-браузерно: + +```html + + +``` + +Это возможно потому, что когда браузер из атрибута создаёт функцию-обработчик, то она выглядит так: `function(event) { alert(event.type) }`. То есть, её первый аргумент называется `"event"`. +[/smart] + +## Особенности IE8- + +IE8- вместо передачи параметра обработчику создаёт глобальный объект `window.event`. Обработчик может обратиться к нему. + +Работает это так: + +```js +elem.onclick = function() { + // window.event - объект события + alert( window.event.clientX ); +}; +``` + +### Кроссбраузерное решение + +Универсальное решение для получения объекта события: + +```js +element.onclick = function(event) { + event = event || window.event; // (*) + + // Теперь event - объект события во всех браузерах. +}; +``` + +Строка `(*)`, в случае, если функция не получила `event` (IE8-), использует `window.event`.-событие `event`. + +Можно написать и иначе, если мы сами не используем переменную `event` в замыкании: + +```js +element.onclick = function(e) { + e = e || event; + + // Теперь e - объект события во всех браузерах. +}; +``` + +## Итого + +
                +
              • Объект события содержит ценную информацию о деталях события.
              • +
              • Он передается первым аргументом `event` в обработчик для всех браузеров, кроме IE8-, в которых используется глобальная переменная `window.event`.
              • +
              + +Кросс-браузерно для JavaScript-обработчика получаем объект события так: + +```js +element.onclick = function(event) { + event = event || window.event; + + // Теперь event - объект события во всех браузерах. +}; +``` + +Еще вариант: + +```js +element.onclick = function(e) { + e = e || event; // если нет другой внешней переменной event + ... +}; +``` + diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/article.md b/2-ui/2-events-and-interfaces/4-event-bubbling/article.md new file mode 100644 index 00000000..7c774993 --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/article.md @@ -0,0 +1,255 @@ +# Всплытие и перехват + +Давайте сразу начнём с примера. + +Этот обработчик для `
              ` сработает, если вы кликните по вложенному тегу `` или ``: + +```html + +
              + Кликните на EM, сработает обработчик на DIV +
              +``` + +Вам не кажется это странным? Почему же сработал обработчик на `
              `, если клик произошёл на ``? + +## Всплытие + +Основной принцип всплытия: + +**При наступлении события обработчики сначала срабатывают на самом вложенном элементе, затем на его родителе, затем выше и так далее, вверх по цепочке вложенности.** + +Например, есть 3 вложенных элемента `FORM > DIV > P`, с обработчиком на каждом: + +```html + +FORM +
              DIV +

              P

              +
              + +``` + +Всплытие гарантирует, что клик по внутреннему `P` вызовет обработчик `onclick` (если есть) сначала на самом `P`, затем на элементе `DIV` далее на элементе `FORM`, и так далее вверх по цепочке родителей до самого `document`. + +Порядок всплытия событий + +Этот процесс называется *всплытием*, потому что события "всплывают" от внутреннего элемента вверх через родителей, подобно тому, как всплывает пузырек воздуха в воде. + +[warn header="Всплывают *почти* все события."] +Ключевое слово в этой фразе -- "почти". + +Например, событие `focus` не всплывает. В дальнейших главах мы будем детально знакомиться с различными событиями и увидим ещё примеры. +[/warn] + +## Целевой элемент event.target + +На каком бы элементе мы ни поймали событие, всегда можно узнать, где конкретно оно произошло. + +**Самый глубокий элемент, который вызывает событие, называется *"целевым"* или *"исходным"* элементом и доступен как `event.target`.** + +Отличия от `this` (=`event.currentTarget`): +
                +
              • `event.target` -- это **исходный элемент**, на котором произошло событие, в процессе всплытия он неизменен.
              • +
              • `this` -- это **текущий элемент**, до которого дошло всплытие, на нём сейчас выполняется обработчик.
              • +
              + +Например, если стоит только один обработчик `form.onclick`, то он "поймает" все клики внутри него. Где бы ни был клик внутри -- он всплывёт до элемента `
              `, на котором сработает обработчик. + +
                +
              • `event.target` будет содержать элемент, на котором произошёл клик.
              • +
              • `this` (`=event.currentTarget`) всегда будет сама форма, так как обработчик сработал на ней.
              • +
              + +[example height=220 src="bubble-target"] + +## Прекращение всплытия + +Всплытие идет прямо наверх. Обычно событие будет всплывать наверх и наверх, до элемента ``, а затем до `document` и даже до `window`, вызывая все обработчики на своем пути. + +**Но любой промежуточный обработчик может решить, что событие полностью обработано, и остановить всплытие.** + +Для остановки всплытия нужно вызвать метод `event.stopPropagation()`. + +Например, здесь при клике на кнопку обработчик `body.onclick` не сработает: + +```html + + + + +``` + +[smart header="event.stopImmediatePropagation()"] +Если у элемента есть несколько обработчиков на одно событие, то даже при прекращении всплытия все они будут выполнены. + +То есть, `stopPropagation` препятствует продвижению события дальше, но на текущем элементе все обработчики отработают. + +Для того, чтобы полностью остановить обработку, современные браузеры поддерживают метод `event.stopImmediatePropagation()`. Он не только предотвращает всплытие, но и останавливает обработку событий на текущем элементе. +[/smart] + +[warn header="Не прекращайте всплытие без необходимости!"] +Всплытие -- это удобно. Не прекращайте его без явной нужды, очевидной и архитектурно прозрачной. + +Зачастую прекращение всплытия создаёт свои подводные камни, которые потом приходится обходить. + +Например: +
                +
              1. Мы делаем меню. Оно обрабатывает клики на своих элементах и делает для них `stopPropagation`. Вроде бы, всё работает.
              2. +
              3. Позже мы решили отслеживать все клики в окне, для какой-то своей функциональности, к примеру, для статистики -- где вообще у нас кликают люди. Например, Яндекс.Метрика так делает, если включить соответствующую опцию.
              4. +
              5. Над областью, где клики убиваются `stopPropagation`, статистика работать не будет! Получилась "мёртвая зона".
              6. +
              + +Проблема в том, что `stopPropagation` убивает всякую возможность отследить событие сверху, а это бывает нужно для реализации чего-нибудь "эдакого", что к меню отношения совсем не имеет. +[/warn] + +## Погружение + +В современном стандарте, кроме "всплытия" событий, предусмотрено ещё и "погружение". + +Оно гораздо менее востребовано, но иногда, очень редко, знание о нём может быть полезным. + +[cut] + +## Три стадии прохода события + +Кроме всплытия, есть ещё стадии прохода события. + +В соответствии со стандартом, их три: + +
                +
              1. Событие сначала идет сверху вниз. Эта стадия называется *"стадия перехвата"* (capturing stage).
              2. +
              3. Событие достигло целевого элемента. Это -- *"стадия цели"* (target stage).
              4. +
              5. После этого событие начинает всплывать. Это -- *"стадия всплытия"* (bubbling stage).
              6. +
              + +В [стандарте DOM Events 3](http://www.w3.org/TR/DOM-Level-3-Events/) это продемонстрировано так: + + + +То есть, при клике на `TD` событие путешествует по цепочке родителей сначала вниз к элементу ("погружается"), а потом наверх ("всплывает"), по пути задействуя обработчики. + +**Ранее мы говорили только о всплытии, потому что другие стадии, как правило, не используются и проходят незаметно для нас.** + +Обработчики, добавленные через `on...`, ничего не знают о стадии перехвата, а начинают работать со всплытия. + +**Чтобы поймать событие на стадии перехвата, нужно использовать третий аргумент `addEventListener`:** + +
                +
              • Если аргумент `true`, то событие будет перехвачено по дороге вниз.
              • +
              • Если аргумент `false`, то событие будет поймано при всплытии.
              • +
              + +Стадия цели, обозначенная на рисунке цифрой `(2)`, особо не обрабатывается, так как обработчики, назначаемые обоими этими способами, срабатывают также на целевом элементе. + +## Примеры + +В примере ниже на `form, div, p` стоят те же обработчики, что и раньше, но на этот раз -- на стадии погружения. + +Чтобы увидеть перехват в действии, кликните на элементе `P`: + +[example height=220 src="capture"] + +Обработчики сработают в порядке "сверху-вниз": `FORM` -> `DIV` -> `P`. + +JS-код здесь такой: + +```js +var elems = document.querySelectorAll('form,div,p'); + +// на каждый элемент повесить обработчик на стадии перехвата +for(var i=0; i `DIV` -> `P` -> `P` -> `DIV` -> `FORM`. Заметим, что элемент `P` участвует в обоих стадиях. + +Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства `event.eventPhase` (=1, если погружение, =3, если всплытие). + +[smart header="Есть события, которые не всплывают, но которые можно перехватить"] +Есть события, которые можно поймать только на стадии перехвата, а на стадии всплытия -- нельзя.. + +Например, таково событие фокусировки на элементе [onfocus](/focus-blur). +[/smart] + + +## Отличия IE8- + +Чтобы было проще ориентироваться, я собрал отличия IE8-, которые имеют отношение ко всплытию, в одну секцию. + +Их знание понадобится, если вы решите писать на чистом JS, без фреймворков и вам понадобится поддержка IE8-. + +
              +
              Нет свойства `event.currentTarget`
              +
              Обратим внимание, что при назначении обработчика через `onсвойство` у нас есть `this`, поэтому `event.currentTarget`, как правило, не нужно, а вот при назначении через `attachEvent` обработчик не получает `this`, так что текущий элемент, если нужен, можно будет взять лишь из замыкания.
              +
              Вместо `event.target` в IE8- используется `event.srcElement`
              +
              Если мы пишем обработчик, который будет поддерживать и IE8- и современные браузеры, то можно начать его так: + +```js +elem.onclick = function(event) { + event = event || window.event; + var target = event.target || event.srcElement; + + // ... теперь у нас есть объект события и target + ... +} +``` + +
              +
              Для остановки всплытия используется код `event.cancelBubble=true`.
              +
              Кросс-браузерно остановить всплытие можно так: + +```js +event.stopPropagation ? event.stopPropagation() : (event.cancelBubble=true); +``` + +
              +
              + +Далее в учебнике мы будем использовать стандартные свойства и вызовы, поскольку добавление этих строк, обеспечивающих совместимость -- достаточно простая и очевидная задача. + +Ещё раз хотелось бы заметить -- эти отличия понадобятся при написании JS-кода с поддержкой IE8- без фреймворков. Почти все JS-фреймворки обеспечивают кросс-браузерную поддержку `target`, `currentTarget` и `stopPropagation()`. + +## Итого + +Алгоритм: +
                +
              • При наступлении события -- элемент, на котором оно произошло, помечается как "целевой" (`event.target`).
              • +
              • Далее событие сначала двигается вниз от корня документа к `event.target`, по пути вызывая обработчики, поставленные через `addEventListener(...., true)`.
              • +
              • Далее событие двигается от `event.target` вверх к корню документа, по пути вызывая обработчики, поставленные через `on*` и `addEventListener(...., false)`.
              • +
              + +Каждый обработчик имеет доступ к свойствам события: +
                +
              • `event.target` -- самый глубокий элемент, на котором прозошло событие.
              • +
              • `event.currentTarget` (=`this`) -- элемент, на котором в данный момент сработал обработчик (до которого "доплыло" событие).
              • +
              • `event.eventPhase` -- на какой фазе он сработал (погружение =1, всплытие = 3).
              • +
              + +Любой обработчик может остановить событие вызовом `event.stopPropagation()`, но делать это не рекомендуется, так как в дальнейшем это событие может понадобиться, иногда для самых неожиданных вещей. + +В современной разработке стадия погружения используется очень редко. + +Этому есть две причины: +
                +
              1. Историческая, так как IE лишь с версии 9 в полной мере поддерживает современный стандарт.
              2. +
              3. Разумная -- когда происходит событие, то разумно дать возможность первому сработать обработчику на самом элементе, поскольку он наиболее конкретен. Код, который поставил обработчик именно на этот элемент, знает максимум деталей о том, что это за элемент, чем он занимается, и обработчик через замыкание, скорее всего, имеет к ним доступ. + +Далее имеет смысл передать обработку события родителю -- он тоже понимает, что происходит, но уже менее детально, далее -- выше, и так далее, до самого объекта `document`, обработчик на котором реализовывает самую общую функциональность уровня документа.
              4. +
              diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/example.css b/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/example.css new file mode 100755 index 00000000..4fc1a986 --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/example.css @@ -0,0 +1,28 @@ +form { + background-color: green; + position: relative; + width: 150px; + height: 150px; + text-align: center; + cursor: pointer; +} + +div { + background-color: blue; + position: absolute; + top: 25px; + left: 25px; + width: 100px; + height: 100px; +} + +p { + background-color: red; + position: absolute; + top: 25px; + left: 25px; + width: 50px; + height: 50px; + line-height: 50px; + margin: 0; +} diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/index.html b/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/index.html new file mode 100755 index 00000000..8099db3b --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/index.html @@ -0,0 +1,14 @@ + + + + + +FORM +
              DIV +

              P

              +
              + + + + + diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/script.js b/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/script.js new file mode 100755 index 00000000..b7c03f1c --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/both.view/script.js @@ -0,0 +1,13 @@ + +var elems = document.querySelectorAll('form,div,p'); + +for(var i=0; i + + + + +
              FORM +
              DIV +

              P

              +
              +
              + + + + diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/bubble-target.view/script.js b/2-ui/2-events-and-interfaces/4-event-bubbling/bubble-target.view/script.js new file mode 100755 index 00000000..5e454fed --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/bubble-target.view/script.js @@ -0,0 +1,10 @@ + +var form = document.querySelector('form'); + +form.onclick = function(event) { + event.target.style.backgroundColor = 'yellow'; + + alert("target = " + event.target.tagName + ", this=" + this.tagName); + + event.target.style.backgroundColor = ''; +}; diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/example.css b/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/example.css new file mode 100755 index 00000000..4fc1a986 --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/example.css @@ -0,0 +1,28 @@ +form { + background-color: green; + position: relative; + width: 150px; + height: 150px; + text-align: center; + cursor: pointer; +} + +div { + background-color: blue; + position: absolute; + top: 25px; + left: 25px; + width: 100px; + height: 100px; +} + +p { + background-color: red; + position: absolute; + top: 25px; + left: 25px; + width: 50px; + height: 50px; + line-height: 50px; + margin: 0; +} diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/index.html b/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/index.html new file mode 100755 index 00000000..8099db3b --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/index.html @@ -0,0 +1,14 @@ + + + + + +
              FORM +
              DIV +

              P

              +
              +
              + + + + diff --git a/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/script.js b/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/script.js new file mode 100755 index 00000000..d105514a --- /dev/null +++ b/2-ui/2-events-and-interfaces/4-event-bubbling/capture.view/script.js @@ -0,0 +1,12 @@ + +var elems = document.querySelectorAll('form,div,p'); + +for(var i=0; i + + + + + + + + +
              +
              +

              Лошадь

              +

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

              + +
              +
              +

              Осёл

              +

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

              + +
              +
              +

              Корова, а также пара слов о диком быке, о волах и о тёлках.

              +

              Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.

              + +
              +
              + + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/solution.view/messages.css b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/solution.view/messages.css new file mode 100755 index 00000000..ad86dadf --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/solution.view/messages.css @@ -0,0 +1,35 @@ +body { + margin: 10px auto; + width: 470px; +} +h3 { + margin: 0; + padding-bottom: .3em; + font-size: 1.1em; +} +p { + margin: 0; + padding: 0 0 .5em; +} +.pane { + position: relative; + padding: 10px 20px 10px; + border-top: solid 2px #c4df9b; + background: #edf5e1; +} + + +.remove-button { + position: absolute; + top: 10px; + right: 10px; + display: block; + width: 16px; + height: 16px; + background: url(http://js.cx/clipart/delete.gif) no-repeat; + cursor: pointer; +} + +h3 { + padding-right: 20px; +} diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/source.view/index.html b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/source.view/index.html new file mode 100755 index 00000000..6b9ed4a5 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/source.view/index.html @@ -0,0 +1,34 @@ + + + + + + + + + +
              +
              +

              Лошадь

              +

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

              + +
              +
              +

              Осёл

              +

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

              + +
              +
              +

              Корова, а также пара слов о диком быке, о волах и о тёлках.

              +

              Коро́ва — самка домашнего быка, одомашненного подвида дикого быка, парнокопытного жвачного животного семейства полорогих. Самцы вида называются быками, молодняк — телятами, кастрированные самцы — волами. Молодых (до первой стельности) самок называют тёлками.

              + +
              +
              + + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/source.view/messages.css b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/source.view/messages.css new file mode 100755 index 00000000..ed64b446 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/source.view/messages.css @@ -0,0 +1,32 @@ +body { + margin: 10px auto; + width: 470px; +} +h3 { + margin: 0; + padding-bottom: .3em; + padding-right: 20px; + font-size: 1.1em; +} +p { + margin: 0; + padding: 0 0 .5em; +} +.pane { + background: #edf5e1; + padding: 10px 20px 10px; + border-top: solid 2px #c4df9b; + position: relative; +} + +.remove-button { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + display: block; + background: url(http://js.cx/clipart/delete.gif) no-repeat; + width: 16px; + height: 16px; +} + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/task.md b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/task.md new file mode 100644 index 00000000..8142d5a6 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/1-hide-message-delegate/task.md @@ -0,0 +1,12 @@ +# Скрытие сообщения с помощью делегирования + +[importance 5] + +Дан список сообщений. Добавьте каждому сообщению кнопку для его удаления. + +**Используйте делегирование событий. Один обработчик для всего.** + +В результате, должно работать вот так(кликните на крестик): +[iframe src="solution" height=500] + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/solution.md b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/solution.md new file mode 100644 index 00000000..5726f23d --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/solution.md @@ -0,0 +1,109 @@ +# Схема решения + +Дерево устроено как вложенный список. + +Клики на все элементы можно поймать, повесив единый обработчик `onclick` на внешний `UL`. + +Как поймать клик на заголовке? Элемент `LI` является блочным, поэтому нельзя понять, был ли клик на *тексте*, или справа от него. + +Например, ниже -- участок дерева с выделенными рамкой узлами. Кликните справа от любого заголовка. Видите, клик ловится? А лучше бы такие клики (не на тексте) игнорировать. + +```html + + + +
                +
              • Млекопетающие +
                  +
                • Коровы
                • +
                • Ослы
                • +
                • Собаки
                • +
                • Тигры
                • +
                +
              • +
              +``` + +В примере выше видно, что проблема в верстке, в том что `LI` занимает всю ширину. Можно кликнуть справа от текста, это все еще `LI`. + +Один из способов это поправить -- обернуть заголовки в дополнительный элемент `SPAN`, и обрабатывать только клики внутри `SPAN'ов`, получать по `SPAN'у` его родителя `LI` и ставить ему класс открыт/закрыт. + +Напишите для этого JavaScript-код. + +# Оборачиваем заголовки в SPAN + +Следующий код ищет все `LI` и оборачивает текстовые узлы в `SPAN`. + +```js +var treeUl = document.getElementsByTagName('ul')[0]; + +var treeLis = treeUl.getElementsByTagName('li'); + +for(var i=0; i + + +
                +
              • Млекопетающие +
                  +
                • Коровы
                • +
                • Ослы
                • +
                • Собаки
                • +
                • Тигры
                • +
                +
              • +
              +``` + +Так как `SPAN` -- инлайновый элемент, он всегда такого же размера как текст. Да здравствует `SPAN`! + +В реальной жизни дерево, скорее всего, будет сразу со `SPAN`: если HTML-код дерева генерируется на сервере, то это несложно, если дерево генерируется в JavaScript -- тем более просто. + +# Итоговое решение + +Для делегирования нужно по клику понять, на каком узле он произошел. + +В нашем случае у `SPAN` нет детей-элементов, поэтому не нужно подниматься вверх по цепочке родителей. Достаточно просто проверить `event.target.tagName == 'SPAN'`, чтобы понять, где был клик, и спрятать потомков. + +```js +var tree = document.getElementsByTagName('ul')[0]; + +tree.onclick = function(event) { + var target = event.target; + + if (target.tagName != 'SPAN') { + return; // клик был не на заголовке + } + + var li = target.parentNode; // получить родительский LI + + // получить UL с потомками -- это первый UL внутри LI + var node = li.getElementsByTagName('ul')[0]; + + if (!node) return; // потомков нет -- ничего не надо делать + + // спрятать/показать (можно и через CSS-класс) + node.style.display = node.style.display ? '' : 'none'; +} +``` + +Выделение узлов жирным при наведении делается при помощи CSS-селектора `:hover`. + +[edit src="solution"]Полное решение[/edit] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/solution.view/index.html b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/solution.view/index.html new file mode 100755 index 00000000..4292b440 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/solution.view/index.html @@ -0,0 +1,86 @@ + + + + + + + + +
                +
              • Животные +
                  +
                • Млекопитающие +
                    +
                  • Коровы
                  • +
                  • Ослы
                  • +
                  • Собаки
                  • +
                  • Тигры
                  • +
                  +
                • +
                • Другие +
                    +
                  • Змеи
                  • +
                  • Птицы
                  • +
                  • Ящерицы
                  • +
                  +
                • +
                +
              • +
              • Рыбы +
                  +
                • Аквариумные +
                    +
                  • Гуппи
                  • +
                  • Скалярии
                  • +
                  + +
                • +
                • Морские +
                    +
                  • Морская форель
                  • +
                  +
                • +
                +
              • +
              + + + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/source.view/index.html b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/source.view/index.html new file mode 100755 index 00000000..e4100d0a --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/source.view/index.html @@ -0,0 +1,45 @@ + + + + + +
                +
              • Животные +
                  +
                • Млекопитающие +
                    +
                  • Коровы
                  • +
                  • Ослы
                  • +
                  • Собаки
                  • +
                  • Тигры
                  • +
                  +
                • +
                • Другие +
                    +
                  • Змеи
                  • +
                  • Птицы
                  • +
                  • Ящерицы
                  • +
                  +
                • +
                +
              • +
              • Рыбы +
                  +
                • Аквариумные +
                    +
                  • Гуппи
                  • +
                  • Скалярии
                  • +
                  + +
                • +
                • Морские +
                    +
                  • Морская форель
                  • +
                  +
                • +
                +
              • +
              + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/task.md b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/task.md new file mode 100644 index 00000000..d838b3b7 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/2-sliding-tree/task.md @@ -0,0 +1,19 @@ +# Раскрывающееся дерево + +[importance 5] + +Создайте дерево, которое по клику на заголовок скрывает-показывает детей: + +[iframe border=1 src="solution"] + + +Требования: +
                +
              • Использовать делегирование.
              • +
              • Клик вне текста заголовка (на пустом месте) ничего делать не должен.
              • +
              • При наведении на заголовок -- он становится жирным, реализовать через CSS.
              • +
              + +P.S. При необходимости HTML/CSS дерева можно изменить. + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/solution.md b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/solution.md new file mode 100644 index 00000000..ae704e84 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/solution.md @@ -0,0 +1,21 @@ +# Подсказка (обработчик) + +
                +
              1. Обработчик `onclick` можно повесить один, на всю таблицу или `THEAD`. Он будет игнорировать клики не на `TH`.
              2. +
              3. При клике на `TH` обработчик будет получать номер из `TH`, на котором кликнули (`TH.cellIndex`) и вызывать функцию `sortColumn`, передавая ей номер колонки и тип.
              4. +
              5. Функция `sortColumn(colNum, type)` будет сортировать.
              6. +
              + +# Подсказка (сортировка) + +Функция сортировки: + +
                +
              1. Переносит все `TR` из `TBODY` в массив `rowsArr`
              2. +
              3. Сортирует массив, используя `rowsArr.sort(compare)`, функция `compare` зависит от типа столбца.
              4. +
              5. Добавляет `TR` из массива обратно в `TBODY`
              6. +
              + +# Решение + +[edit src="solution"/] \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/solution.view/index.html b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/solution.view/index.html new file mode 100755 index 00000000..572ca9d6 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/solution.view/index.html @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              ВозрастИмя
              5Вася
              2Петя
              12Женя
              9Маша
              1Илья
              + + + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/source.view/index.html b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/source.view/index.html new file mode 100755 index 00000000..9aaad9c9 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/source.view/index.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              ВозрастИмя
              5Вася
              2Петя
              12Женя
              9Маша
              1Илья
              + + + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/task.md b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/task.md new file mode 100644 index 00000000..23b0059f --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/3-sort-table/task.md @@ -0,0 +1,20 @@ +# Сортировка таблицы + +[importance 4] + +Сделать сортировку таблицы при клике на заголовок. + +Демо: +[iframe border=1 src="solution"] + +Требования: +
                +
              • Использовать делегирование.
              • +
              • Код не должен меняться при увеличении количества столбцов или строк.
              • +
              + +[edit src="source" task/] + +P.S. Обратите внимание, тип столбца задан атрибутом у заголовка. Это необходимо, ведь числа сортируются иначе чем строки. Соответственно, код это может использовать. + +P.P.S. Вам помогут дополнительные [навигационные ссылки по таблицам](#dom-navigation-tables). \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/article.md b/2-ui/2-events-and-interfaces/5-event-delegation/article.md new file mode 100644 index 00000000..82ce00db --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/article.md @@ -0,0 +1,201 @@ +# Делегирование событий + +Всплытие событий позволяет реализовать один из самых важных приёмов разработки -- *делегирование*. + +Он заключается в том, что если у нас есть много элементов, события на которых нужно обрабатывать похожим образом, то вместо того, чтобы назначать обработчик каждому -- мы ставим один обработчик на их общего предка. Из него можно получить целевой элемент `event.target`, понять на каком именно потомке произошло событие и обработать его. + +## Пример "Ба Гуа" + +Рассмотрим пример -- диаграмму "Ба Гуа". Это таблица, отражающая древнюю китайскую философию. + +Вот она: +[iframe height=350 src="bagua" edit link] + +Её HTML (схематично): + +```html + + + + + + + + + + ...еще 2 строки такого же вида... + ...еще 2 строки такого же вида... +
              Bagua Chart: Direction, Element, Color, Meaning
              ...Northwest.........
              +``` + +В этой таблице всего 9 ячеек, но могло быть и 99, и даже 9999, не важно. + +**Наша задача -- реализовать подсветку ячейки `` при клике.** + +Вместо того, чтобы назначать обработчик для каждой ячейки, мы повесим *один обработчик* на элемент ``. + +Он использует `event.target`, чтобы получить элемент, на котором произошло событие, и подсветить его. + +Код будет таким: + +```js +var selectedTd; + +*!* +table.onclick = function(event) { + var target = event.target; // где был клик? + + if (target.tagName != 'TD') return; // не на TD? тогда не интересует + + highlight(target); // подсветить TD +}; +*/!* + +function highlight(node) { + if (selectedTd) { + selectedTd.classList.remove('highlight'); + } + selectedTd = node; + selectedTd.classList.add('highlight'); +} +``` + +Такой код будет работать и ему без разницы, сколько ячеек в таблице. Обработчик всё равно один. Я могу добавлять, удалять `
              ` из таблицы, менять их количество -- моя подсветка будет стабильно работать, так как обработчик стоит на ``. + +Однако, у текущей версии кода есть недостаток. + +**Клик может быть не на том теге, который нас интересует, а внутри него.** + +В нашем случае клик может произойти на вложенном элементе, внутри `
              `, например на ``. Такой клик будет пойман по пути наверх, но `target` у него будет не ``, а ``: + + + +**Внутри обработчика `table.onclick` мы должны найти нужный `` по `event.target`.** + +Для этого мы вручную, используя ссылку `parentNode`, будем идти вверх по иерархии родителей от `event.target` и выше и проверять: +
                +
              • Если нашли `
              `, значит это то что нужно. +
            1. Если дошли до элемента `table` и при этом `
            2. ` не найден, то наверное клик был вне ``, например на элементе заголовка таблицы. + + +Улучшенный обработчик `table.onclick` с циклом `while`, который этот делает: + +```js +table.onclick = function(event) { + var target = event.target; + + // цикл двигается вверх от target к родителям до table + while(target != table) { + if (target.tagName == 'TD') { + // нашли элемент, который нас интересует! + highlight(target); + return; + } + target = target.parentNode; + } + + // возможна ситуация, когда клик был вне + // если цикл дошёл до table и ничего не нашёл, + // то обработчик просто заканчивает работу +} +``` + +[smart] +Кстати, в проверке `while` можно бы было использовать `this` вместо `table`: + +```js +while(target != this) { + // ... +} +``` + +Это тоже будет работать, так как в обработчике `table.onclick` значением `this` является текущий элемент, то есть `table`. +[/smart] + + +## Применение делегирования: действия в разметке + +Обычно делегирование -- это средство оптимизации интерфейса. Мы используем один обработчик для *схожих* действий на однотипных элементах. + +**Но делегирование позволяет использовать обработчик и для абсолютно разных действий.** + +Например, нам нужно сделать меню с разными кнопками: "Сохранить", "Загрузить", "Поиск" и т.д. И есть объект с соответствующими методами: `save`, `load`, `search` и т.п... + +Первое, что может прийти в голову -- это найти каждую кнопку и назначить ей свой обработчик среди методов объекта. + +Но более изящно решить задачу можно путем добавления одного обработчика на всё меню, а для каждой кнопки в специальном атрибуте, который мы назовем `data-action` (можно придумать любое название, но `data-*` является валидным в HTML5), укажем, что она должна вызывать: + +```html + +``` + +Обработчик считывает содержимое атрибута и выполняет метод. Взгляните на рабочий пример: + +```html + + + + +``` + +Обратите внимание, как используется трюк с `var self = this`, чтобы сохранить ссылку на объект `Menu`. Иначе обработчик просто бы не смог вызвать методы `Menu`, потому что *его собственный `this` ссылается на элемент*. + +Что в этом случае нам дает использование делегирования событий? +[compare] ++Не нужно писать код, чтобы присвоить обработчик каждой кнопке. Меньше кода, меньше времени, потраченного на инициализацию. ++Структура HTML становится по-настоящему гибкой. Мы можем добавлять/удалять кнопки в любое время. ++Данный подход является семантичным. Мы можем использовать классы `.action-save`, `.action-load` вместо атрибута `data-action`, если захотим. +[/compare] + + +## Итого + +Делегирование событий -- это здорово! Пожалуй, это один из самых полезных приёмов для работы с DOM. Он отлично подходит, если есть много элементов, обработка которых очень схожа. + +Алгоритм: +
                +
              1. Вешаем обработчик на контейнер.
              2. +
              3. В обработчике: получаем `event.target`.
              4. +
              5. В обработчике: если необходимо, проходим вверх цепочку `target.parentNode`, пока не найдем нужный подходящий элемент (и обработаем его), или пока не упремся в контейнер (`this`).
              6. +
              +Зачем использовать: +[compare] ++Упрощает инициализацию и экономит память: не нужно вешать много обработчиков. ++Меньше кода: при добавлении и удалении элементов не нужно ставить или снимать обработчики. ++Удобство изменений: можно массово добавлять или удалять элементы путём изменения `innerHTML`. +[/compare] + +Конечно, у делегирования событий есть свои ограничения. + +[compare] +-Во-первых, событие должно всплывать, и нельзя, чтобы какой-то промежуточный обработчик вызвал `event.stopPropagation()`. +-Во-вторых, делегирование создает дополнительную нагрузку на браузер, ведь обработчик запускается, когда событие происходит в любом месте контейнера, не обязательно на элементах, которые нам интересны. Но обычно эта нагрузка невелика и не является проблемой. +[/compare] + + + + diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/bagua.png b/2-ui/2-events-and-interfaces/5-event-delegation/bagua.png new file mode 100755 index 00000000..9a256a24 Binary files /dev/null and b/2-ui/2-events-and-interfaces/5-event-delegation/bagua.png differ diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/bagua.view/bagua.css b/2-ui/2-events-and-interfaces/5-event-delegation/bagua.view/bagua.css new file mode 100755 index 00000000..82ec4cb1 --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/bagua.view/bagua.css @@ -0,0 +1,56 @@ +#bagua-table th { + text-align: center; + font-weight: bold; +} + +#bagua-table td { + width: 150px; + white-space: nowrap; + text-align: center; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 12px; +} + +#bagua-table .nw { + background: #999; +} + +#bagua-table .n { + background: #03f; + color: #fff; +} + +#bagua-table .ne { + background: #ff6; +} + +#bagua-table .w { + background: #ff0; +} +#bagua-table .c { + background: #60c; + color: #fff; +} +#bagua-table .e { + background: #09f; + color: #fff; +} + +#bagua-table .sw { + background: #963; + color: #fff; +} + +#bagua-table .s { + background: #f60; + color: #fff; +} +#bagua-table .se { + background: #0c3; + color: #fff; +} + +#bagua-table .highlight { + background: red; +} diff --git a/2-ui/2-events-and-interfaces/5-event-delegation/bagua.view/index.html b/2-ui/2-events-and-interfaces/5-event-delegation/bagua.view/index.html new file mode 100755 index 00000000..ae37006e --- /dev/null +++ b/2-ui/2-events-and-interfaces/5-event-delegation/bagua.view/index.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
              Bagua Chart: Direction, Element, Color, Meaning
              Northwest
              Metal
              Silver
              Elders +
              North
              Water
              Blue
              Change +
              Northeast
              Earth
              Yellow
              Direction +
              West
              Metal
              Gold
              Youth +
              Center
              All
              Purple
              Harmony +
              East
              Wood
              Blue
              Future +
              Southwest
              Earth
              Brown
              Tranquility +
              South
              Fire
              Orange
              Fame +
              Southeast
              Wood
              Green
              Romance +
              + + + + + diff --git a/2-ui/2-events-and-interfaces/6-behavior/article.md b/2-ui/2-events-and-interfaces/6-behavior/article.md new file mode 100644 index 00000000..6af50d13 --- /dev/null +++ b/2-ui/2-events-and-interfaces/6-behavior/article.md @@ -0,0 +1,76 @@ +# Приём проектирования "поведение" + +Шаблон проектирования "поведение" (behavior) позволяет задавать хитрые обработчики на элементе *декларативно*, установкой специальных HTML-атрибутов и классов. +[cut] + +Например, хочется, чтобы при клике на один элемент показывался другой. Конечно, можно поставить обработчик в атрибуте `onclick` или даже описать его где-нибудь в JS-коде страницы. + +Но есть решение другое, и очень изящное. + +## Описание + +Приём проектирования "поведение" состоит из двух частей: +
                +
              1. Элементу ставится атрибут, описывающий его поведение.
              2. +
              3. При помощи делегирования ставится обработчик на документ, который ловит все клики и, если элемент имеет нужный атрибут, производит нужное действие. +
              + +## Пример + +Например, я хочу, чтобы при клике на один элемент скрывался/показывался другой. + +Конечно, можно написать соответствующий обработчик в JavaScript. + +А что, если подобная задача возникает часто? + +Хотелось бы получить более простой способ задания такого *поведения*, например -- *декларативно*, при помощи особого атрибута. + +**Сделаем так, что при клике на элемент с атрибутом `data-toggle-id` будет скрываться/показываться элемент с заданным `id`.** + +```html + + + + + + +``` + +**При помощи JavaScript мы добавили "поведение" -- возможность через атрибут указывать, что делает элемент.** + +[smart header="Не только атрибут"] +Для своих целей мы можем использовать в HTML любые атрибуты, но стандарт рекомендует для своих целей называть атрибуты `data-*`. + +В обработчике `document.onclick` мы могли бы проверять не атрибут, а класс или что-то ещё. +[/smart] + +Обратите внимание: обработчик поставлен на `document`, то есть клик на любом элементе страницы пройдёт через него. Теперь для того, чтобы добавить скрытие-раскрытие любому элементу -- даже не надо знать JavaScript, можно просто написать атрибут. + +Также для добавления обработчиков на `document` рекомендуется использовать `addEventListener`, чтобы можно было добавлять несколько различных поведений на документ. + +## Итого + +Шаблон "поведение" удобен тем, что сколь угодно сложное JavaScript-поведение можно "навесить" на элемент одним лишь атрибутом. А можно -- несколькими атрибутами на связанных элементах. + +Здесь мы рассмотрели базовый пример, который можно как угодно модифицировать и масштабировать. Важно не переусердствовать. + +**Приём разработки "поведение" рекомендуется использовать для расширения возможностей разметки, как альтернативу мини-фрагментам JavaScript.** + +Далее у нас ещё будут задачи, где мы реализуем этот приём разработки. \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/1-why-return-false-fails/solution.md b/2-ui/2-events-and-interfaces/7-default-browser-action/1-why-return-false-fails/solution.md new file mode 100644 index 00000000..bba60480 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/1-why-return-false-fails/solution.md @@ -0,0 +1,42 @@ +Дело в том, что обработчик из атрибута `onclick` делается браузером как функция с заданным телом. + +То есть, он будет таким: + +```js +function(event) { + handler() +} +``` + +При этом возвращаемое `handler` значение никак не используется и не влияет на результат. + +Рабочий вариант: + +```html + + + +w3.org +``` + +Альтернатива -- передать и использовать объект события для вызова `event.preventDefault()` (или кросс-браузерного варианта для поддержки старых IE). + +```html + + + +w3.org +``` + diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/1-why-return-false-fails/task.md b/2-ui/2-events-and-interfaces/7-default-browser-action/1-why-return-false-fails/task.md new file mode 100644 index 00000000..ba5604e9 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/1-why-return-false-fails/task.md @@ -0,0 +1,21 @@ +# Почему не работает return false? + +[importance 3] + +Почему в этом документе `return false` не работает? + +```html + + + +w3.org +``` + +По замыслу, переход на `w3.org` при клике должен отменяться. Однако, на самом деле он происходит. + +В чём дело и как поправить, сохранив `onclick` в HTML? \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/solution.md b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/solution.md new file mode 100644 index 00000000..81aa6a91 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/solution.md @@ -0,0 +1,29 @@ +Это -- классическая задача на тему делегирования. + +В реальной жизни, мы можем перехватить событие и создать AJAX-запрос к серверу, который сохранит информацию о том, по какой ссылке ушел посетитель. + +Мы перехватываем событие на `contents` и поднимаемся до `parentNode` пока не получим `A` или не упремся в контейнер. + +```js +contents.onclick = function(evt) { + var target = evt.target; + + function handleLink(href) { + var isLeaving = confirm('Уйти на '+href+'?'); + if (!isLeaving) return false; + } + + while(target != this) { + if (target.nodeName == 'A') { +*!* + return handleLink(target.getAttribute('href')); // (*) +*/!* + } + target = target.parentNode; + } +}; +``` + +В строке `(*)` используется атрибут, а не свойство `href`, чтобы показать в `confirm` именно то, что написано в HTML-атрибуте, так как свойство может отличаться, оно обязано содержать полный валидный адрес. + +[edit src="solution"]Полное решение[/edit]. diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/solution.view/index.html b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/solution.view/index.html new file mode 100755 index 00000000..96ee683d --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/solution.view/index.html @@ -0,0 +1,33 @@ + + + + + + + +
              +

              + Как насчет почитать Википедию, или посетить W3.org и узнать про современные стандарты? +

              +
              + + + + diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/source.view/index.html b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/source.view/index.html new file mode 100755 index 00000000..a70b2602 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/source.view/index.html @@ -0,0 +1,15 @@ + + + + + + + +
              +

              + Как насчет почитать Википедию, или посетить W3.org и узнать про современные стандарты? +

              +
              + + + diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/task.md b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/task.md new file mode 100644 index 00000000..935c9a55 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/2-catch-link-navigation/task.md @@ -0,0 +1,18 @@ +# Поймайте переход по ссылке + +[importance 5] + +Сделайте так, чтобы при клике на ссылки внутри <DIV id="contents"> пользователю выводился вопрос о том, действительно ли он хочет покинуть страницу и если он не хочет, то прерывать переход по ссылке. + +Так это должно работать: + +[iframe height=100 border=1 src="solution"] + +Детали: +
                +
              • Содержимое блока `DIV` может быть загружено динамически и присвоено при помощи `innerHTML`. Так что найти все ссылки и поставить на них обработчики нельзя. Используйте делегирование.
              • +
              • Содержимое может содержать вложенные теги, *в том числе внутри ссылок*, например, `...`.
              • +
              + +[edit src="source" task/] + diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/solution.md b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/solution.md new file mode 100644 index 00000000..856b4204 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/solution.md @@ -0,0 +1,57 @@ +Решение состоит в том, чтобы добавить обработчик на контейнер `#thumbs` и отслеживать клики на ссылках. + +Когда происходит событие, обработчик должен изменять `src` `#largeImg` на `href` ссылки и заменять `alt` на ее `title`. + +Код решения: + +```js +var largeImg = document.getElementById('largeImg'); + +document.getElementById('thumbs').onclick = function(e) { + e = e || window.event; + var target = e.target || e.srcElement; + + while(target != this) { + + if (target.nodeName == 'A') { + showThumbnail(target.href, target.title); + return false; + } + + target = target.parentNode; + } + +} + +function showThumbnail(href, title) { + largeImg.src = href; + largeImg.alt = title; +} +``` + +**Предзагрузка картинок** + +Для того, чтобы картинка загрузилась, достаточно создать новый элемент `IMG` и указать ему `src`, вот так: + +```js +var imgs = thumbs.getElementsByTagName('img'); +for(var i=0; i + + + Галерея + + + + + + +

              Large image

              + +
                + +
              • +
              • +
              • +
              • +
              • +
              + + + + + diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/source.view/gallery.css b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/source.view/gallery.css new file mode 100755 index 00000000..230421b7 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/source.view/gallery.css @@ -0,0 +1,35 @@ + +body { + margin: 0; + padding: 0; + font: 75%/120% Arial, Helvetica, sans-serif; +} + +h2 { + font: bold 190%/100% Arial, Helvetica, sans-serif; + margin: 0 0 .2em; +} +h2 em { + font: normal 80%/100% Arial, Helvetica, sans-serif; + color: #999999; +} + +#largeImg { + border: solid 1px #ccc; + width: 550px; + height: 400px; + padding: 5px; +} + +#thumbs a { + border: solid 1px #ccc; + width: 100px; + height: 100px; + padding: 3px; + margin: 2px; + float: left; +} + +#thumbs a:hover { + border-color: #FF9900; +} diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/source.view/index.html b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/source.view/index.html new file mode 100755 index 00000000..a6fb7d22 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/source.view/index.html @@ -0,0 +1,23 @@ + + + + Галерея + + + + + + +

              Large image

              + +
              + + + + + + +
              + + + diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/task.md b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/task.md new file mode 100644 index 00000000..0d404c27 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/3-image-gallery/task.md @@ -0,0 +1,22 @@ +# Галерея изображений + +[importance 5] + +Создайте галерею изображений, в которой основное изображение изменяется при клике на уменьшенный вариант. + +Результат должен выглядеть так: + +[iframe src="solution" height=600] + +Для обработки событий используйте делегирование, т.е. не более одного обработчика. + +[edit src="source" task/] + +P.S. Обратите внимание -- клик может быть как на маленьком изображении `IMG`, так и на `A` вне него. При этом `event.target` будет, соответственно, либо `IMG`, либо `A`. + +Дополнительно: + +
                +
              • Если получится -- сделайте предзагрузку больших изображений, чтобы при клике они появлялись сразу.
              • +
              • Всё ли в порядке с семантической вёрсткой в HTML исходного документа? Если нет -- поправьте, чтобы было, как нужно.
              • +
              \ No newline at end of file diff --git a/2-ui/2-events-and-interfaces/7-default-browser-action/article.md b/2-ui/2-events-and-interfaces/7-default-browser-action/article.md new file mode 100644 index 00000000..7382d601 --- /dev/null +++ b/2-ui/2-events-and-interfaces/7-default-browser-action/article.md @@ -0,0 +1,152 @@ +# Действия браузера по умолчанию + +Многие события влекут за собой действие браузера. + +Например: +
                +
              • Клик по ссылке инициирует переход на новый URL
              • +
              • Нажатие на кнопку "отправить" в форме -- посылку ее на сервер
              • +
              • Двойной клик на тексте -- инициирует его выделение.
              • +
              + +**Зачастую, мы полностью обрабатываем событие в JavaScript, и такое действие браузера нам не нужно.** + +К счастью, его можно отменить. + +[cut] +## Отмена действия браузера + +Есть два способа отменить действие браузера: +
                +
              • **Основной способ -- это воспользоваться объектом события. Для отмены действия браузера существует стандартный метод `event.preventDefault()`.**
              • +
              • Если же обработчик назначен через `on...` (не через `addEventListener/attachEvent`), то можно просто вернуть `false` из обработчика.
              • +
              + +В следующем примере при клике по ссылке переход не произойдет: + +```html + +Нажми здесь +или +здесь +``` + +[warn header="Возвращать `true` не нужно"] +Вообще говоря, значение, которое возвращает обработчик, игнорируется. + +Единственное исключение -- это `return false` из обработчика, назначенного через `onсобытие`. + +Иногда в коде начинающих разработчиков можно увидеть `return` других значений. Но они не нужны и никак не обрабатываются. +[/warn] + +### Пример: меню + +Рассмотрим задачу, когда нужно создать меню для сайта, например такое: + +```html + +``` + +Данный пример при помощи CSS может выводиться так: + +[iframe height=70 src="menu" link edit] + +**Все элементы меню являются ссылками, то есть тегами ``.** + +Это потому, что некоторые посетители очень любят сочетание "правый клик - открыть в новом окне". Да, мы можем использовать и ` + + +``` + +[smart header="Как отличить реальное нажатие от скриптового?"] +В целях безопасности иногда хорошо бы знать -- инициировано ли действие посетителем или это кликнул скрипт. + +Единственный способ, которым код может отличить реальное нажатие от программного, является проверка свойства `event.isTrusted`. + +Оно на момент написания статьи поддерживается IE и Firefox и равно `true`, если посетитель кликнул сам, и всегда `false` -- если событие инициировал скрипт. +[/smart] + +Браузер автоматически ставит следующие свойства объекта `event`: + +
                +
              • `isTrusted: false` -- означает, что событие сгенерировано скриптом, это свойство изменить невозможно.
              • +
              • `target: null` -- это свойство ставится автоматически позже при `dispatchEvent`.
              • +
              • `type: тип события` -- первый аргумент `new Event`.
              • +
              • `bubbles`, `cancelable` -- по второму аргументу `new Event`.
              • +
              + +Другие свойства события, если они нужны, например координаты для события мыши -- можно присвоить в объект события позже, например: + +```js +var event = new Event("click"); +event.clientX = 100; +event.clientY = 100; +``` + +### Пример с hello + +Можно генерировать события с любыми названиями. + +Для примера сгенерируем совершенно новое событие `"hello"`: + +```html + +

              Привет от скрипта!

              + + +``` + +Обратите внимание: +
                +
              1. Обработчик события `hello` стоит на `document`. Мы его поймаем на всплытии.
              2. +
              3. Вызов `event.preventDefault()` приведёт к тому, что `dispatchEvent` вернёт `false`.
              4. +
              5. Чтобы событие всплывало и его можно было отменить, указан второй аргумент `new Event`.
              6. +
              + +Никакой разницы между встроенными событиями (`click`) и своими (`hello`) здесь нет, они создаются и работают совершенно одинаково. + +## Конструкторы MouseEvent, KeyboardEvent и другие + +Для конкретных типов событий есть свои конструкторы. + +Вот список конструкторов для различных событий интерфейса которые можно найти в спецификации [UI Event](http://www.w3.org/TR/uievents/): +
                +
              • `UIEvent`
              • +
              • `FocusEvent`
              • +
              • `MouseEvent`
              • +
              • `WheelEvent`
              • +
              • `KeyboardEvent`
              • +
              • `CompositionEvent`
              • +
              + +Вместо `new Event("click")` можно вызвать `new MouseEvent("click")`. + +**Конкретный конструктор позволяет указать стандартные свойства для данного типа события.** + +Например: + +```js +//+ run +var e = new MouseEvent("click", { + bubbles: true, + cancelable: true, + clientX: 100, + clientY: 100 +}); + +*!* +alert(e.clientX); // 100 +*/!* +``` + +Сравните это с обычным `Event`: + +```js +//+ run +var e = new Event("click", { + bubbles: true, + cancelable: true, + clientX: 100, + clientY: 100 +}); + +*!* +alert(e.clientX); // undefined +*/!* +``` + +...То есть, "мышиные" свойства можно сразу же в конструкторе указать только если это `MouseEvent`, а `Event` их игнорирует. + +**Использование конкретного конструктора не является обязательным, можно обойтись `Event`.** + +Свойства можно присвоить и явно, после конструктора. Здесь это скорее вопрос удобства и желания следовать правилам. События, которые генерирует браузер, всегда имеют правильный тип. + +Полный список свойств по типам событий вы найдёте в спецификации, например для `MouseEvent`: [MouseEvent Constructor](http://www.w3.org/TR/uievents/#constructor-mouseevent). + +## Свои события + +Для генерации встроенных событий существуют описанные выше конструкторы, а для генерации своих, нестандартных, событий существует конструктор [CustomEvent](http://www.w3.org/TR/dom/#customevent). + +Технически, он абсолютно идентичен `Event`, кроме небольшой детали: у второго аргумента-объекта есть дополнительное свойство `detail`, в котором можно указывать информацию для передачи в событие. + +Например: + +```html + +

              Привет для Васи!

              + + +``` + +Надо сказать, что никто не мешает и в обычное `Event` записать любые свойства. Но `CustomEvent` более явно говорит, что событие не встроенное, а своё, и выделяет отдельно "информационное" поле `detail`, в которое можно записать что угодно без конфликта со стандартными свойствами объекта. + +## Старое API для IE9+ + +В предыдущем стандарте [DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events) была предусмотрена [иерархия событий](http://www.w3.org/TR/DOM-Level-3-Events/#event-interfaces), с различными методами инициализации. + +Она поддерживается как современными браузерами, так и IE9+. Для генерации событий используется немного другой синтаксис, но по возможностям -- всё то же самое, что и в современном стандарте. + +Объект события создаётся вызовом `document.createEvent`: + +```js +var event = document.createEvent(eventInterface); +``` + +Аргументы: +
                +
              • `eventInterface` -- это тип события, например `MouseEvent`, `FocusEvent`, `KeyboardEvent`. В [секции 5 DOM 3 Events](http://www.w3.org/TR/DOM-Level-3-Events/#events-module) есть подробный список, какое событие к какому интерфейсу относится. +
              • +
              + +**На практике можно всегда использовать самый общий интерфейс: `document.createEvent("Event")`.** + +Далее событие нужно инициализовать: + +```js +event.initEvent(type, boolean bubbles, boolean cancelable); +``` + +Аргументы: +
                +
              • `type` -- тип события, например `"click"`.
              • +
              • `bubbles` -- всплывает ли событие.
              • +
              • `cancelable` -- можно ли отменить событие`.
              • +
              + +Эти два кода аналогичны: + +```js +// современный стандарт +var event = new Event("click", { bubbles: true, cancelable: true }); + +// старый стандарт +var event = document.createEvent("Event"); +event.initEvent("click", true, true); +``` + +Единственная разница -- старый стандарт поддерживается IE9+. + +Этот пример с событием `hello` будет работать во всех браузерах, кроме IE8-: + +```html + +

              Привет от скрипта!

              + + +``` + +[smart header="`initMouseEvent`, `initKeyboardEvent` и другие..."] +У конкретных типов событий, например `MouseEvent`, `KeyboardEvent`, есть методы, которые позволяют указать стандартные свойства. + +Они называются по аналогии: `initMouseEvent`, `initKeyboardEvent`. + +Их можно использовать вместо базового `initEvent`, если хочется, чтобы свойства событий соответствовали встроенным браузерным. + +Выглядят они немного страшновато, например (взято из [спецификации](http://www.w3.org/TR/DOM-Level-3-Events/#idl-interface-MouseEvent-initializers)): + +```js +void initMouseEvent ( + DOMString typeArg, // тип + boolean bubblesArg, // всплывает? + boolean cancelableArg, // можно отменить? + AbstractView? viewArg, // объект window, null означает текущее окно + long detailArg, // свойство detail и другие... + long screenXArg, + long screenYArg, + long clientXArg, + long clientYArg, + boolean ctrlKeyArg, + boolean altKeyArg, + boolean shiftKeyArg, + boolean metaKeyArg, + unsigned short buttonArg, + EventTarget? relatedTargetArg); +}; +``` + +Для инициализации мышиного события нужно обязательно указать *все* аргументы, например: + +```html + + + + +``` + +Браузер, по стандарту, может сгенерировать отсутствующие свойства самостоятельно, например `pageX`, но это нужно проверять в конкретных случаях, обычно это не работает или работает некорректно. +[/smart] + +## Антистандарт: IE8- + +В совсем старом IE были "свои" методы `document.createEventObject()` и `elem.fireEvent()`. + +Пример с ними для IE8: + +```html + + + + +``` + +**При помощи `fireEvent` можно сгенерировать только встроенные события.** + +Если указать `"hello"` вместо `"onclick"` в примере выше -- будет ошибка. + +Параметры `bubbles` и `cancelable` настраивать нельзя, браузер использует стандартные для данного типа событий. + +## Кросс-браузерный пример + +Для поддержки IE9+ достаточно использовать методы `document.createEvent` и `event.initEvent`, как показано выше, и всё будет хорошо. + +Если же нужен IE8, то подойдёт такой код: + +```js +function trigger(elem, type){ + if (document.createEvent) { + var event = document.createEvent('Event') : + event.initEvent(type); + return elem.dispatchEvent(event); + } + + var event = document.createEventObject(); + return elem.fireEvent("on"+type, event); +} + +// использование: +trigger(elem, "click"); +``` + +Конечно, надо иметь в виду, что в IE8 события можно использовать только встроенные, а `bubbles` и `cancelable` поставить нельзя. + +## Итого + +
                +
              • Все браузеры, кроме IE, позволяют генерировать любые события, следуя стандарту DOM4.
              • +
              • IE9+ тоже справляется, если использовать вызовы более старого стандарта, и имеет в итоге тот же функционал.
              • +
              • IE8- может генерировать только встроенные события.
              • +
              + +**Несмотря на техническую возможность генерировать браузерные события -- пользоваться ей стоит с большой осторожностью.** + +В 98% случаев, когда разработчик начинающего или среднего уровня хочет сгенерировать *встроенное* событие -- это вызвано "кривой" архитектурой кода, и взаимодействие нужно на уровне выше. + +Как правило события имеет смысл генерировать: +
                +
              • Либо как явный и грубый хак, чтобы заставить работать сторонние библиотеки, в которых не предусмотрены другие средства взаимодействия.
              • +
              • Либо для автоматического тестирования, чтобы скриптом "нажать на кнопку" и посмотреть, произошло ли нужное действие.
              • +
              • Либо при создании своих "элементов интерфейса". Например, никто не мешает при помощи JavaScript создать из `
                ` красивый календарь и генерировать на нём событие `change` при выборе даты. Эту тему мы разовьём позже.
              • +
              diff --git a/2-ui/2-events-and-interfaces/index.md b/2-ui/2-events-and-interfaces/index.md new file mode 100644 index 00000000..a4fa3992 --- /dev/null +++ b/2-ui/2-events-and-interfaces/index.md @@ -0,0 +1,3 @@ +# Основы работы с событиями + +Введение в браузерные события, общие свойства всех событий и приёмы работы с ними. \ No newline at end of file diff --git a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.md b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.md new file mode 100644 index 00000000..9421aca8 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Решение со всеми видами выделения[/edit] \ No newline at end of file diff --git a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html new file mode 100755 index 00000000..530a64aa --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/solution.view/index.html @@ -0,0 +1,108 @@ + + + + + + + + +Клик на элементе выделяет только его.
              +Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.
              +Shift+Клик добавляет промежуток от последнего кликнутого к выделению.
              + +
                +
              • Кристофер Робин
              • +
              • Винни-Пух
              • +
              • Ослик Иа
              • +
              • Мудрая Сова
              • +
              • Кролик. Просто кролик.
              • +
              + + + + diff --git a/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html new file mode 100755 index 00000000..b9607c6a --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/1-selectable-list/source.view/index.html @@ -0,0 +1,47 @@ + + + + + + + + +Клик на элементе выделяет только его.
              +Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.
              +Shift+Клик добавляет промежуток от последнего кликнутого к выделению.
              + +
                +
              • Кристофер Робин
              • +
              • Винни-Пух
              • +
              • Ослик Иа
              • +
              • Мудрая Сова
              • +
              • Кролик. Просто кролик.
              • +
              + + + + + diff --git a/2-ui/3-event-details/1-mouse-clicks/2-tree-coords/source.view/index.html b/2-ui/3-event-details/1-mouse-clicks/2-tree-coords/source.view/index.html new file mode 100755 index 00000000..6ee56ce0 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/2-tree-coords/source.view/index.html @@ -0,0 +1,68 @@ + + + + + + + + +
                +
              • Животные +
                  +
                • Млекопитающие +
                    +
                  • Коровы
                  • +
                  • Ослы
                  • +
                  • Собаки
                  • +
                  • Тигры
                  • +
                  +
                • +
                • Другие +
                    +
                  • Змеи
                  • +
                  • Птицы
                  • +
                  • Ящерицы
                  • +
                  +
                • +
                +
              • +
              • Рыбы +
                  +
                • Аквариумные +
                    +
                  • Гуппи
                  • +
                  • Скалярии
                  • +
                  + +
                • +
                • Морские +
                    +
                  • Морская форель
                  • +
                  +
                • +
                +
              • +
              + + + + + diff --git a/2-ui/3-event-details/1-mouse-clicks/2-tree-coords/task.md b/2-ui/3-event-details/1-mouse-clicks/2-tree-coords/task.md new file mode 100644 index 00000000..e4b22c5b --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/2-tree-coords/task.md @@ -0,0 +1,37 @@ +# Дерево: проверка клика на заголовке + +[importance 3] + +Есть кликабельное JavaScript-дерево UL/LI (см. задачу [](/task/sliding-tree)). + +```html +
                +
              • Млекопитающие +
                  +
                • Коровы
                • +
                • Ослы
                • +
                • Собаки
                • +
                • Тигры
                • +
                +
              • +
              +``` + +При клике на заголовке его список его детей скрывается-раскрывается. +Выглядит это так: (кликайте на заголовки) + +[iframe edit link border="1" src="source"] + +Однако, проблема в том, что скрытие-раскрытие происходит даже при клике *вне заголовка*, на пустом пространстве справа от него. + +**Как скрывать/раскрывать детей только при клике на заголовок?** + +В задаче [](/task/sliding-tree) это решено так: заголовки завёрнуты в элементы `SPAN` и проверяются клики только на них. Представим на минуту, что мы не хотим оборачивать текст в `SPAN`, а хотим оставить как есть. Например, по соображениям производительности, если дерево и так очень большое, ведь оборачивание всех заголовков в `SPAN` увеличит количество DOM-узлов в 2 раза. + +**Решите задачу без обёртывания заголовков в `SPAN`, используя работу с координатами.** + +Исходный документ содержит кликабельное дерево. + +[edit src="source" task/] + +P.S. Задача -- скорее на сообразительность, однако подход может быть полезен в реальной жизни. diff --git a/2-ui/3-event-details/1-mouse-clicks/article.md b/2-ui/3-event-details/1-mouse-clicks/article.md new file mode 100644 index 00000000..1e2e1a78 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/article.md @@ -0,0 +1,334 @@ +# Мышь: клики, кнопка, координаты + +В этой главе мы глубже разберёмся со списком событий мыши, рассмотрим их общие свойства, а также те события, которые связаны с кликом. +[cut] +## Типы событий мыши + +Условно можно разделить события на два типа: "простые" и "комплексные". + +### Простые события + +
              +
              `mousedown`
              +
              Кнопка мыши нажата над элементом.
              +
              `mouseup`
              +
              Кнопка мыши отпущена над элементом.
              +
              `mouseover`
              +
              Мышь появилась над элементом.
              +
              `mouseout`
              +
              Мышь ушла с элемента.
              +
              `mousemove`
              +
              Каждое движение мыши над элементом генерирует это событие.
              +
              + +### Комплексные события + +
              +
              `click`
              +
              Вызывается при клике мышью, то есть при `mousedown`, а затем `mouseup` на одном элементе
              +
              `contextmenu`
              +
              Вызывается при клике правой кнопкой мыши на элементе.
              +
              `dblclick`
              +
              Вызывается при двойном клике по элементу.
              +
              + +Комплексные можно составить из простых, поэтому в теории можно было бы обойтись вообще без них. Но они есть, и это хорошо, потому что с ними удобнее. + +### Порядок срабатывания событий + +Одно действие может вызывать несколько событий. + +Например, клик вызывает сначала `mousedown` при нажатии, а затем `mouseup` и `click` при отпускании кнопки. + +В тех случаях, когда одно действие генерирует несколько событий, их порядок фиксирован. То есть, обработчики вызовутся в порядке `mousedown -> mouseup -> click`. + +Кликните по кнопке ниже и вы увидите, какие при этом происходят события. Попробуйте также двойной клик. + +На тест-стенде ниже все мышиные события записываются, и если между событиями проходит больше 1 секунды, то они для удобства чтения отделяются линией. Также присутствуют свойства `which/button`, по которым можно определить кнопку мыши. Мы их рассмотрим далее. + +
              + +**Каждое событие обрабатывается независимо.** + +Например, при клике события `mouseup + click` возникают одновременно, но обрабатываются последовательно. Сначала полностью завершается обработка `mouseup`, затем запускается `click`. + + +## Получение информации о кнопке: which + +При обработке событий, связанных с кликами мыши, бывает важно знать, какая кнопка нажата. + +**Для получения кнопки мыши в объекте `event` есть свойство `which`.** + +На практике оно используется редко, т.к. обычно обработчик вешается либо `onclick` -- только на левую кнопку мыши, либо `oncontextmenu` -- только на правую. + +Возможны следующие значения: +
                +
              • `event.which == 1` - левая кнопка
              • +
              • `event.which == 2` - средняя кнопка
              • +
              • `event.which == 3` - правая кнопка
              • +
              + +Это свойство не поддерживается IE8-, но его можно получить способом, описанным в конце главы. + +## Правый клик: oncontextmenu + +При клике правой кнопкой мыши браузер показывает свое контекстное меню. Это является его действием по умолчанию: + +```html + + +``` + +...Но если мы не хотим, чтобы показывалось встроенное меню, например потому что показываем своё, то можно отменить действие по умолчанию. + +В примере ниже встроенное меню показано не будет: + +```html + + +``` + +## Модификаторы shift, alt, ctrl и meta + +Во всех событиях мыши присутствует информация о нажатых клавишах-модификаторах. + +Соответствующие свойства: +
                +
              • `shiftKey`
              • +
              • `altKey`
              • +
              • `ctrlKey`
              • +
              • `metaKey` (для Mac)
              • +
              + +Например, кнопка ниже сработает только на Alt+Shift+Клик: + +```html + + + + +``` + +[warn header="Внимание: на Mac вместо `Ctrl` используется `Cmd`"] +На компьютерах Mac кроме клавиш [key Alt], [key Shift] и [key Ctrl], есть ещё одна специальная клавиша: [key Cmd], которой соответствует свойство `metaKey`. + +В большинстве случаев на Mac вместо [key Ctrl] используется [key Cmd]. Там, где пользователь Windows нажимает [key Ctrl+Enter] или [key Ctrl+A], пользователь Mac нажмёт [key Cmd+Enter] или [key Cmd+A], и так далее, почти всегда [key Cmd] вместо [key Ctrl]. + +Поэтому, если мы хотим поддерживать [key Ctrl]+click, то под Mac имеет смысл обрабатывать [key Cmd]+click. + +Даже если бы мы хотели бы заставить пользователей Mac использовать именно [key Ctrl]+click -- это было бы затруднительно. Дело в том, что обычный клик с зажатым [key Ctrl] под Mac работает как *правый клик* и генерирует другое событие: `oncontextmenu`, так что сгенерировать именно [key Ctrl]+click под Mac достаточно сложно. + +Вывод -- чтобы пользователи обоих операционных систем работали с комфортом, в паре с `ctrlKey` нужно обязательно использовать `metaKey`. + +В JS-коде это означает, что для удобства пользователей Mac нужно проверять `if (event.ctrlKey || event.metaKey)`. +[/warn] + +## Координаты мыши + +Все мышиные события предоставляют текущие координаты курсора в двух видах: относительно окна и относительно документа. + +### Относительно окна: clientX/Y + +Есть отличное кросс-браузерное свойство `clientX`(`clientY`), которое содержит координаты курсора относительно `window`. + +При этом, например, если ваше окно размером 500x500, а мышь находится в центре, тогда и `clientX` и `clientY` будут равны 250. + +Можно как угодно прокручивать страницу, но если не двигать при этом мышь, то координаты курсора `clientX/clientY` не изменятся, потому что они считаются относительно окна, а не документа. + +Проведите мышью над полем ввода, чтобы увидеть `clientX/clientY`: + +```html + +``` + + + + +### Относительно документа: pageX/Y + +Координаты курсора относительно документа находятся в свойствах `pageX/pageY`. + +Так как эти координаты -- относительно левого-верхнего узла документа, а не окна, то они учитывают прокрутку. Если прокрутить страницу, а мышь не трогать, то координаты курсора `pageX/pageY` изменятся на величину прокрутки, они привязаны к конкретной точке в документе. + +В IE8- этих свойств нет, но можно получить их способом, описанным в конце главы. + +Проведите мышью над полем ввода, чтобы увидеть `pageX/pageY` (кроме IE8-): + +```html + +``` + + + +[warn header="Устарели: `x, y, layerX, layerY`"] +Некоторые браузеры поддерживают свойства `event.x/y`, `event.layerX/layerY`. + +Эти свойства устарели, они нестандартные и не добавляют ничего к описанным ваше. Использовать их не стоит. +[/warn] + + +## Особенности IE8- + +### Двойной клик + +Все браузеры, кроме IE8-, генерируют `dblclick` *в дополнение* к другим событиям. + +То есть, обычно: +
                +
              • `mousedown` (нажал)
              • +
              • `mouseup+click` (отжал)
              • +
              • `mousedown` (нажал)
              • +
              • `mouseup+click+dblclick` (отжал).
              • +
              + +**IE8- на втором клике не генерирует `mousedown` и `click`.** + +Получается: +
                +
              • `mousedown` (нажал)
              • +
              • `mouseup+click` (отжал)
              • +
              • (нажал второй раз, без события)
              • +
              • `mouseup+dblclick` (отжал).
              • +
              + +**Поэтому отловить двойной клик в IE8-, отслеживая только `click`, нельзя, ведь при втором нажатии его нет. Нужно именно событие `dblclick`.** + +### Свойство which/button + + +В старых IE8- не поддерживалось свойство `which`, а вместо него использовалось свойство `button`, которое является 3-х битным числом, в котором каждому биту соответствует кнопка мыши. Бит установлен в 1, только если соответствующая кнопка нажата. + +Чтобы его расшифровать -- нужна [побитовая операция](/bitwise-operators) `&` ("битовое И"): + +
                +
              • `!!(button & 1) == true` (1й бит установлен), если нажата левая кнопка,
              • +
              • `!!(button & 2) == true` (2й бит установлен), если нажата правая кнопка,
              • +
              • `!!(button & 4) == true` (3й бит установлен), если нажата средняя кнопка.
              • +
              + +Что интересно, при этом мы можем узнать, были ли две кнопки нажаты одновременно, в то время как стандартный `which` такой возможности не даёт. Так что, в некотором смысле, свойство `button` -- более мощное. + +Можно легко сделать функцию, которая будет ставить свойство `which` из `button`, если его нет: + +```js +function fixWhich(e) { + if (!e.which && e.button) { // если which нет, но есть button... (IE8-) + if (e.button & 1) e.which = 1; // левая кнопка + else if (e.button & 4) e.which = 2; // средняя кнопка + else if (e.button & 2) e.which = 3; // правая кнопка + } +} +``` + +### Свойства pageX/pageY [#fixPageXY] + +В IE до версии 9 не поддерживаются свойства `pageX/pageY`, но их можно получить, прибавив к `clientX/clientY` величину прокрутки страницы. + +Более подробно о её вычислении вы можете прочитать в разделе [прокрутка страницы](#page-scroll). + +Мы же здесь приведем готовый вариант, который позволяет нам получить `pageX/pageY` для старых IE: + +```js +function fixPageXY(e) { + if (e.pageX == null && e.clientX != null ) { // если нет pageX.. + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } +} +``` + +## Итого + +События мыши имеют следующие свойства: + +
                +
              • Кнопка мыши: `which` (для IE<9: нужно ставить из `button`)
              • +
              • Элемент, вызвавший событие: `target`
              • +
              • Координаты, относительно окна: `clientX/clientY`
              • +
              • Координаты, относительно документа: `pageX/pageY` (для IE<9: нужно ставить по `clientX/Y` и прокрутке)
              • +
              • Если зажата спец. клавиша, то стоит соответствующее свойство: `altKey`, `ctrlKey`, `shiftKey` или `metaKey` (Mac).
              • +
              • Для поддержки [key Ctrl]+`click` не забываем проверить `if (e.metaKey || e.ctrlKey)`, чтобы пользователи `Mac` тоже были довольны.
              • +
              + + + + +[head] + +[/head] \ No newline at end of file diff --git a/2-ui/3-event-details/10-onload-ondomcontentloaded/article.md b/2-ui/3-event-details/10-onload-ondomcontentloaded/article.md new file mode 100644 index 00000000..592a3161 --- /dev/null +++ b/2-ui/3-event-details/10-onload-ondomcontentloaded/article.md @@ -0,0 +1,214 @@ +# Загрузка документа: DOMContentLoaded, load, beforeunload, unload + +Процесс загрузки HTML-документа, условно, состоит из трёх стадий: +
                +
              • `DOMContentLoaded` -- браузер полностью загрузил HTML, и построил DOM-дерево.
              • +
              • `load` -- браузер загрузил все ресурсы.
              • +
              • `beforeunload/unload` -- уход со страницы.
              • +
              + +Все эти стадии очень важны. На каждую можно повесить обработчик, чтобы совершить полезные действия: +
                +
              • `DOMContentLoaded` -- означает, что все DOM-элементы разметки уже созданы, можно их искать, вешать обработчики, создавать интерфейс, но при этом, возможно, ещё не догрузились какие-то картинки или стили.
              • +
              • `load` -- страница и все ресурсы загружены, используется редко, обычно нет нужды ждать этого момента.
              • +
              • `beforeunload/unload` -- можно проверить, сохранил ли посетитель изменения, уточнить, действительно ли он хочет покинуть страницу.
              • +
              + +Далее мы рассмотрим важные детали этих событий. + +[cut] + +## DOMContentLoaded +Событие `DOMContentLoaded` поддерживается во всех браузерах, кроме IE8-. Про поддержку аналогичного функционала в старых IE мы поговорим в конце главы. + +Обработчик на него вешается только через `addEventListener`: + +```js +document.addEventListener( "DOMContentLoaded", ready, false ); +``` + +Пример: + +```html + + + + +``` + +В примере выше размеры обработчик `DOMContentLoaded` сработает сразу после загрузки документа, не дожидаясь получения картинки. + +Поэтому на момент вывода `alert` и сама картинка будет невидна и её размеры -- неизвестны (кроме случая, когда картинка взята из кеша браузера). + +В своей сути, событие `onDOMContentLoaded` -- простое, как пробка. Полностью создано DOM-дерево -- и вот событие. Но с ним связан ряд существенных тонкостей. + +### DOMContentLoaded и скрипты + +Если в документе есть теги ` +``` + +Такое поведение прописано в стандарте. Его причина -- скрипт может захотеть получить информацию со страницы, зависящую от стилей, например, ширину элемента, и поэтому обязан дождаться загрузки `style.css`. + +**Побочный эффект -- так как событие `DOMContentLoaded` будет ждать выполнения скрипта, то оно подождёт и загрузки стилей, которые идут перед ` + + +``` + +## window.onunload + +Когда человек уходит со страницы или закрывает окно, срабатывает `window.unload`. В нём можно сделать что-то, не требующее ожидания, например, закрыть вспомогательные popup-окна, но отменить сам переход нельзя. + +Это позволяет другое событие -- `window.onbeforeunload`, которое поэтому используется гораздо чаще. + +## window.onbeforeunload [#window.onbeforeunload] + +Если посетитель инициировал переход на другую страницу или нажал "закрыть окно", то обработчик `onbeforeunload` может приостановить процесс и спросить подтверждение. + +Для этого ему нужно вернуть строку, которую браузеры покажут посетителю, спрашивая -- нужно ли переходить. + +Например: + +```js +window.onbeforeunload = function() { + return "Данные не сохранены. Точно перейти?"; +}; +``` + +[warn header="Firefox игнорирует текст, он показывает своё сообщение"] +Firefox игнорирует текст, а всегда показывает своё сообщение. + +Это сделано в целях безопасности. +[/warn] + +Кликните на кнопку в `IFRAME'е` ниже, чтобы поставить обработчик, а затем по ссылке, чтобы увидеть его в действии: + +[iframe src="window-onbeforeunload" border="1" height="80" link] + + +## Эмуляция DOMContentLoaded для IE8- + +Прежде чем что-то эмулировать, заметим, что альтернативой событию `onDOMContentLoaded` является вызов функции `init` из скрипта в самом конце `BODY`, когда основная часть DOM уже готова: + +```html + + ... + + +``` + +Причина, по которой обычно предпочитают именно событие -- одна: удобство. Вешается обработчик и не надо ничего писать в конец `BODY`. + +### Мини-скрипт documentReady +Если вы всё же хотите использовать `onDOMContentLoaded` кросс-браузерно, то нужно либо подключить какой-нибудь фреймворк -- почти все предоставляют такой функционал, либо использовать функцию из мини-библиотеки [jquery.documentReady.js](https://github.com/addyosmani/jquery.parts/blob/master/jquery.documentReady.js). + +Несмотря на то, что в названии содержится слово "jquery", эта библиотечка не требует [jQuery](http://jquery.com). Наоборот, она представляет собой единственную функцию с названием `$`, вызов которой `$(callback)` добавляет обработчик `callback` на `DOMContentLoaded` (можно вызывать много раз), либо, если документ уже загружен -- выполняет его тут же. + +Пример использования: + +```html + + + + + + +
              Текст страницы
              +``` + +Здесь `alert` сработает до загрузки картинки, но после создания DOM, в частности, после появления текста. И так будет для всех браузеров, включая даже очень старые IE. + +[smart header="Как именно эмулируется `DOMContentLoaded`?"] +Технически, эмуляция `DOMContentLoaded` для старых IE осуществляется очень забавно. + +Основной приём -- это попытка прокрутить документ вызовом: + +```js +document.documentElement.doScroll("left"); +``` + +Метод `doScroll` работает только в IE и "методом тыка" было обнаружено, что он бросает исключение, если DOM не полностью создан. + +Поэтому библиотека пытается вызвать прокрутку, если не получается -- через `setTimeout(.., 1)` пытается прокрутить его ещё раз, и так до тех пор, пока действие не перестанет вызывать ошибку. На этом этапе документ считается загрузившимся. + +Внутри фреймов и в очень старых браузерах такой подход может ошибаться, поэтому дополнительно ставится резервный обработчик на `onload`, чтобы уж точно сработал. +[/smart] + + +## Итого + +
                +
              • Самое востребованное событие из описанных -- без сомнения, `DOMContentLoaded`. Многие страницы сделаны так, что инициализуют интерфейсы именно по этому событию. + +Это удобно, ведь можно в `` написать скрипт, который будет запущен в момент, когда все DOM-элементы доступны. + +С другой стороны, следует иметь в виду, что событие `DOMContentLoaded` будет ждать не только, собственно, HTML-страницу, но и внешние скрипты, подключенные тегом ` + + + +Уйти на EXAMPLE.COM + + diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.md b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.md new file mode 100644 index 00000000..958b2434 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.md @@ -0,0 +1,9 @@ +# Подсказка + +Текст на странице пусть будет изначально `DIV`, с классом `img-replace` и атрибутом `data-src` для картинки. + +Функция `replaceImg()` должна искать такие `DIV` и загружать изображение с указанным `src`. По `onload` осуществляется замена `DIV` на картинку. + +# Решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html new file mode 100755 index 00000000..7e4e59b5 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html @@ -0,0 +1,42 @@ + + + + + + + + + +
                +
                +Google +
                + +
                +Яндекс +
                + +
                bing
                + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html new file mode 100755 index 00000000..21acdfeb --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html @@ -0,0 +1,32 @@ + + + + + + + + + + +
                +Google +
                + + +
                +Яндекс +
                + + +
                bing
                + +
                + + +Яндекс +Google +Файла нет (bing) + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md new file mode 100644 index 00000000..cab148fc --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md @@ -0,0 +1,18 @@ +# Красивый "ALT" + +[importance 5] + +Обычно, до того как изображение загрузится (или при отключенных картинках), посетитель видит пустое место с текстом из "ALT". Но этот атрибут не допускает HTML-форматирования. + +При мобильном доступе скорость небольшая, и хочется, чтобы посетитель сразу видел красивый текст. + +**Реализуйте "красивый" (HTML) аналог `alt` при помощи CSS/JavaScript, который затем будет заменён картинкой сразу же как только она загрузится.** А если загрузка не состоится -- то не заменён. + +Демо: (нажмите "перезагрузить", чтобы увидеть процесс загрузки и замены) +[iframe src="solution" height="100"] + +Картинки для `bing` специально нет, так что текст остается "как есть". + +Исходный документ содержит разметку текста и ссылки на изображения. + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.md b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.md new file mode 100644 index 00000000..0100decd --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.md @@ -0,0 +1,9 @@ +# Подсказка + +Создайте переменную-счетчик для подсчёта количества загруженных картинок, и увеличивайте при каждом `onload/onerror`. + +Когда счетчик станет равен количеству картинок -- вызывайте `callback`. + +# Решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.view/index.html new file mode 100755 index 00000000..2b657ab6 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.view/index.html @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/source.view/index.html new file mode 100755 index 00000000..dfb6a365 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/source.view/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/task.md b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/task.md new file mode 100644 index 00000000..101b5b87 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/task.md @@ -0,0 +1,23 @@ +# Загрузить изображения с коллбэком + +[importance 4] + +Создайте функцию `preloadImages(sources, callback)`, которая предзагружает изображения из массива `sources`, и после загрузки вызывает функцию `callback`. + +Пример использования: + +```js +addScripts(["1.jpg", "2.jpg", "3.jpg"], callback); +``` + +Если вдруг возникает ошибка при загрузке -- считаем такое изображение загруженным, чтобы не ломать поток выполнения. + +Такая функция может полезна, например, для фоновой загрузки картинок в онлайн-галерею. + +В исходном документе содержатся ссылки на картинки, а также код для проверки, действительно ли изображения загрузились. Он должен выводить "0", затем "300". + +[edit src="source" task/] + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.md b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.md new file mode 100644 index 00000000..8b36e7ac --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.md @@ -0,0 +1,19 @@ +# Подсказка + +Добавляйте `SCRIPT` при помощи методов `DOM`: + +```js +var script = document.createElement('script'); +script.src = src; + +// в документе может не быть HEAD или BODY, +// но хотя бы один (текущий) SCRIPT в документе есть +var s = document.getElementsByTagName('script')[0]; +s.parentNode.insertBefore(script, s); // перед ним и вставим +``` + +На скрипт повесьте обработчики `onload/onreadystatechange`. + +# Решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/go.js b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/go.js new file mode 100755 index 00000000..777a73b9 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/go.js @@ -0,0 +1,3 @@ +function go () { + alert("ok"); +} diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/index.html new file mode 100755 index 00000000..f7994a00 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/go.js b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/go.js new file mode 100755 index 00000000..c409fbd4 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/go.js @@ -0,0 +1,3 @@ +function go() { + alert("ok"); +} diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/index.html new file mode 100755 index 00000000..2dbe0b70 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/task.md b/2-ui/3-event-details/11-onload-onerror/3-script-callback/task.md new file mode 100644 index 00000000..d6612c4d --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/task.md @@ -0,0 +1,20 @@ +# Скрипт с коллбэком + +[importance 4] + +Создайте функцию `addScript(src, callback)`, которая загружает скрипт с данным `src`, и после его загрузки и выполнения вызывает функцию `callback`. + +Скрипт может быть любым, работа функции не должна зависеть от его содержимого. + +Пример использования: + +```js +// go.js содержит функцию go() +addScript("go.js", function() { + go(); +}); +``` + +Ошибки загрузки обрабатывать не нужно. + +[edit src="source" task/] \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.md b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.md new file mode 100644 index 00000000..9e1ea754 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.md @@ -0,0 +1,9 @@ +# Подсказки + +Создайте переменную-счетчик для подсчёта количества загруженных скриптов. + +Чтобы один скрипт не учитывался два раза (например, `onreadystatechange` запустился при `loaded` и `complete`), учитывайте его состояние в объекте `loaded`. Свойство `loaded[i] = true` означает что `i`-й скрипт уже учтён. + +# Решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/a.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/a.js new file mode 100755 index 00000000..9d6742cd --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/a.js @@ -0,0 +1,3 @@ +function a() { + b(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/b.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/b.js new file mode 100755 index 00000000..ce1689f2 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/b.js @@ -0,0 +1,3 @@ +function b() { + c(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/c.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/c.js new file mode 100755 index 00000000..e343ee11 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/c.js @@ -0,0 +1,3 @@ +function c() { + alert('ok'); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/index.html new file mode 100755 index 00000000..6b75f501 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/index.html @@ -0,0 +1,51 @@ + + + + + + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/a.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/a.js new file mode 100755 index 00000000..9d6742cd --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/a.js @@ -0,0 +1,3 @@ +function a() { + b(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/b.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/b.js new file mode 100755 index 00000000..ce1689f2 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/b.js @@ -0,0 +1,3 @@ +function b() { + c(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/c.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/c.js new file mode 100755 index 00000000..e343ee11 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/c.js @@ -0,0 +1,3 @@ +function c() { + alert('ok'); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/index.html new file mode 100755 index 00000000..58601437 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/task.md b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/task.md new file mode 100644 index 00000000..99016f1c --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/task.md @@ -0,0 +1,23 @@ +# Скрипты с коллбэком + +[importance 5] + +Создайте функцию `addScripts(scripts, callback)`, которая загружает скрипты из массива `scripts`, и *после загрузки и выполнения их всех* вызывает функцию `callback`. + +Скрипт может быть любым, работа функции не должна зависеть от его содержимого. + +Пример использования: + +```js +addScripts(["a.js", "b.js", "c.js"], function() { a() }); +/* функция a() описана в a.js и использует b.js,c.js */ +``` + +
                  +
                • Ошибки загрузки обрабатывать не нужно.
                • +
                • Один скрипт не ждёт другого. Они все загружаются, а по окончании вызывается обработчик `callback`.
                • + +Исходный содержит скрипты `a.js`, `b.js`, `c.js`: + +[edit src="source" task/] + diff --git a/2-ui/3-event-details/11-onload-onerror/article.md b/2-ui/3-event-details/11-onload-onerror/article.md new file mode 100644 index 00000000..9b12cb36 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/article.md @@ -0,0 +1,239 @@ +# Загрузка скриптов, картинок, фреймов: onload и onerror + +Браузер позволяет отслеживать загрузку внешних ресурсов -- скриптов, ифреймов, картинок и других. + +Для этого есть два события: +
                    +
                  • `onload` -- если загрузка успешна.
                  • +
                  • `onerror` -- если при загрузке произошла ошибка.
                  • +
                  + +## Загрузка SCRIPT + +Рассмотрим следующую задачу. + +В браузере работает сложный интерфейс и, чтобы создать очередной компонент, нужно загрузить скрипт с сервера. + +Подгрузить внешний скрипт -- достаточно просто: + +```js +var script = document.createElement('script'); +script.src = "my.js"; + +document.documentElement.appendChild(script); +``` + +...Но, как подгрузки выполнить функцию из этого скрипта? Конечно, можно вызвать её в самом скрипте, но если скрипт -- это универсальная библиотека, то это было бы неправильно. + +### script.onload [#onload] + +Первым нашим помощником станет событие `onload`. + +**Событие `onload` сработает, когда скрипт загрузился *и* выполнился.** + +Например: + +```js +//+ run +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js" +document.documentElement.appendChild(script); + +*!* +script.onload = function() { + alert(jQuery); +} +*/!* +``` + +Это даёт возможность, как в примере выше, получить переменные из скрипта и выполнять объявленные в нём функции. + +...А что, если загрузка скрипта не удалась? Например, такого скрипта на сервере нет (ошибка 404) или сервер "упал" (ошибка 500). + +По-хорошему, такое тоже нужно как-то обрабатывать, хотя бы сообщить посетителю о возникшей проблеме. + +### script.onerror [#onerror] + +Любые ошибки загрузки (но не выполнения) скрипта отслеживаются обработчиком `onerror`. + +Например, для заведомо отсутствующего скрипта: + +```js +//+ run +var script = document.createElement('script'); +script.src = "http://example.com/404.js" +document.documentElement.appendChild(script); + +*!* +script.onerror = function() { + alert("Ошибка: " + this.src); +}; +*/!* +``` + +### IE8-: script.onreadystatechange [#onreadystatechange] + +Примеры выше работают во всех браузерах, кроме IE8-. + +В IE для отслеживания загрузки есть другое событие: `onreadystatechange`. Оно срабатывает многократно, при каждом обновлении состояния загрузки. + +Текущая стадия процесса находится в `script.readyState`: +
                  +
                  `loading`
                  +
                  В процессе загрузки.
                  +
                  `loaded`
                  +
                  Получен ответ с сервера -- скрипт или ошибка. Скрипт на фазе `loaded` может быть ещё не выполнен.
                  +
                  `complete`
                  +
                  Скрипт выполнен.
                  +
                  + +Например, рабочий скрипт: + +```js +//+ run +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; +document.documentElement.appendChild(script); + +*!* +script.onreadystatechange = function() { + alert(this.readyState); // loading -> loaded -> complete +} +*/!* +``` + +Скрипт с ошибкой: + +```js +//+ run +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/404.js"; +document.documentElement.appendChild(script); + +*!* +script.onreadystatechange = function() { + alert(this.readyState); // loading -> loaded +} +*/!* +``` + +Обратим внимание на две особенности: +
                    +
                  • **Стадии могут пропускаться.** + +Если скрипт в кэше браузера -- он сразу даст `complete`. Вы можете увидеть это, если несколько раз запустите первый пример.
                  • +
                  • **Нет особой стадии для ошибки.** + +В примере выше это видно, обработка останавливается на `loaded`. +
                  • +
                  + +Итак, самое надёжное средство для IE8- поймать загрузку (или ошибку загрузки) -- это повесить обработчик на событие `onreadystatechange`, который будет срабатывать и на стадии `complete` и на стадии `loaded`. Так как скрипт может быть ещё не выполнен к этому моменту, то вызов функции лучше сделать через `setTimeout(.., 0)`. + +Пример ниже вызывает `afterLoad` после загрузки скрипта и работает только в IE: + +```js +//+ run +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; +document.documentElement.appendChild(script); + +function afterLoad() { + alert("Загрузка завершена: " + typeof(jQuery)); +} + +*!* +script.onreadystatechange = function() { + if (this.readyState == "complete") { // на случай пропуска loaded + afterLoad(); // (2) + } + + if (this.readyState == "loaded") { + setTimeout(afterLoad, 0); // (1) + + // убираем обработчик, чтобы не сработал на complete + this.onreadystatechange = null; + } +} +*/!* +``` + +Вызов `(1)` выполнится при первой загрузке скрипта, а `(2)` -- при второй, когда он уже будет в кеше, и стадия станет сразу `complete`. + +Функция `afterLoad` может и не обнаружить `jQuery`, если при загрузке была ошибка, причём не важно какая -- файл не найден или синтаксис скрипта ошибочен. + +### Кросс-браузерное решение + +Для кросс-браузерной обработки загрузки скрипта или её ошибки поставим обработчик на все три события: `onload`, `onerror`, `onreadystatechange`. + +Пример ниже выполняет функцию `afterLoad` после загрузки скрипта *или* при ошибке. + +Работает во всех браузерах: + +```js +//+ run +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; +document.documentElement.appendChild(script); + +function afterLoad() { + alert("Загрузка завершена: " + typeof(jQuery)); +} + +script.onload = script.onerror = function() { + if (!this.executed) { // выполнится только один раз + this.executed = true; + afterLoad(); + } +}; + +script.onreadystatechange = function() { + var self = this; + if (this.readyState == "complete" || this.readyState == "loaded") { + setTimeout(function() { self.onload() }, 0);// сохранить "this" для onload + } +}; +``` + +## Загрузка других ресурсов + +Поддержка этих событий для других типов ресурсов различна: + +
                  +
                  ``, `` (стили)
                  +
                  Поддерживает `onload/onerror` во всех браузерах.
                  +
                  `"); + return htmlfile.body.lastChild; // window in .document.parentWindow +} diff --git a/3-more/10-ajax/13-ajax-iframe-htmlfile/activex/index.html b/3-more/10-ajax/13-ajax-iframe-htmlfile/activex/index.html new file mode 100755 index 00000000..9b96f404 --- /dev/null +++ b/3-more/10-ajax/13-ajax-iframe-htmlfile/activex/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/3-more/10-ajax/13-ajax-iframe-htmlfile/activex/samedomain.php b/3-more/10-ajax/13-ajax-iframe-htmlfile/activex/samedomain.php new file mode 100755 index 00000000..fe97317b --- /dev/null +++ b/3-more/10-ajax/13-ajax-iframe-htmlfile/activex/samedomain.php @@ -0,0 +1,8 @@ + + diff --git a/3-more/10-ajax/13-ajax-iframe-htmlfile/article.md b/3-more/10-ajax/13-ajax-iframe-htmlfile/article.md new file mode 100644 index 00000000..8c98c8f7 --- /dev/null +++ b/3-more/10-ajax/13-ajax-iframe-htmlfile/article.md @@ -0,0 +1,120 @@ +# IFRAME в IE: cкрытие в ActiveX "htmlfile" + +Так как в IE6-9 современные транспорты не поддерживаются, то приходится использовать `iframe`. И здесь есть две проблемы: + +
                    +
                  • При POST в `IFRAME` раздаётся звук "клика".
                  • +
                  • Пока идёт запрос в ифрейм, показываются часики и анимируется загрузка страницы.
                  • +
                  + +И того и другого можно избежать, и сделать запрос полностью незаметным, если воспользоваться некоторыми специфическими возможностями Internet Explorer. +[cut] + +В Internet Explorer есть безопасный ActiveX-объект `htmlfile`. IE не требует разрешений на его создание. Фактически, это независимый HTML-документ. + +Оказывается, если `iframe` создать в нём, то никакой анимации и звуков не будет. + +Итак, схема: +
                    +
                  1. Основное окно `main` создёт вспомогательный объект: `new ActiveXObject("htmlfile")`. Это HTML-документ со своим `window`, похоже на встроенный `iframe`.
                  2. +
                  3. В `htmlfile` записывается `iframe`.
                  4. +
                  5. Цепочка общения: основное окно - `htmlfile` - ифрейм.
                  6. +
                  + +## iframeActiveXGet + +На самом деле всё еще проще, если посмотреть на код: + +Метод `iframeActiveXGet` по существу идентичен обычному `iframeGet`, которое мы рассмотрели. Единственное отличие -- вместо `createIframe` используется особый метод `createActiveXFrame`: + +```js +function iframeActiveXGet(url, onSuccess, onError) { + + var iframeOk = false; + + var iframeName = Math.random(); +*!* + var iframe = createActiveXFrame(iframeName, url); +*/!* + + CallbackRegistry[iframeName] = function(data) { + iframeOk = true; + onSuccess(data); + } + + iframe.onload = function() { + iframe.parentNode.removeChild(iframe); // очистка + delete CallbackRegistry[iframeName]; + if (!iframeOk) onError(); // если коллбэк не вызвался - что-то не так + } + +} +``` + +## createActiveXFrame + +В этой функции творится вся IE-магия: + +```js +function createActiveXFrame(name, src) { + // (1) + var htmlfile = window.htmlfile; + if (!htmlfile) { + htmlfile = window.htmlfile = new ActiveXObject("htmlfile"); + htmlfile.open(); + // (2) + htmlfile.write(""); + htmlfile.close(); + // (3) + htmlfile.parentWindow.CallbackRegistry = CallbackRegistry; + } + + // (4) + src = src || 'javascript:false'; + htmlfile.body.insertAdjacentHTML('beforeEnd', + ""); + return htmlfile.body.lastChild; +} +``` + +
                    +
                  1. Вспомогательный объект `htmlfile` будет один и он будет глобальным. Можно и спрятать переменную в замыкании. Смысл в том, что в один `htmlfile` можно записать много ифреймов, так что не будем множить сущности и занимать ими лишнюю память.
                  2. +
                  3. В `htmlfile` можно записать любой текст и, при необходимости, через `document.write(' +``` + +Здесь `parent'ом` для `iframe'а` будет `htmlfile`, т.е. `CallbackRegistry` будет искаться среди переменных соответствующего ему окна, а вовсе не верхнего `window`. + +Окно для `htmlfile` доступно как `htmlfile.parentWindow`, копируем в него ссылку на реестр коллбэков `CallbackRegistry`. Теперь ифрейм его найдёт.
                  4. +
                  5. Далее вставляем ифрейм в документ. В старых `IE` нельзя поменять `name` ифрейму через DOM, поэтому вставляем строкой через `insertAdjacentHTML`.
                  6. +
                  + +Пример в действии (только IE): + +[iframe src="activex" border=1 link zip] + +Запрос, который происходит, полностью незаметен. Мы ещё воспользуемся описанным способом для реализации COMET. + +## POST через ActiveX + +Ранее мы рассмотрели метод GET. Метод POST подразумевает отправку формы в ифрейм. + +Для этого форму нужно добавить не во внешнее окно, а в `htmlfile`, через `htmlfile.appendChild`. + +В остальном -- всё также, как и при обычной отправке через ифрейм. + + + + + + + + +[head] + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/14-ajax-iframe-xdomain/article.md b/3-more/10-ajax/14-ajax-iframe-xdomain/article.md new file mode 100644 index 00000000..eee67293 --- /dev/null +++ b/3-more/10-ajax/14-ajax-iframe-xdomain/article.md @@ -0,0 +1,199 @@ +# IFRAME для кросс-доменных запросов + +Современные браузеры поддерживают множество альтернатив ифреймам. В первую очередь, это современный [XMLHttpRequest](/ajax-xmlhttprequest), который может делать любые запросы, в том числе кросс-доменные и с пересылкой файлов. + +А в IE6-9 ифреймы в некоторых ситуациях незаменимы, поэтому способ, описанный в этой статье, в первую очередь используется для поддержки этих браузеров. +Существует несколько вариантов реализации кросс-доменных запросов. Конкретный выбор зависит от версии IE, которую мы собираемся поддерживать. + +## IE8+: postMessage-ответ + +IE8 и выше поддерживают интерфейс [postMessage](https://developer.mozilla.org/en-US/docs/DOM/window.postMessage) для общения между окнами с разных доменов. + +Для его использования серверный код должен ответить на запрос примерно так: + +```js + +``` + +Браузер проверит, действительно ли родительское окно `parent` с домена `parentDomain`, и если это так, то на нём будет вызвано событие `onmessage`, которое получит доступ к тексту `message`. + +В `message` должна содержаться информация о том, на какой запрос пришёл ответ, и данные. Так как кросс-браузерно поддерживаются только сообщения-строки, то передать эту информацию можно через JSON. + +Серверный код, который это делает: + +```php + + +``` + +Код на клиенте, который будет реагировать на `postMessage`: + +```js +window.onmessage = function(event) { + // распаковать сообщение + var message = JSON.parse(event.data); + // вызвать обработку результата запроса + CallbackRegistry[message.name](message.body); +} +``` + +Ещё одна небольшая тонкость -- время вызова `iframe.onload`. + +Задача этого обработчика -- проверить, всё ли вызвалось. Для этого он должен отработать обязательно *после* `onmessage`. А вызов `postMessage` -- асинхронный, поэтому нужно отложить проверку на `setTimeout(..,0)`. + +При этом получится следующий поток выполнения: + +
                    +
                  1. Пришёл ответ с сервера, вызвал `postMessage`.
                  2. +
                  3. Событие `onmessage` встало в очередь выполнения.
                  4. +
                  5. Обработано окончание загрузки, вызвался `iframe.onload`, поставил в очередь проверку.
                  6. +
                  7. Сработает `onmessage`, а затем проверка.
                  8. +
                  + +Полный код кросс-доменного `iframeGet` (`iframePost` -- аналогично): + +```js +//+ src="postmessage.js" +``` + +Пример в действии: + +[iframe src="postmessage" border="1" link zip] + +### postMessage-прокси + +Если нужно часто обмениваться данными с одним доменом, то можно загрузить `iframe` с этого домена, содержащий код `postMessage` и `onmessage`: + +```html + +``` + +Время от времени этому `iframe` пересылаются сообщения, он обменивается информацией со своей страницей (можно через `XMLHttpRequest`) и отвечает основной странице. + +При этом ифрейм будет только один, пересоздавать ничего не нужно. + + +## IE6: передача через window.name + +Этот способ активно используется, так как с одной стороны покрывает ряд важных сценариев использования, а с другой -- поддерживается всеми браузерами, включая IE6+. + +У `IFRAME` есть атрибут `name`, который задаёт "имя окна". Обычно он используется для отправки форм в данный ифрейм или для доступа к внутреннему окну через `window.frames[name]`. + +Но можно и использовать его для другой, непредусмотренной цели -- передачи данных. + +Дело в том, что имя окна не меняется при навигации в `IFRAME`, и при этом доступно из JavaScript. Поэтому если одна страница поставила `window.name = "Вася"`, а потом посетитель нажал на ссылку в `IFRAME` и перешёл на другую страницу (домен не важен), то она сможет прочитать "Вася" из `window.name`. + +Это делает возможным следующий алгоритм: + +
                    +
                  1. Страница на основном домене создаёт `IFRAME` и отправляет в него запрос *на другой домен*.
                  2. +
                  3. Сервер отвечает кодом, который выглядит примерно так: + +```php + + +``` + +
                  4. +
                  5. Страница по `iframe.onload` читает `iframe.contentWindow.name` и получает данные.
                  6. +
                  + +Ниже реализован метод POST, т.к. кросс-доменный GET можно сделать и при помощи [Транспорта SCRIPT](/ajax-jsonp): + +```js +//+ src="name.js" +``` + +Обратите внимание на комментарии, они более подробно описывают важные детали коммуникации. + +Пример в действии: + +[iframe src="name" border="1" zip link] + +### Сфера использования + +У метода `window.name` почти нет недостатков. + +
                    +
                  • Он работает во всех браузерах.
                  • +
                  • Он прост в реализации.
                  • +
                  • В `window.name` кросс-браузерно влезает до 2МБ.
                  • +
                  • Пустой документ `blank.html` будет сразу же закэширован браузером, и в дальнейшем лишнего запроса не будет.
                  • +
                  + +Единственное ограничение -- ответ до 2МБ. Многие сценарии использования, например кросс-доменная авторизация, легко вписываются в них. + +## IE6+: hash-прокси + +Этот способ представляет собой отвратительнейший хак. Он заключается в том, что при общении между основной страницей и ифреймом с другого домена -- они могут легко менять `hash` друг другу (часть пути после `#`). + +Считается, что это безопасно, т.к. изменение хэша -- всего лишь прыжок по странице, поэтому браузеры это разрешают. + +При этом читать хэши друг друга нельзя, можно только писать в них. + +Соответственно, логика взаимодействия такая: + +
                    +
                  1. Главная страница подключает ифрейм с другого домена.
                  2. +
                  3. Ифрейму меняется `hash`, он видит это и понимает, что ему "хотят что-то этим сказать", обрабатывает полученное сообщение.
                  4. +
                  5. Когда ифрейм готов, он, в свою очередь, меняет хэш родителю. Тот принимает ответ.
                  6. +
                  + +При этом у родителя и ифрейма существует протокол общения через хэш. Ограничение -- не более 4kb (минус длина базовой части URL) на сообщение. + +Обход проблем: +
                    +
                  • **При такой реализации на п.3 окно-родитель будет "прыгать" -- ему же меняют хэш!** + +Поэтому в реальности используют три ифрейма: родительское окно `main` включает ифрейм с того же домена `localProxy.html`, а уже он включает в себя ифрейм с другого домена `remoteProxy.html`. + +Общение осуществляется по цепочке `main -> localProxy -> remoteProxy -> localProxy -> main`. Хэш меняется у промежуточного прокси, который сообщает об этом родительскому окну (он на одном домене с ним). + +Можно сделать фреймы `localProxy` и `remoteProxy` на одном уровне. Тогда не во всех браузерах будет работать (в Opera не будет?), но в IE всё (вроде!) будет ок.
                  • **Как окну(или ифрейму) обнаружить изменение хэща?** + +В старых браузерах отсутствует событие `onhashchange`, поэтому используются два подхода: +
                      +
                    1. Окно раз в 50мс проверяет, поменялся ли хэш.
                    2. +
                    3. Используется `window.onresize` как сигнал: когда ифрейму меняем хэш -- тут же немного изменим и его размер, на нём сработает событие `onresize`.
                    4. +
                    + +
                  + +Автор этих строк реализовывал такой протокол в очень бородатые времена, несколько раз участвовал в обсуждениях проектов, использующих такое решение -- и всегда был удивлён, насколько стабильно оно работает. Несмотря на хакерскую природу самого решения. + +В этом случае ифрейм перегружать не надо, поэтому большей частью такой подход используется даже не для AJAX, а для общения между постоянно встроенным виджетом-ифреймом и основной страницей. + +Область применения -- когда обязательна поддержка IE6,7 и никакие другие способы, почему-то, не подходят. + + +[head] + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/14-ajax-iframe-xdomain/name.js b/3-more/10-ajax/14-ajax-iframe-xdomain/name.js new file mode 100755 index 00000000..1221f9f9 --- /dev/null +++ b/3-more/10-ajax/14-ajax-iframe-xdomain/name.js @@ -0,0 +1,36 @@ + +function iframePost(url, data, onSuccess, onError) { + + var iframeName = Math.random(); + var iframe = createIframe(iframeName); + + // onload после append, чтобы не срабатывал лишний раз + iframe.onload = function() { + + var newName = iframe.contentWindow.name; + + if (newName == iframeName) { + // имя получили, но оно осталось тем же - значит сервер его не заменил + // значит что-то не так в ответе (ошибка сервера или неверный формат) + onError(); + return; + } + + // имя получено, коммуникация завершена + iframe.parentNode.removeChild(iframe); // очистка + + // такой eval можно использовать вместо JSON.parse для старых IE, + // при условии доверия содержимому к newName + // если доверия нет -- используйте библиотеку json2.js + onSuccess( eval('(' + newName +')') ); + }; + + // для IE<9 нужно использовать attachEvent вместо iframe.onload + if (iframe.attachEvent && !iframe.addEventListener) { + iframe.attachEvent("onload", iframe.onload); + iframe.onload = null; + } + + postToIframe(url, data, iframeName); +} + diff --git a/3-more/10-ajax/14-ajax-iframe-xdomain/name/.zip b/3-more/10-ajax/14-ajax-iframe-xdomain/name/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/14-ajax-iframe-xdomain/name/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/14-ajax-iframe-xdomain/name/anydomain.php b/3-more/10-ajax/14-ajax-iframe-xdomain/name/anydomain.php new file mode 100755 index 00000000..c2bcc4ef --- /dev/null +++ b/3-more/10-ajax/14-ajax-iframe-xdomain/name/anydomain.php @@ -0,0 +1,13 @@ + + diff --git a/3-more/10-ajax/14-ajax-iframe-xdomain/name/blank.html b/3-more/10-ajax/14-ajax-iframe-xdomain/name/blank.html new file mode 100755 index 00000000..95222a60 --- /dev/null +++ b/3-more/10-ajax/14-ajax-iframe-xdomain/name/blank.html @@ -0,0 +1,2 @@ + + diff --git a/3-more/10-ajax/14-ajax-iframe-xdomain/name/iframe.js b/3-more/10-ajax/14-ajax-iframe-xdomain/name/iframe.js new file mode 100755 index 00000000..f7cf9169 --- /dev/null +++ b/3-more/10-ajax/14-ajax-iframe-xdomain/name/iframe.js @@ -0,0 +1,49 @@ + + +function createIframe(name, src, debug) { + src = src || 'javascript:false'; // пустой src + + var tmpElem = document.createElement('div'); + + // в старых IE нельзя присвоить name после создания iframe, поэтому создаём через innerHTML + tmpElem.innerHTML = '"); + iframe = htmlfile.body.lastChild; // window in .document.parentWindow + } + +} \ No newline at end of file diff --git a/3-more/10-ajax/16-ajax-summary/article.md b/3-more/10-ajax/16-ajax-summary/article.md new file mode 100644 index 00000000..d1cade8b --- /dev/null +++ b/3-more/10-ajax/16-ajax-summary/article.md @@ -0,0 +1,113 @@ +# Таблица транспортов и их возможностей + +Здесь мы подведём итог раздела, сравним транспорты и их возможности. +[cut] + +## Способы опроса сервера + +Основные способы опроса сервера: +
                    +
                  1. **Частые опросы** -- регулярно к серверу отправляется запрос за данными. Сервер тут же отвечает на него, возвращая данные, если они есть. Если нет -- получается, что запрос был зря. + +Этот способ очень лёгок в реализации, но приводит к большому количеству лишних запросов, поэтому мы его далее не рассматриваем. +
                  2. +
                  3. **Длинные опросы** -- к серверу отправляется запрос за данными. Сервер не отвечает на него, пока данные не появятся. Когда данные появились -- ответ с ними отправляется в браузер, и тот тут же делает новый запрос. + +Способ хорош, пока сообщений не слишком много. В идеальном случае соединение почти всё время висит открытым, лишь иногда сервер отвечает на него, доставляя данные в браузер. + +Также удобен в реализации, но даёт большое количество висящий соединений на сервере. Не все сервера хорошо поддерживают это. Например, `Apache` будет есть очень много памяти. +
                  4. +
                  5. **Потоковое соединение** -- открыто соединение к серверу, и через него непрерывно поступают данные.
                  6. +
                  + +## Таблица транспортов +Основные характеристики всех транспортов, которые мы обсуждали в деталях, собраны в этой таблице. + +Они были детально рассмотрены в предыдущих главах раздела. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                  `XMLHttpRequest``IFRAME``SCRIPT``EventSource``WebSocket`
                  Кросс-доменностьда, кроме IE<10x1да, сложности в IE<8i1дадада
                  МетодыЛюбыеGET / POSTGETGETСвой протокол
                  COMETДлинные опросыx2Непрерывное соединениеДлинные опросыНепрерывное соединениеНепрерывное соединение в обе стороны
                  ПоддержкаВсе браузеры, ограничения в IE<10x3Все браузерыВсе браузерыКроме IEIE 10, FF11, Chrome 16, Safari 6, Opera 12.5w1
                  + +Пояснения: + +
                  +
                  `XMLHttpRequest`
                  +
                  +
                    +
                  1. В IE8-9 поддерживаются кросс-доменные GET/POST запросы с ограничениями через `XDomainRequest`.
                  2. +
                  3. Можно говорить об ограниченной поддержке непрерывного соединения через `onprogress`, но это событие вызывается не чаще чем в `50ms` и не гарантирует получение полного пакета данных. Например, сервер может записать "Привет!", а событие вызовется один раз, когда браузер получил "При". Поэтому наладить обмен пакетами сообщений с его помощью затруднительно. +
                  4. +
                  5. Многие возможности современного стандарта включены в IE лишь с версии 10.
                  6. +
                  +
                  +
                  `IFRAME`
                  +
                  +
                    +
                  1. Во всех современных браузерах и IE8 кросс-доменность обеспечивает `postMessage`. В более старых браузерах возможны решения через `window.name` и `hash`.
                  2. +
                  +
                  +
                  `WebSocket`
                  +
                  +
                    +
                  1. Имеется в виду поддержка окончательной редакции протокола [RFC 6455](http://tools.ietf.org/html/rfc6455). Более старые браузеры могут поддерживать черновики протокола. IE<10 не поддерживает `WebSocket`.
                  2. +
                  +
                  +
                  + +Существует также транспорт, не рассмотренный здесь: +
                    +
                  • XMLHttpRequest с флагом `multipart`, только для Firefox. + +При указании свойства `xhr.multipart = true` и специального multipart-формата ответа сервера, Firefox инициирует `onload` при получении очередной части ответа. + +Ответ может состоять из любого количества частей, досылаемых по инициативе сервера. +
                  • +
                  +Мы не рассматривали его, т.к. Firefox поддерживает и другие, стандартные, транспорты. + + + + +[head] + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/2-ajax-nodejs/article.md b/3-more/10-ajax/2-ajax-nodejs/article.md new file mode 100644 index 00000000..1184ed57 --- /dev/null +++ b/3-more/10-ajax/2-ajax-nodejs/article.md @@ -0,0 +1,136 @@ +# Node.JS для решения задач + +В этом разделе предлагаются задачи по теме AJAX. + +Конечно же, они требуют взаимодействия с сервером. Чтобы не давать преимущества ни одному из серверных языков программирования ;), мы будем использовать серверную часть, написанную на Node.JS. + +[cut] + +Если вы не использовали Node.JS ранее -- не беспокойтесь. Здесь нашей целью является преимущественно клиентская часть, поэтому прямо сейчас изучать Node.JS не обязательно. Серверные скрипты уже готовы. Нужно только поставить и добавить модулей, чтобы их запускать. + + +## Установка + +Для настройки окружения будет достаточно сделать два шага: + +
                    +
                  1. Сначала установите сам сервер Node.JS. + +Если у вас Unix-система -- рекомендуется собрать последнюю версию из исходников, а также NPM. Вы справитесь. + +Если Windows -- посетите сайт http://nodejs.org или скачайте установщик (32 или 64-битный) с расширением `.msi` из http://nodejs.org/dist/latest/. +
                  2. +
                  3. Выберите директорию, в которой будете решать задачи. Запустите в ней: + +``` +npm install node-static +``` + +Это установит в текущую директорию модуль [node-static](https://github.com/cloudhead/node-static), который станет автоматически доступным для скриптов из поддиректорий. + +**Если у вас Windows и команда не сработала, то скорее всего дело в том, что "не подхватились" новые пути. Перезапустите ваш файловый менеджер или консоль.** +
                  4. +
                  + +## Проверка + +Проверьте инсталяцию. + +Для этого: +
                    +
                  1. Создайте какую-нибудь поддиректорию и в ней файл `server.js` с таким содержимым: + +```js +var http = require('http'); +var static = require('node-static'); +var file = new static.Server('.'); + +http.createServer(function (req, res) { + file.serve(req, res); +}).listen(8080); + +console.log('Server running on port 8080'); +``` + +
                  2. +
                  3. Запустите его: `node server.js`. + +Должно вывести: + +``` +Server running on port 8080 +``` + +[warn header="Нельзя запустить больше одного сервера одновременно!"] +При попытке запуска двух серверов (например, в разных консолях) -- будет конфликт портов и ошибка. +[/warn] + +
                  4. +
                  5. Откройте в браузере http://127.0.0.1:8080/server.js. + +Должно вывести код файла `server.js`.
                  6. +
                  + +Если всё работает -- отлично, теперь вы готовы решать задачи. + +## Примеры + +В примерах, за редким исключением, для краткости будет приводиться не полный скрипт на Node.JS, а только код обработки запроса. + +Например, вместо: + +```js +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); + +function accept(req, res) { + + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache' + }); + + res.end("OK"); +} + +http.createServer(accept).listen(8080); +``` + +...Будет только функция `accept`, или даже только её содержимое: + +```js +res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache' +}); +``` + +## Основные методы + +В функции `accept` используются два объекта: +
                    +
                  • `req` -- объект запроса ("request"), из него читаем данные.
                  • +
                  • `res` -- объект ответа ("response"), в него пишем данные. +
                      +
                    • вызов `res.writeHead(HTTP-код, [строка статуса], {заголовки})` пишет заголовки.
                    • +
                    • вызов `res.write(txt)` пишет текст в ответ.
                    • +
                    • вызов `res.end(txt)` -- завершает запрос ответом.
                    • +
                    +
                  • +
                  + +## Демо + +Кроме просмотра кода, можно будет попробовать и скачать различные демки. + +Вот пример демо, можете попробовать нажать на кнопку -- она работает. + + +[iframe src="example" border="1" zip] + +Если хотите посмотреть пример поближе и поиграть с ним -- скачайте полный код, он будет работать и на вашем Node.JS. + +## Больше о Node.JS + +Больше о сервере Node.JS можно узнать в [скринкасте по Node.JS](/nodejs-screencast). diff --git a/3-more/10-ajax/2-ajax-nodejs/example/.zip b/3-more/10-ajax/2-ajax-nodejs/example/.zip new file mode 100755 index 00000000..e69de29b diff --git a/3-more/10-ajax/2-ajax-nodejs/example/index.html b/3-more/10-ajax/2-ajax-nodejs/example/index.html new file mode 100755 index 00000000..91eab08b --- /dev/null +++ b/3-more/10-ajax/2-ajax-nodejs/example/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/3-more/10-ajax/2-ajax-nodejs/example/index.js b/3-more/10-ajax/2-ajax-nodejs/example/index.js new file mode 100755 index 00000000..0161637d --- /dev/null +++ b/3-more/10-ajax/2-ajax-nodejs/example/index.js @@ -0,0 +1,31 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); +var file = new static.Server('.'); + + +function accept(req, res) { + + // если URL запроса /vote, то... + if (req.url == '/vote') { + // через 1.5 секунды ответить сообщением + setTimeout(function() { + res.end('Ваш голос принят: ' + new Date()); + }, 1500); + } else { + // иначе считаем это запросом к обычному файлу и выводим его + file.serve(req, res); // (если он есть) + } + +} + + +// ------ этот код запускает веб-сервер ------- + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/.zip b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/index.html b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/index.html new file mode 100755 index 00000000..605c5092 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/index.js b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/index.js new file mode 100755 index 00000000..818ce465 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/index.js @@ -0,0 +1,29 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); +var file = new static.Server('.', { cache: 0 }); + + +function accept(req, res) { + + if (req.url == '/phones.json') { + // искусственная задержка для наглядности + setTimeout(function() { + file.serve(req, res); + }, 2000); + } else { + file.serve(req, res); + } + +} + + +// ------ запустить сервер ------- + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/phones.json b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/phones.json new file mode 100755 index 00000000..472b2b16 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-async/phones.json @@ -0,0 +1,155 @@ +[ + { + "age": 0, + "id": "motorola-xoom-with-wi-fi", + "imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg", + "name": "Motorola XOOM\u2122 with Wi-Fi", + "snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 1, + "id": "motorola-xoom", + "imageUrl": "img/phones/motorola-xoom.0.jpg", + "name": "MOTOROLA XOOM\u2122", + "snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 2, + "carrier": "AT&T", + "id": "motorola-atrix-4g", + "imageUrl": "img/phones/motorola-atrix-4g.0.jpg", + "name": "MOTOROLA ATRIX\u2122 4G", + "snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone." + }, + { + "age": 3, + "id": "dell-streak-7", + "imageUrl": "img/phones/dell-streak-7.0.jpg", + "name": "Dell Streak 7", + "snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around." + }, + { + "age": 4, + "carrier": "Cellular South", + "id": "samsung-gem", + "imageUrl": "img/phones/samsung-gem.0.jpg", + "name": "Samsung Gem\u2122", + "snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price." + }, + { + "age": 5, + "carrier": "Dell", + "id": "dell-venue", + "imageUrl": "img/phones/dell-venue.0.jpg", + "name": "Dell Venue", + "snippet": "The Dell Venue; Your Personal Express Lane to Everything" + }, + { + "age": 6, + "carrier": "Best Buy", + "id": "nexus-s", + "imageUrl": "img/phones/nexus-s.0.jpg", + "name": "Nexus S", + "snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet." + }, + { + "age": 7, + "carrier": "Cellular South", + "id": "lg-axis", + "imageUrl": "img/phones/lg-axis.0.jpg", + "name": "LG Axis", + "snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens" + }, + { + "age": 8, + "id": "samsung-galaxy-tab", + "imageUrl": "img/phones/samsung-galaxy-tab.0.jpg", + "name": "Samsung Galaxy Tab\u2122", + "snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility." + }, + { + "age": 9, + "carrier": "Cellular South", + "id": "samsung-showcase-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg", + "name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors" + }, + { + "age": 10, + "carrier": "Verizon", + "id": "droid-2-global-by-motorola", + "imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg", + "name": "DROID\u2122 2 Global by Motorola", + "snippet": "The first smartphone with a 1.2 GHz processor and global capabilities." + }, + { + "age": 11, + "carrier": "Verizon", + "id": "droid-pro-by-motorola", + "imageUrl": "img/phones/droid-pro-by-motorola.0.jpg", + "name": "DROID\u2122 Pro by Motorola", + "snippet": "The next generation of DOES." + }, + { + "age": 12, + "carrier": "AT&T", + "id": "motorola-bravo-with-motoblur", + "imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg", + "name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122", + "snippet": "An experience to cheer about." + }, + { + "age": 13, + "carrier": "T-Mobile", + "id": "motorola-defy-with-motoblur", + "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg", + "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122", + "snippet": "Are you ready for everything life throws your way?" + }, + { + "age": 14, + "carrier": "T-Mobile", + "id": "t-mobile-mytouch-4g", + "imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg", + "name": "T-Mobile myTouch 4G", + "snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi." + }, + { + "age": 15, + "carrier": "US Cellular", + "id": "samsung-mesmerize-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg", + "name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors" + }, + { + "age": 16, + "carrier": "Sprint", + "id": "sanyo-zio", + "imageUrl": "img/phones/sanyo-zio.0.jpg", + "name": "SANYO ZIO", + "snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value." + }, + { + "age": 17, + "id": "samsung-transform", + "imageUrl": "img/phones/samsung-transform.0.jpg", + "name": "Samsung Transform\u2122", + "snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d." + }, + { + "age": 18, + "id": "t-mobile-g2", + "imageUrl": "img/phones/t-mobile-g2.0.jpg", + "name": "T-Mobile G2", + "snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible." + }, + { + "age": 19, + "id": "motorola-charm-with-motoblur", + "imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg", + "name": "Motorola CHARM\u2122 with MOTOBLUR\u2122", + "snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service." + } +] diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/.zip b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/index.html b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/index.html new file mode 100755 index 00000000..eb32f652 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/index.html @@ -0,0 +1,53 @@ + + + + + + + +
                    + + + + + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/index.js b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/index.js new file mode 100755 index 00000000..818ce465 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/index.js @@ -0,0 +1,29 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); +var file = new static.Server('.', { cache: 0 }); + + +function accept(req, res) { + + if (req.url == '/phones.json') { + // искусственная задержка для наглядности + setTimeout(function() { + file.serve(req, res); + }, 2000); + } else { + file.serve(req, res); + } + +} + + +// ------ запустить сервер ------- + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/phones.json b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/phones.json new file mode 100755 index 00000000..472b2b16 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/phones-list/phones.json @@ -0,0 +1,155 @@ +[ + { + "age": 0, + "id": "motorola-xoom-with-wi-fi", + "imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg", + "name": "Motorola XOOM\u2122 with Wi-Fi", + "snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 1, + "id": "motorola-xoom", + "imageUrl": "img/phones/motorola-xoom.0.jpg", + "name": "MOTOROLA XOOM\u2122", + "snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 2, + "carrier": "AT&T", + "id": "motorola-atrix-4g", + "imageUrl": "img/phones/motorola-atrix-4g.0.jpg", + "name": "MOTOROLA ATRIX\u2122 4G", + "snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone." + }, + { + "age": 3, + "id": "dell-streak-7", + "imageUrl": "img/phones/dell-streak-7.0.jpg", + "name": "Dell Streak 7", + "snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around." + }, + { + "age": 4, + "carrier": "Cellular South", + "id": "samsung-gem", + "imageUrl": "img/phones/samsung-gem.0.jpg", + "name": "Samsung Gem\u2122", + "snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price." + }, + { + "age": 5, + "carrier": "Dell", + "id": "dell-venue", + "imageUrl": "img/phones/dell-venue.0.jpg", + "name": "Dell Venue", + "snippet": "The Dell Venue; Your Personal Express Lane to Everything" + }, + { + "age": 6, + "carrier": "Best Buy", + "id": "nexus-s", + "imageUrl": "img/phones/nexus-s.0.jpg", + "name": "Nexus S", + "snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet." + }, + { + "age": 7, + "carrier": "Cellular South", + "id": "lg-axis", + "imageUrl": "img/phones/lg-axis.0.jpg", + "name": "LG Axis", + "snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens" + }, + { + "age": 8, + "id": "samsung-galaxy-tab", + "imageUrl": "img/phones/samsung-galaxy-tab.0.jpg", + "name": "Samsung Galaxy Tab\u2122", + "snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility." + }, + { + "age": 9, + "carrier": "Cellular South", + "id": "samsung-showcase-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg", + "name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors" + }, + { + "age": 10, + "carrier": "Verizon", + "id": "droid-2-global-by-motorola", + "imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg", + "name": "DROID\u2122 2 Global by Motorola", + "snippet": "The first smartphone with a 1.2 GHz processor and global capabilities." + }, + { + "age": 11, + "carrier": "Verizon", + "id": "droid-pro-by-motorola", + "imageUrl": "img/phones/droid-pro-by-motorola.0.jpg", + "name": "DROID\u2122 Pro by Motorola", + "snippet": "The next generation of DOES." + }, + { + "age": 12, + "carrier": "AT&T", + "id": "motorola-bravo-with-motoblur", + "imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg", + "name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122", + "snippet": "An experience to cheer about." + }, + { + "age": 13, + "carrier": "T-Mobile", + "id": "motorola-defy-with-motoblur", + "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg", + "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122", + "snippet": "Are you ready for everything life throws your way?" + }, + { + "age": 14, + "carrier": "T-Mobile", + "id": "t-mobile-mytouch-4g", + "imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg", + "name": "T-Mobile myTouch 4G", + "snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi." + }, + { + "age": 15, + "carrier": "US Cellular", + "id": "samsung-mesmerize-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg", + "name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors" + }, + { + "age": 16, + "carrier": "Sprint", + "id": "sanyo-zio", + "imageUrl": "img/phones/sanyo-zio.0.jpg", + "name": "SANYO ZIO", + "snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value." + }, + { + "age": 17, + "id": "samsung-transform", + "imageUrl": "img/phones/samsung-transform.0.jpg", + "name": "Samsung Transform\u2122", + "snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d." + }, + { + "age": 18, + "id": "t-mobile-g2", + "imageUrl": "img/phones/t-mobile-g2.0.jpg", + "name": "T-Mobile G2", + "snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible." + }, + { + "age": 19, + "id": "motorola-charm-with-motoblur", + "imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg", + "name": "Motorola CHARM\u2122 with MOTOBLUR\u2122", + "snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service." + } +] diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/solution.md b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/solution.md new file mode 100644 index 00000000..4bb95319 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/solution.md @@ -0,0 +1,54 @@ +Код для загрузки и вывода телефонов: + +```js +function loadPhones() { + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', 'phones.json', true); + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + button.parentNode.removeChild(button); + + if (xhr.status != 200) { + // обработать ошибку + alert(xhr.status + ': ' + xhr.statusText); + } else { +*!* + try { + var phones = JSON.parse(xhr.responseText); + } catch(e) { + alert("Некорректный ответ " + e.message); + } + showPhones(phones); +*/!* + } + + } + + xhr.send(""); + + button.innerHTML = 'Загружаю...'; + button.disabled = true; +} + +*!* +function showPhones(phones) { + + phones.forEach(function(phone) { + var li = list.appendChild(document.createElement('li')); + li.innerHTML = phone.name; + }); + +} +*/!* +``` + +Полное решение с возможнотью скачать: +[iframe src="phones-list" height="100" border="1" zip] + +Обратите внимание -- код обрабатывает возможную ошибку при чтении JSON при помощи `try..catch`. + +Технически, это такая же ошибка, как и `status != 200`, так как сервер обязан присылать корректный JSON, поэтому если уж обрабатываем ошибки запроса, то и её тоже. diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/task.md b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/task.md new file mode 100644 index 00000000..5a0422fa --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/1-phones-list/task.md @@ -0,0 +1,13 @@ +# Выведите телефоны + +[importance 5] + +Создайте код, который загрузит файл `phones.json` из текущей директории и выведет все названия телефонов из него в виде списка. + +Демо результата: +[iframe src="phones-list" height="100" border="1"] + + +Исходный код просто выводит содержимое файла (скачайте к себе): +[iframe src="phones-async" border="1" zip] + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/2-request-with-auth/solution.md b/3-more/10-ajax/3-ajax-xmlhttprequest/2-request-with-auth/solution.md new file mode 100644 index 00000000..23ca3e88 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/2-request-with-auth/solution.md @@ -0,0 +1 @@ +Решение: get.zip \ No newline at end of file diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/2-request-with-auth/task.md b/3-more/10-ajax/3-ajax-xmlhttprequest/2-request-with-auth/task.md new file mode 100644 index 00000000..25395567 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/2-request-with-auth/task.md @@ -0,0 +1,15 @@ +# Запрос с авторизацией + +[importance 5] + +Создайте функцию `get(url, password)`, которая отправляет GET-запрос на `url` и передаёт `password` в качестве значения HTTP-заголовка `Authorization`. + +Она должна выводить `responseText`. Если же сервер вернул ошибку -- то выводить "Ошибка" и добавлять информацию из `status` и `statusText`. + +Примеры ошибок: +
                      +
                    • 404 -- неправильный `url`.
                    • +
                    • 403 -- отсутствует или неверен заголовок `Authorization`.
                    • +
                    + +Заготовку вы можете скачать здесь: get-src.zip. Серверный код написан так, что возвращает успешный результат при запросе на адрес `'info'` с паролем `'password'`. diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/article.md b/3-more/10-ajax/3-ajax-xmlhttprequest/article.md new file mode 100644 index 00000000..2dcf7f20 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/article.md @@ -0,0 +1,426 @@ +# Основы XMLHttpRequest + +Объект `XMLHttpRequest` (или, как его кратко называют, "XHR") дает возможность из JavaScript делать HTTP-запросы к серверу без перезагрузки страницы. + +Несмотря на слово "XML" в названии, `XMLHttpRequest` может работать с любыми данными, а не только с XML. + +Использовать его очень просто. + +[cut] + +## Пример использования + +Как правило, `XMLHttpRequest` используют для загрузки данных. + +Разберём его на примере ниже, в котором JavaScript осуществит загрузку файла `phones.json` и вывод содержимого: + +[iframe src="phones" border="1" zip] + +Код, который выполняется при нажатии на кнопку, по шагам: + +```js +*!* +// 1. Создаём новый объект XMLHttpRequest +*/!* +var xhr = new XMLHttpRequest(); + +*!* +// 2. Конфигурируем его: GET-запрос на URL 'phones.json' +xhr.open('GET', 'phones.json', false); +*/!* + +*!* +// 3. Отсылаем запрос +*/!* +xhr.send(); + +*!* +// 4. Если код ответа сервера не 200, то это ошибка +*/!* +if (xhr.status != 200) { + // обработать ошибку + alert(xhr.status + ': ' + xhr.statusText); // пример вывода: 404: Not Found +} else { + // вывести результат + alert(xhr.responseText); // responseText -- текст ответа. +} +``` + +Далее мы более подробно разберём основные методы и свойства `xhr`. + +## Методы open, send и abort + +Эти три метода управляют основным потоком запроса: + +
                    +
                    `open(method, URL, async, user, password)`
                    +
                    +Задаёт основные параметры запроса: +
                      +
                    • `method` -- HTTP-метод. Как правило, используется GET либо POST, хотя доступны и более экзотические, вроде TRACE/DELETE/PUT и т.п.
                    • +
                    • `URL` -- адрес запроса. Можно использовать не только http/https, но и другие протоколы, например `ftp://` и `file://`. + +**При этом есть ограничения безопасности, называемые "Same Origin Policy": запрос со страницы можно отправлять только на тот же `протокол://домен:порт`, с которого она пришла. В следующих главах мы рассмотрим, как их можно обойти.** +
                    • +
                    • `async` -- если установлено в `false`, то запрос производится синхронно, если `true` -- асинхронно. + +"Синхронный запрос" означает, что после вызова `xhr.send()` и до ответа сервера главный поток будет "заморожен": посетитель не сможет взаимодействовать со страницей -- прокручивать, нажимать на кнопки и т.п. После получения ответа выполнение продолжится со следующей строки. + +"Асинхронный запрос" означает, что браузер отправит запрос, а далее результат нужно будет получить через обработчики событий, которые мы рассмотрим далее. +
                    • +
                    • `user`, `password` -- логин и пароль для HTTP-авторизации, если нужны.
                    • +
                    + +
                    +
                    `send([body])`
                    +
                    Отправить запрос на сервер. + +В `body` находится *тело* запроса. Не у всякого запроса есть тело, например у GET-запросов тела нет. + +С другой стороны, в POST основные данные как раз передаются через `body`.
                    +
                    `abort()`
                    +
                    Прерывает выполнение запроса.
                    +
                    + +## Свойства status, statusText, responseText и responseXML + +Основные свойства, содержащие ответ сервера: + +
                    +
                    `status`
                    +
                    HTTP-код ответа: `200`, `404`, `403` и так далее. Может быть также равен `0`, если сервер не ответил или при запросе на другой домен.
                    +
                    `statusText`
                    +
                    Текстовое описание статуса от сервера: `OK` `Not Found`, `Forbidden` и так далее.
                    +
                    `responseText`
                    +
                    Ответ сервера в виде текста.
                    +
                    `responseXml`
                    +
                    Если сервер вернул XML, снабдив его правильным заголовком `Content-type: text/xml`, то браузер создаст из него XML-документ. По нему можно будет делать запросы `xhr.responseXml.querySelector("...")` и другие. + +Как правило, при общении с сервером используют не XML, а JSON. То есть, сервер возвращает JSON в виде текста, который браузер превращает в объект вызовом `JSON.parse(xhr.responseText)`. +
                    +
                    + + +## Асинхронный вызов + +Синхронные вызовы имеют ряд важных отличий от асинхронных: + +
                      +
                    • Блокируют взаимодействие со страницей до окончания загрузки. Посетитель не может даже прокручивать её.
                    • +
                    • Если синхронный вызов занял слишком много времени, то браузер предложит закрыть "зависшую" страницу.
                    • +
                    • Кроме того, забегая вперёд, заметим, что ряд продвинутых возможностей, таких как возможность делать запросы на другой домен и указывать таймаут, в синхронном режиме не работают.
                    • +
                    + +**Поэтому, как правило, используют асинхронные вызовы.** + +Для того, чтобы запрос стал асинхронным, укажем параметр `async` равным `true`. + +Демо: + +[iframe src="phones-async" border zip] + +JS-код изменился: + +```js +var xhr = new XMLHttpRequest(); + +xhr.open('GET', 'phones.json', *!*true*/!*); + +*!* +xhr.onreadystatechange = function() { // (3) + if (xhr.readyState != 4) return; +*/!* + + button.innerHTML = 'Готово!'; + + if (xhr.status != 200) { + alert(xhr.status + ': ' + xhr.statusText); + } else { + alert(xhr.responseText); + } + +} + +xhr.send(); // (1) + +button.innerHTML = 'Загружаю...'; // (2) +button.disabled = true; +``` + +Теперь мы отправляем запрос асинхронно: +
                      +
                    1. JavaScript продолжает выполнение после `xhr.send()`.
                    2. +
                    3. Для того, чтобы показать, что задача "в процессе" -- мы меняем текст кнопки `button`.
                    4. +
                    5. Скрипт завершается, страница полностью активна, а ответ, когда он появится, вызовет обработчик `onreadystatechange`.
                    6. +
                    + +**Событие `readystatechange` происходит несколько раз в процессе отсылки и получения ответа. При этом можно посмотреть "текущее состояние запроса" в свойстве `xhr.readyState`.** + +Все состояния, по [спецификации](http://www.w3.org/TR/XMLHttpRequest/#states): + +```js +const unsigned short UNSENT = 0; // начальное состояние +const unsigned short OPENED = 1; // вызван open +const unsigned short HEADERS_RECEIVED = 2; // получены заголовки +const unsigned short LOADING = 3; // загружается тело (получен очередной пакет данных) +const unsigned short DONE = 4; // запрос завершён +``` + +Запрос проходит их в порядке `0` -> `1` -> `2` -> `3` -> ... -> `3` -> `4`, состояние `3` повторяется при каждом получении очередного пакета данных по сети. + +В данном случае нас интересует состояние `4` (запрос завершён). + +Пример ниже демонстрирует переключение между состояниями. В нём сервер отвечает на запрос `digits`, пересылая по строке из 1000 цифр раз в секунду. + +[iframe src="readystate" height=200 border zip] + +[warn header="Точка разрыва не гарантирована"] +При `readyState=3` мы можем посмотреть текущие данные в `responseText` и, казалось бы, могли бы работать с этими данными как с "ответом на текущий момент". + +Однако, технически мы не управляем разрывами между сетевыми пакетами. Если протестировать пример выше в локальной сети, то в большинстве браузеров разрывы будут каждые 1000 символов, но в реальности событие может возникнуть на любом байте. + +Вплоть до того, что символ русского языка в кодировке UTF-16 кодируется двумя байтами -- и разрыв может возникнуть *между ними*. + +Получится, что при очередном `readyState` в конце `responseText` будет байт-полсимвола, то есть он не будет корректной строкой -- частью ответа! Если скрипт будет предполагать, что это -- полноценная часть ответа, то неизбежны глюки. +[/warn] + + +## HTTP-заголовки, методы *RequestHeader + +`XMLHttpRequest` умеет как указывать свои заголовки в запросе, так и читать присланные в ответ. + +Для работы с HTTP-заголовками есть 3 метода: + +
                    +
                    `setRequestHeader(name, value)`
                    +
                    Устанавливает заголовок `name` запроса со значением `value`. + +Например: + +```js +xhr.setRequestHeader('Content-Type', 'application/json'); +``` + +[warn header="Ограничения на заголовки"] +Нельзя установить заголовки, которые контролирует браузер, например `Referer` или `Host` и ряд других (полный список [тут](http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method)). + +Это ограничение существует в целях безопасности и для контроля корректности запроса. +[/warn] + +[warn header="Поставленный заголовок нельзя снять"] +Особенностью `XMLHttpRequest` является то, что отменить `setRequestHeader` невозможно. + +Повторные вызовы добавляют информацию к заголовку: + +```js +xhr.setRequestHeader('X-Auth', '123'); +xhr.setRequestHeader('X-Auth', '456'); + +// в результате будет заголовок: +// X-Auth: 123, 456 +``` + +[/warn] + +
                    +
                    `getResponseHeader(name)`
                    +
                    Возвращает значение заголовка ответа `name`, кроме `Set-Cookie` и `Set-Cookie2`. + +Например: + +```js +xhr.getResponseHeader('Content-Type') +``` + +
                    +
                    `getAllResponseHeaders()`
                    +
                    Возвращает все заголовки ответа, кроме `Set-Cookie` и `Set-Cookie2`. + +Заголовки возвращаются в виде единой строки, например: + +``` +Cache-Control: max-age=31536000 +Content-Length: 4260 +Content-Type: image/png +Date: Sat, 08 Sep 2012 16:53:16 GMT +``` + +Между заголовками стоит перевод строки в два символа `"\r\n"` (не зависит от ОС), значение заголовка отделено двоеточием с пробелом `": "`. Этот формат задан стандартом. + +Таким образом, если хочется получить объект с парами заголовок-значение, то эту строку необходимо разбить и обработать. +
                    +
                    + + +## Таймаут: свойство и событие timeout + +Максимальную продолжительность запроса можно задать свойством `timeout`: + +```js +xhr.timeout = 30000; // 30 секунд (в миллисекундах) +``` + +При превышении этого времени запрос будет оборван и сгенерировано событие `ontimeout`: + +```js +xhr.ontimeout = function() { + alert('Извините, запрос превысил максимальное время'); +} +``` + +## Полный список событий загрузки + +Современная [спецификация](http://www.w3.org/TR/XMLHttpRequest/#events) предусматривает следующие события: + +
                      +
                    • `loadstart` -- запрос начат.
                    • +
                    • `progress` -- браузер получил очередной пакет данных, можно прочитать текущие полученные данные в `responseText`.
                    • +
                    • `abort` -- запрос был отменён вызовом `xhr.abort()`.
                    • +
                    • `error` -- произошла ошибка.
                    • +
                    • `load` -- запрос был успешно (без ошибок) завершён.
                    • +
                    • `timeout` -- запрос был прекращён по таймауту.
                    • +
                    • `loadend` -- запрос был завершён (успешно или неуспешно)
                    • + +Используя эти события можно более удобно отслеживать загрузку (`onload`) и ошибку (`onerror`), а также количество загруженных данных (`onprogress`). + +Также поддерживается событие `readystatechange`, которое было описано ранее. Оно появилось гораздо раньше, ещё до появления текущего стандарта. От него можно отказаться в пользу современных событий, если учесть особенности IE8-9 (см. далее). + +## Особенности IE8,9: XDomainRequest + +В IE8 и IE9 поддержка `XMLHttpRequest` ограничена: + +
                        +
                      • Не поддерживаются события, кроме `onreadystatechange`.
                      • +
                      • Некорректно поддерживается состояние `readyState = 3`: браузер может сгенерировать его только один раз во время запроса, а не при каждом пакете данных. Кроме того, он не даёт доступ к ответу `responseText` до того, как он будет до конца получен.
                      • +
                      + +Дело в том, что, когда создавались эти браузеры, спецификации были не до конца проработаны. Поэтому разработчики браузера решили добавить свой объект [XDomainRequest](/xhr-crossdomain) (только IE!), который реализовывал часть возможностей современного стандарта. + +А обычный `XMLHttpRequest` решили не трогать, чтобы ненароком не сломать существующий код. + +**Для того, чтобы получить некоторые из современных возможностей, вместо `new XMLHttpRequest()` пишем `new XDomainRequest`.** + +Если кросс-браузерно: + +```js +var XHR = window.XDomainRequest || XMLHttpRequest; +var xhr = new XHR(); +``` + +Теперь в IE8,9 поддерживаются события `onload`, `onerror` и `onprogress`. + +**Ещё одна деталь: IE9- по умолчанию кеширует ответы, не снабжённые антикеш-заголовком. Другие браузеры этого не делают.** + +**Чтобы этого избежать, сервер должен добавить в ответ соответствующие антикеш-заголовки, например `Cache-Control: no-cache`.** + +По правде говоря, использовать заголовки типа `Expires`, `Last-Modified` и `Cache-Control` рекомендуется в любом случае, чтобы дать понять браузеру (не обязательно IE), что ему следует делать. + +**Альтернативный вариант -- добавить в URL запроса случайный параметр, предотвращающий кеширование.** + +Например, вместо `xhr.open('GET', 'service', false)` написать: + +```js +xhr.open('GET', '*!*'service?r=' + Math.random()*/!*, false); +``` + +По историческим причинам такой способ предотвращения кеширования можно увидеть много где, так как старые браузеры плохо обрабатывали кеширующие заголовки. Сейчас серверные заголовки поддерживаются хорошо. + +## Итого + +Типовой код для GET-запроса при помощи `XMLHttpRequest`: + +```js +var xhr = new XMLHttpRequest(); + +xhr.open('GET', '/my/url', true); + +xhr.onreadystatechange = function() { + if (this.readyState != 4) return; + + // по окончании запроса доступны: + // status, statusText + // responseText, responseXML (при content-type: text/xml) + + if (this.status != 200) { + // обработать ошибку + alert('ошибка: ' + (this.status ? this.statusText || 'запрос не удался') ); + return; + } + + // получить результат из this.responseText или this.responseXML +} + +xhr.send(); +``` + +Методы: + +
                        +
                      • `open(method, url, async, user, password)`
                      • +
                      • `send(body)`
                      • +
                      • `abort()`
                      • +
                      • `setRequestHeader(name, value)`
                      • +
                      • `getResponseHeader(name)`
                      • +
                      • `getAllResponseHeaders()`
                      • +
                      + +Свойства: + +
                        +
                      • `timeout`
                      • +
                      • `responseText`
                      • +
                      • `responseXML`
                      • +
                      • `status`
                      • +
                      • `statusText`
                      • +
                      + +События: + +
                        +
                      • `onreadystatechange`
                      • +
                      • `ontimeout`
                      • +
                      • `onerror`
                      • +
                      • `onload`
                      • +
                      • `onprogress`
                      • +
                      • `onabort`
                      • +
                      • `onloadstart`
                      • +
                      • `onloadend`
                      • +
                      + + +[head] + + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/.zip b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/index.html b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/index.html new file mode 100755 index 00000000..605c5092 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/index.js b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/index.js new file mode 100755 index 00000000..818ce465 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/index.js @@ -0,0 +1,29 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); +var file = new static.Server('.', { cache: 0 }); + + +function accept(req, res) { + + if (req.url == '/phones.json') { + // искусственная задержка для наглядности + setTimeout(function() { + file.serve(req, res); + }, 2000); + } else { + file.serve(req, res); + } + +} + + +// ------ запустить сервер ------- + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/phones.json b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/phones.json new file mode 100755 index 00000000..472b2b16 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones-async/phones.json @@ -0,0 +1,155 @@ +[ + { + "age": 0, + "id": "motorola-xoom-with-wi-fi", + "imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg", + "name": "Motorola XOOM\u2122 with Wi-Fi", + "snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 1, + "id": "motorola-xoom", + "imageUrl": "img/phones/motorola-xoom.0.jpg", + "name": "MOTOROLA XOOM\u2122", + "snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 2, + "carrier": "AT&T", + "id": "motorola-atrix-4g", + "imageUrl": "img/phones/motorola-atrix-4g.0.jpg", + "name": "MOTOROLA ATRIX\u2122 4G", + "snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone." + }, + { + "age": 3, + "id": "dell-streak-7", + "imageUrl": "img/phones/dell-streak-7.0.jpg", + "name": "Dell Streak 7", + "snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around." + }, + { + "age": 4, + "carrier": "Cellular South", + "id": "samsung-gem", + "imageUrl": "img/phones/samsung-gem.0.jpg", + "name": "Samsung Gem\u2122", + "snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price." + }, + { + "age": 5, + "carrier": "Dell", + "id": "dell-venue", + "imageUrl": "img/phones/dell-venue.0.jpg", + "name": "Dell Venue", + "snippet": "The Dell Venue; Your Personal Express Lane to Everything" + }, + { + "age": 6, + "carrier": "Best Buy", + "id": "nexus-s", + "imageUrl": "img/phones/nexus-s.0.jpg", + "name": "Nexus S", + "snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet." + }, + { + "age": 7, + "carrier": "Cellular South", + "id": "lg-axis", + "imageUrl": "img/phones/lg-axis.0.jpg", + "name": "LG Axis", + "snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens" + }, + { + "age": 8, + "id": "samsung-galaxy-tab", + "imageUrl": "img/phones/samsung-galaxy-tab.0.jpg", + "name": "Samsung Galaxy Tab\u2122", + "snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility." + }, + { + "age": 9, + "carrier": "Cellular South", + "id": "samsung-showcase-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg", + "name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors" + }, + { + "age": 10, + "carrier": "Verizon", + "id": "droid-2-global-by-motorola", + "imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg", + "name": "DROID\u2122 2 Global by Motorola", + "snippet": "The first smartphone with a 1.2 GHz processor and global capabilities." + }, + { + "age": 11, + "carrier": "Verizon", + "id": "droid-pro-by-motorola", + "imageUrl": "img/phones/droid-pro-by-motorola.0.jpg", + "name": "DROID\u2122 Pro by Motorola", + "snippet": "The next generation of DOES." + }, + { + "age": 12, + "carrier": "AT&T", + "id": "motorola-bravo-with-motoblur", + "imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg", + "name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122", + "snippet": "An experience to cheer about." + }, + { + "age": 13, + "carrier": "T-Mobile", + "id": "motorola-defy-with-motoblur", + "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg", + "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122", + "snippet": "Are you ready for everything life throws your way?" + }, + { + "age": 14, + "carrier": "T-Mobile", + "id": "t-mobile-mytouch-4g", + "imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg", + "name": "T-Mobile myTouch 4G", + "snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi." + }, + { + "age": 15, + "carrier": "US Cellular", + "id": "samsung-mesmerize-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg", + "name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors" + }, + { + "age": 16, + "carrier": "Sprint", + "id": "sanyo-zio", + "imageUrl": "img/phones/sanyo-zio.0.jpg", + "name": "SANYO ZIO", + "snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value." + }, + { + "age": 17, + "id": "samsung-transform", + "imageUrl": "img/phones/samsung-transform.0.jpg", + "name": "Samsung Transform\u2122", + "snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d." + }, + { + "age": 18, + "id": "t-mobile-g2", + "imageUrl": "img/phones/t-mobile-g2.0.jpg", + "name": "T-Mobile G2", + "snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible." + }, + { + "age": 19, + "id": "motorola-charm-with-motoblur", + "imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg", + "name": "Motorola CHARM\u2122 with MOTOBLUR\u2122", + "snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service." + } +] diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones/.zip b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones/index.html b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/index.html new file mode 100755 index 00000000..6d60bc59 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones/index.js b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/index.js new file mode 100755 index 00000000..818ce465 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/index.js @@ -0,0 +1,29 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); +var file = new static.Server('.', { cache: 0 }); + + +function accept(req, res) { + + if (req.url == '/phones.json') { + // искусственная задержка для наглядности + setTimeout(function() { + file.serve(req, res); + }, 2000); + } else { + file.serve(req, res); + } + +} + + +// ------ запустить сервер ------- + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/phones/phones.json b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/phones.json new file mode 100755 index 00000000..472b2b16 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/phones/phones.json @@ -0,0 +1,155 @@ +[ + { + "age": 0, + "id": "motorola-xoom-with-wi-fi", + "imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg", + "name": "Motorola XOOM\u2122 with Wi-Fi", + "snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 1, + "id": "motorola-xoom", + "imageUrl": "img/phones/motorola-xoom.0.jpg", + "name": "MOTOROLA XOOM\u2122", + "snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)." + }, + { + "age": 2, + "carrier": "AT&T", + "id": "motorola-atrix-4g", + "imageUrl": "img/phones/motorola-atrix-4g.0.jpg", + "name": "MOTOROLA ATRIX\u2122 4G", + "snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone." + }, + { + "age": 3, + "id": "dell-streak-7", + "imageUrl": "img/phones/dell-streak-7.0.jpg", + "name": "Dell Streak 7", + "snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around." + }, + { + "age": 4, + "carrier": "Cellular South", + "id": "samsung-gem", + "imageUrl": "img/phones/samsung-gem.0.jpg", + "name": "Samsung Gem\u2122", + "snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price." + }, + { + "age": 5, + "carrier": "Dell", + "id": "dell-venue", + "imageUrl": "img/phones/dell-venue.0.jpg", + "name": "Dell Venue", + "snippet": "The Dell Venue; Your Personal Express Lane to Everything" + }, + { + "age": 6, + "carrier": "Best Buy", + "id": "nexus-s", + "imageUrl": "img/phones/nexus-s.0.jpg", + "name": "Nexus S", + "snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet." + }, + { + "age": 7, + "carrier": "Cellular South", + "id": "lg-axis", + "imageUrl": "img/phones/lg-axis.0.jpg", + "name": "LG Axis", + "snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens" + }, + { + "age": 8, + "id": "samsung-galaxy-tab", + "imageUrl": "img/phones/samsung-galaxy-tab.0.jpg", + "name": "Samsung Galaxy Tab\u2122", + "snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility." + }, + { + "age": 9, + "carrier": "Cellular South", + "id": "samsung-showcase-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg", + "name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors" + }, + { + "age": 10, + "carrier": "Verizon", + "id": "droid-2-global-by-motorola", + "imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg", + "name": "DROID\u2122 2 Global by Motorola", + "snippet": "The first smartphone with a 1.2 GHz processor and global capabilities." + }, + { + "age": 11, + "carrier": "Verizon", + "id": "droid-pro-by-motorola", + "imageUrl": "img/phones/droid-pro-by-motorola.0.jpg", + "name": "DROID\u2122 Pro by Motorola", + "snippet": "The next generation of DOES." + }, + { + "age": 12, + "carrier": "AT&T", + "id": "motorola-bravo-with-motoblur", + "imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg", + "name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122", + "snippet": "An experience to cheer about." + }, + { + "age": 13, + "carrier": "T-Mobile", + "id": "motorola-defy-with-motoblur", + "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg", + "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122", + "snippet": "Are you ready for everything life throws your way?" + }, + { + "age": 14, + "carrier": "T-Mobile", + "id": "t-mobile-mytouch-4g", + "imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg", + "name": "T-Mobile myTouch 4G", + "snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi." + }, + { + "age": 15, + "carrier": "US Cellular", + "id": "samsung-mesmerize-a-galaxy-s-phone", + "imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg", + "name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone", + "snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors" + }, + { + "age": 16, + "carrier": "Sprint", + "id": "sanyo-zio", + "imageUrl": "img/phones/sanyo-zio.0.jpg", + "name": "SANYO ZIO", + "snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value." + }, + { + "age": 17, + "id": "samsung-transform", + "imageUrl": "img/phones/samsung-transform.0.jpg", + "name": "Samsung Transform\u2122", + "snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d." + }, + { + "age": 18, + "id": "t-mobile-g2", + "imageUrl": "img/phones/t-mobile-g2.0.jpg", + "name": "T-Mobile G2", + "snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible." + }, + { + "age": 19, + "id": "motorola-charm-with-motoblur", + "imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg", + "name": "Motorola CHARM\u2122 with MOTOBLUR\u2122", + "snippet": "Motorola CHARM fits easily in your pocket or palm. Includes MOTOBLUR service." + } +] diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/.zip b/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/index.html b/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/index.html new file mode 100755 index 00000000..f8e6ebc1 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/index.html @@ -0,0 +1,33 @@ + + + + + + + +
                        + + + + + diff --git a/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/index.js b/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/index.js new file mode 100755 index 00000000..f23d5556 --- /dev/null +++ b/3-more/10-ajax/3-ajax-xmlhttprequest/readystate/index.js @@ -0,0 +1,42 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); +var file = new static.Server('.'); + +function accept(req, res) { + + if (req.url == '/digits') { + + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache' + }); + + var i=0; + + var timer = setInterval(write, 1000); + write(); + + function write() { + res.write(new Array(1000).join(++i+'') + ' '); + if (i == 9) { + clearInterval(timer); + res.end(); + } + + } + } else { + file.serve(req, res); + } +} + + + +// ----- запуск accept как сервера из консоли или как модуля ------ + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} diff --git a/3-more/10-ajax/4-xhr-forms/article.md b/3-more/10-ajax/4-xhr-forms/article.md new file mode 100644 index 00000000..cc041afc --- /dev/null +++ b/3-more/10-ajax/4-xhr-forms/article.md @@ -0,0 +1,275 @@ +# XMLHttpRequest и POST, отсылка форм + +Во время обычной отправки формы браузер сам кодирует значения полей и составляет тело GET/POST-запроса для посылки на сервер. + +При отправке данных через `XMLHttpRequest`, это нужно делать самим, в javascript-коде. Большинство проблем и вопросов здесь связано с непониманием, где и какое кодирование нужно осуществлять. + +[cut] + +## Кодировка urlencoded + +Основной способ кодировки запросов -- это [:urlencoded], то есть -- стандартное кодирование URL. При этом пробел представляется как `%20`, русские буквы и большинство спецсимволов кодируются, английские буквы и дефис оставляются как есть. + +Способ, которым следует кодировать данные формы при отправке, задаётся в её HTML-теге. + +Например: + +```html +
                        метод GET с кодировкой по умолчанию + метод POST с кодировкой по умолчанию + +``` + +Во всех этих случаях будет использована кодировка `urlencoded`. + +Если форма отправляется обычным образом, то браузер сам кодирует (urlencode) название и значение каждого поля данных, и отсылает форму на сервер в закодированном виде. + +Например, форма: + +```html + + + +
                        +``` + +Будет отправлена как: `/submit?name=%D0%92%D0%B8%D0%BA%D1%82%D0%BE%D1%80&surname=%D0%A6%D0%BE%D0%B9`. + +### GET-запрос + +Формируя XMLHttpRequest, мы должны формировать запрос "руками", кодируя поля функцией [:encodeURIComponent]. + +Например, для посылки GET-запроса с произвольными параметрами `name` и `surname`, их необходимо закодировать вот так: + +```js +// Передаём name и surname в параметрах запроса + +var xhr = new XMLHttpRequest(); + +var params = 'name=' + encodeURIComponent(name) + + '&surname=' + encodeURIComponent(surname); + +xhr.open("GET", '/submit?'+params, true); + +xhr.onreadystatechange = ...; + +xhr.send(''); +``` + +[smart header="Прочие заголовки"] +Заголовки `Content-Length`, `Connection` не нужны. + +Более того, по спецификации браузер запрещает их явную установку, как и некоторых других низкоуровневых HTTP-заголовков, которые могли бы ввести в заблуждение сервер относительно того, кто и сколько данных ему прислал, например `Referer`. Это сделано в целях контроля правильности запроса и для безопасности. +[/smart] + +[smart header="AJAX или не AJAX?"] +**Запрос, отправленный кодом выше через `XMLHttpRequest`, никак не отличается от обычной отправки формы.** + +Поэтому в некоторых фреймворках, чтобы сказать серверу, что это AJAX, добавляют специальный заголовок. + +Например: + +```js +xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); +``` + +Сервер может, увидев заголовок, отправить данные в JSON-формате, а без него -- генерировать полноценную страницу. Получится, что один URL работает и как страница и как AJAX-сервис. +[/smart] + +### POST-запрос + +В методе POST параметры передаются не в URL, а в теле, посылаемом через `send(body)`. Поэтому `params` нужно указывать не в `open`, а в `send`. + +Кроме того, при POST обязателен заголовок `Content-Type`, содержащий кодировку. Это указание для сервера -- как обрабатывать (раскодировать) пришедший запрос. + +```js +// Пример с POST +var xhr = new XMLHttpRequest(); + +var params = 'name=' + encodeURIComponent(name) + + '&surname=' + encodeURIComponent(surname); + +xhr.open("POST", '/submit', true) +xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') + +xhr.onreadystatechange = ...; + +xhr.send(params); +``` + +[warn header="Только UTF-8"] +Всегда используется только UTF-8, независимо от языка страницы. + +Если сервер вдруг ожидает данные в windows-1251 (к примеру), то их нужно будет перекодировать. +[/warn] + +## Кодировка multipart/form-data + +Второй способ кодирования называется "[multipart/form-data](http://ru.wikipedia.org/wiki/Multipart_form-data)". При этом поля пересылаются одно за другим, через строку-разделитель. + +Этот способ используется в методе `POST` и указывается атрибутом `enctype="multipart/form-data"`. + +Пример формы + +```html +
                        + + +
                        +``` + +Форма при такой кодировке пересылается в теле запроса, поля разделены случайно сгенерированной строкой `boundary`, вот так: + +``` +...Заголовки... +Content-Type: *!*multipart/form-data; boundary=RaNdOmDeLiMiTeR*/!* + +--RaNdOmDeLiMiTeR +Content-Disposition: form-data; name="*!*name*/!*" + +*!*Виктор*/!* +--RaNdOmDeLiMiTeR +Content-Disposition: form-data; name="*!*surname*/!*" + +*!*Цой*/!* +--RaNdOmDeLiMiTeR-- +``` + +Сервер видит заголовок `Content-Type: multipart/form-data` и раскодирует поля формы. + +**Как видно, само содержимое полей при этом оставляется "как есть". Поэтому такой способ используется в первую очередь при пересылке файла.** + + +### POST-запрос + +В `XMLHttpRequest` можно указать кодировку `multipart/form-data` и вручную сформировать тело запроса, удовлетворяющее требованиям кодировки. + +Пример кода для формирования запроса в кодировке `multipart/form-data`: + +```js +var data = { + name: 'Виктор', + surname: 'Цой' +}; + +var boundary = String(Math.random()).slice(2); +var boundaryMiddle = '--' + boundary + '\r\n'; +var boundaryLast = '--' + boundary + '--\r\n' + +var body = ['\r\n']; +for (var key in data) { + // добавление поля + body.push('Content-Disposition: form-data; name="'+key+'"\r\n\r\n'+data[key]+'\r\n'); +} + +body = body.join(boundaryMiddle) + boundaryLast; + +// Тело запроса готово, отправляем + +var xhr = new XMLHttpRequest(); +xhr.open('POST', '/submit', true); + +xhr.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary); + +xhr.onreadystatechange = function() { + if (this.readyState != 4) return; + + alert(this.responseText); +} + +xhr.send(body); +``` + +Тело запроса будет иметь вид, описанный выше, то есть поля через разделитель. + +[smart header="Отправка файла"] +**Можно создать запрос, который сервер воспримет как загрузку файла.** + +Для этого текст файла должен быть уже доступен JavaScript, т.е. произвольный файл прочитать и переслать, конечно же, нельзя. + +Для добавления файла нужно использовать тот же код, что выше, но при добавлении поля вместо строки `body.push('Content-Disposition: form-data; name=...')` указать расширенные заголовки: + +```js +Content-Disposition: form-data; name="myfile"; filename="pic.jpg" +Content-Type: image/jpeg +(пустая строка) +содержимое файла +``` + +Код будет выглядеть так: + +```js +body.push('Content-Disposition: form-data; name="'+key+'"; filename="pic.jpg"\r\nContent-Type: image/jpeg\r\n\r\n'+data[key]+'\r\n'); +``` + +Имя файла `pic.jpg` здесь задано явно, но вам не составит труда его заменить. +[/smart] + +## FormData + +Современные браузеры, исключая IE<10, поддерживают объект [FormData](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/FormData/Using_FormData_Objects), который позволяет загружать формы напрямую. + +**В форме могут быть любые поля, в том числе файловые.** + +Лучше всего это показывает пример: + +```html +
                        + +
                        + + +``` + +Этот код отправит на сервер форму с полями `firstname` и `lastname`. + +Интерфейс: +
                          +
                        • Конструктор `new FormData([form])` вызывается либо без аргументов, либо с DOM-элементом формы.
                        • +
                        • Метод `formData.append(name, value)` добавляет данные к форме.
                        • +
                        + +Интеграция `FormData` с `XMLHttpRequest` встроена в браузер. +[head] + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/5-xhr-longpoll/article.md b/3-more/10-ajax/5-xhr-longpoll/article.md new file mode 100644 index 00000000..f2b02d82 --- /dev/null +++ b/3-more/10-ajax/5-xhr-longpoll/article.md @@ -0,0 +1,104 @@ +# XMLHttpRequest: длинные опросы + +Способ организации COMET, который мы рассмотрим в этой главе, прост и подходит в 90% реальных случаев. + +[cut] + +## Частые опросы + +Первое решение, которое приходит в голову для получения событий с сервера -- это "частые опросы" (polling), т.е периодические запросы на сервер: "эй, я тут, изменилось ли что-нибудь?". Например, раз в 10 секунд. + +В ответ сервер во-первых помечает у себя, что клиент онлайн, а во-вторых посылает сообщение, в котором в специальном формате содержится весь пакет событий, накопившихся к данному моменту. + +**У частых опросов есть одна большая проблема, а именно -- большие задержки между созданием и получением данных.** + +Сервер отсылает их не тогда, когда они появились, а когда настанет время очередного запроса. + +[summary] +Задержка = время между опросами + установление соединения + передача данных. +[/summary] + +Другой минус -- лишний входящий трафик на сервер. При каждом запросе браузер передает множество заголовков и в ответ получает, кроме данных, также заголовки. Для некоторых приложений трафик заголовков может в 10 и более раз превосходить трафик реальных данных. + +[compare] +-Задержки между событием и уведомлением. +-Лишний трафик и запросы на сервер. ++Простота реализации. +[/compare] + +Причём, простота реализации тут достаточно условная. Клиентская часть -- довольно проста, а вот сервер получает сразу большой поток запросов. + +Даже если клиент ушёл пить чай -- его браузер каждые 10 секунд будет "долбить" сервер запросами. Готов ли сервер к такому? + +## Длинные опросы + +Длинные опросы -- отличная альтернатива частым опросам. Они также удобны в реализации, и при этом сообщения доставляются без задержек. + +Схема: +
                          +
                        1. Отправляется запрос на сервер.
                        2. +
                        3. Соединение не закрывается сервером, пока не появится сообщение.
                        4. +
                        5. Когда сообщение появилось -- сервер отвечает на запрос, пересылая данные.
                        6. +
                        7. Браузер тут же делает новый запрос.
                        8. +
                        + +Ситуация, когда браузер отправил запрос и держит соединение с сервером, ожидая ответа, является стандартной и прерывается только доставкой сообщений. + +Схема коммуникации: + + + +При этом если соединение рвётся само, например, закрыто прокси-сервером, то браузер тут же отсылает новый запрос. + +Примерный код клиентской части: + +```js +function subscribe(url) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState != 4) return; + + if (this.status == 200) { + onMessage(this.responseText); + } else { + onError(this); + } + + subscribe(url); + } + xhr.open("GET", url, true); + xhr.send(''); +} +``` + +Функция `subscribe` делает запрос, при ответе обрабатывает результат, и тут же запускает процесс по новой. + +[compare] +-Сообщение отправляются клиенту тут же. +-Сервер должен уметь держать большое количество ожидающих соединений. +-Простота реализации. +[/compare] + +## Демо: чат + +Для примера вы можете скачать простейший чат, где в качестве сервера -- Node.JS: [longpoll.zip](/zip/tutorial/ajax/xhr/longpoll.zip). + +Демо: + +[iframe src="longpoll" border="1" height="200px" link zip] + +## Область применения + +Длинные опросы отлично работают в тех случаях, когда сообщения приходят редко. + +При большом количестве частых сообщений график приёма-отправки, приведённый выше, превращается в "пилу". Каждое сообщение -- это новый запрос, дополнительный трафик. + +В этих случаях используются другие способы получения данных, подразумевающие непрерывное соединение с сервером. Мы рассмотрим их в следующих главах. + + + + + + + + diff --git a/3-more/10-ajax/5-xhr-longpoll/longpoll.png b/3-more/10-ajax/5-xhr-longpoll/longpoll.png new file mode 100755 index 00000000..fae89022 Binary files /dev/null and b/3-more/10-ajax/5-xhr-longpoll/longpoll.png differ diff --git a/3-more/10-ajax/5-xhr-longpoll/longpoll/.zip b/3-more/10-ajax/5-xhr-longpoll/longpoll/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/5-xhr-longpoll/longpoll/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/5-xhr-longpoll/longpoll/browser.js b/3-more/10-ajax/5-xhr-longpoll/longpoll/browser.js new file mode 100755 index 00000000..bac497c7 --- /dev/null +++ b/3-more/10-ajax/5-xhr-longpoll/longpoll/browser.js @@ -0,0 +1,52 @@ +// Посылка запросов -- обычными XHR POST +function PublishForm(form, url) { + + function sendMessage(message) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.send('message='+encodeURIComponent(message)); + } + + form.onsubmit = function() { + var message = form.message.value; + if (message) { + form.message.value = ''; + sendMessage(message); + } + return false; + }; +} + +// Получение сообщений, COMET +function SubscribePane(elem, url) { + + function showMessage(message) { + var messageElem = document.createElement('div'); + messageElem.appendChild(document.createTextNode(message)); + elem.appendChild(messageElem); + } + + function subscribe() { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState != 4) return; + + if (this.status == 200) { + showMessage(this.responseText); + subscribe(); + return; + } + + if (this.status != 404 ) { // 404 может означать, что сервер перезагружается + showMessage(this.statusText); // показать ошибку + } + + setTimeout(subscribe, 1000); // попробовать ещё раз через 1 сек + } + xhr.open("GET", url, true); + xhr.send(''); + } + + subscribe(); + +} diff --git a/3-more/10-ajax/5-xhr-longpoll/longpoll/index.html b/3-more/10-ajax/5-xhr-longpoll/longpoll/index.html new file mode 100755 index 00000000..a3d8f465 --- /dev/null +++ b/3-more/10-ajax/5-xhr-longpoll/longpoll/index.html @@ -0,0 +1,25 @@ + + + + + + + + +Несколько человек при заходе на эту страницу будут получать сообщения друг друга. + +
                        + + +
                        + +
                        +
                        + + + + + diff --git a/3-more/10-ajax/5-xhr-longpoll/longpoll/index.js b/3-more/10-ajax/5-xhr-longpoll/longpoll/index.js new file mode 100755 index 00000000..8c45a053 --- /dev/null +++ b/3-more/10-ajax/5-xhr-longpoll/longpoll/index.js @@ -0,0 +1,79 @@ +var http = require('http'); +var url = require('url'); +var querystring = require('querystring'); +var static = require('node-static'); + +var fileServer = new static.Server('.'); + +var subscribers = {}; + +function onSubscribe(req, res) { + var id = Math.random(); + + subscribers[id] = res; + console.log("новый клиент " + id + ", клиентов:" + Object.keys(subscribers).length); + + req.on('close', function() { + delete subscribers[id]; + console.log("клиент "+id+" отсоединился, клиентов:" + Object.keys(subscribers).length); + }); + +} + +function onPublish(req, res, message) { + + console.log("есть сообщение, клиентов:" + Object.keys(subscribers).length); + + for(var id in subscribers) { + console.log("отсылаю сообщение " + id); + var res = subscribers[id]; + + res.writeHead(200, { + 'Content-Type': 'text/plain;charset=utf-8', + "Cache-Control": "no-cache, must-revalidate" + }); + + res.write(message, 'utf-8'); + res.end(); + } + + subscribers = {}; +} + +function accept(req, res) { + var urlParsed = url.parse(req.url, true); + + // новый клиент хочет получать сообщения + if (urlParsed.pathname == '/subscribe') { + onSubscribe(req, res); // собственно, подписка + return; + } + + // отправка сообщения + if (urlParsed.pathname == '/publish' && req.method == 'POST') { + // принять POST-запрос + var post; + req.addListener('data', function (chunk) { + post = querystring.parse(chunk.toString()); + }).addListener('end', function () { + onPublish(req, res, post.message.toString()); // собственно, отправка + }); + + return; + } + + // всё остальное -- статика + fileServer.serve(req, res); + +} + + +// ----------------------------------- + +if (!module.parent) { + http.createServer(accept).listen(8080); + console.log('Сервер запущен на порту 8080'); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/6-xhr-crossdomain/1-do-we-need-origin/solution.md b/3-more/10-ajax/6-xhr-crossdomain/1-do-we-need-origin/solution.md new file mode 100644 index 00000000..42ead589 --- /dev/null +++ b/3-more/10-ajax/6-xhr-crossdomain/1-do-we-need-origin/solution.md @@ -0,0 +1,3 @@ +`Origin` нужен, потому что `Referer` передаётся не всегда. В частности, при запросе со страницы на HTTPS -- нет `Referer`. + +Что же касается "неправильного" `Referer` -- это из области фантастики. Когда-то, много лет назад, в браузерах были ошибки, которые позволяли подменить `Referer` из JavaScript, но они давно исправлены. diff --git a/3-more/10-ajax/6-xhr-crossdomain/1-do-we-need-origin/task.md b/3-more/10-ajax/6-xhr-crossdomain/1-do-we-need-origin/task.md new file mode 100644 index 00000000..fe1a1532 --- /dev/null +++ b/3-more/10-ajax/6-xhr-crossdomain/1-do-we-need-origin/task.md @@ -0,0 +1,28 @@ +# Зачем нужен Origin? + +[importance 3] + +Как вы, наверняка, знаете, существует HTTP-заголовок `Referer`, в котором обычно указан адрес страницы, с которой инициирован запрос. + +Например, при отправке `XMLHttpRequest` со страницы `http://javascript.ru/some/url` на `http://google.ru`, заголовки будут примерно такими: + +``` +Accept:*/* +Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 +Accept-Encoding:gzip,deflate,sdch +Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 +Connection:keep-alive +Host:google.ru +*!* +Origin:http://learn.javascript.ru +Referer:http://learn.javascript.ru/node/add/learning-task +*/!* +``` + +Как видно, здесь присутствуют и `Referer` и `Origin`. + +Итак, вопросы: +
                          +
                        1. Зачем нужен `Origin`, если `Referer` содержит даже более полную информацию?
                        2. +
                        3. Может ли быть такое, что заголовка `Referer` нет или он неправильный?
                        4. +
                        diff --git a/3-more/10-ajax/6-xhr-crossdomain/article.md b/3-more/10-ajax/6-xhr-crossdomain/article.md new file mode 100644 index 00000000..534847f5 --- /dev/null +++ b/3-more/10-ajax/6-xhr-crossdomain/article.md @@ -0,0 +1,342 @@ +# XMLHttpRequest: кросс-доменные запросы + +Обычно запрос `XMLHttpRequest` может делать запрос только в рамках текущего сайта. При попытке использовать другой домен/порт/протокол -- браузер выдаёт ошибку. + +Существует современный стандарт [XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest/), он ещё в состоянии черновика, но предусматривает кросс-доменные запросы и многое другое. +[cut] +Большинство возможностей этого стандарта уже поддерживаются всеми браузерами, но увы, не в IE<10. + +Впрочем, частично кросс-доменные запросы поддерживаются, начиная с IE8, только вместо `XMLHttpRequest` нужно использовать объект [XDomainRequest](http://msdn.microsoft.com/en-us/library/ie/cc288060.aspx). + +## Кросс-доменные запросы [#cors] + +Разберём кросс-доменные запросы на примере кода: + +```js +// (1) +var xhr = new XMLHttpRequest(); +if (!xhr.onload) { // это IE 8/9, в них старый XMLHttpRequest + if (!window.XDomainRequest) throw new Error("Not supported"); + xhr = new XDomainRequest(); // ..но есть XDomainRequest +} + +// (2) запрос на другой домен :) +xhr.open('GET', 'http://anywhere.com/vote.php', true); + +// (3) +xhr.onload = function() { + alert(this.responseText); +} + +xhr.onerror = function() { + alert('Ошибка ' + this.status); +} + +xhr.send(''); +``` + +Комментарии: +
                          +
                        1. Мы создаём `XMLHttpRequest` и проверяем, поддерживает ли он событие `onload`. Если нет, то это старый `XMLHttpRequest`. Возможно, у нас IE8-9, значит надо попробовать `XDomainRequest`.
                        2. +
                        3. Запрос на другой домен отсылается просто указанием соответствующего URL в `open`. Он должен быть асинхронным.
                        4. +
                        5. Все браузеры, которые поддерживают кросс-доменные запросы, поддерживают и события `onload/onerror`, которые можно использовать вместо `onreadystatechange`.
                        6. +
                        + + + +### Контроль безопасности + +Контроль безопасности осуществляется на уровне браузера. + +**В кросс-доменный запрос браузер автоматически добавляет заголовок `Origin`, содержащий домен, с которого осуществлён запрос.** + +В данном случае заголовки будут примерно такие: + +``` +GET /vote.php +Accept:*/* +Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 +Accept-Encoding:gzip,deflate,sdch +Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 +Connection:keep-alive +Host:anywhere.com +*!* +Origin:http://javascript.ru +*/!* +``` + +Сервер должен, со своей стороны, ответить специальными заголовками, разрешает ли он такой запрос к себе. + + +**Если сервер разрешает кросс-доменный запрос с этого домена -- он должен добавить к ответу заголовок `Access-Control-Allow-Origin`, содержащий домен запроса или звёздочку `*`.** + +Только при наличии такого заголовка в ответе -- браузер сочтёт запрос успешным, а иначе JavaScript получит ошибку. + +То есть, ответ сервера может быть примерно таким: + +``` +HTTP/1.1 200 OK +Content-Type:text/html; charset=UTF-8 +*!* +Access-Control-Allow-Origin: http://javascript.ru +*/!* +``` + +**Описанный выше поток выполнения обладает двумя особенностями:** + +
                          +
                        1. Не передаются куки и заголовки HTTP-авторизации. Параметры `user` и `password` в методе `open` игнорируются.
                        2. +
                        3. Речь пока только о GET и POST-запросах, другие методы обрабатываются по-другому.
                        4. +
                        + +[warn header="Что может сделать хакер, используя такие запросы?"] +Описанные выше ограничения приводят к тому, что запрос полностью безопасен. + +Действительно, нехороший человек со злой страницы может сформировать любой GET/POST-запрос и отправить его, но без разрешения сервера ответа он не получит. + +А без ответа такой запрос, по сути, эквивалентен отправке формы GET/POST, причём без авторизации. +[/warn] + + + + + + +### Ограничения IE<10 + +В IE<10 используется `XDomainRequest`, который представляет собой урезанный `XMLHttpRequest`. + +На него действуют ограничения: + +
                          +
                        • Протокол нужно сохранять: запросы допустимы с HTTP на HTTP, с HTTPS на HTTPS. Другие протоколы запрещены.
                        • +
                        • Метод `open(method, url)` имеет только два параметра. Он всегда асинхронный.
                        • +
                        • Ряд возможностей современного стандарта недоступны, в частности: +
                          • Недоступны методы, кроме GET или POST.
                          • +
                          • Нельзя добавлять свои заголовки, даже нельзя указать свой `Content-Type` для запроса, он всегда `text/plain`.
                          • +
                          • Нельзя включить передачу кук и данных HTTP-авторизации.
                          • +
                          +
                        • +
                        • В IE8 в режиме просмотра InPrivate кросс-доменные запросы не работают.
                        • +
                        + +Современный стандарт [XMLHttpRequest](http://www.w3.org/TR/XMLHttpRequest/) предусматривает средства для преодоления этих ограничений, но на момент выхода IE8 они ещё не были проработаны, поэтому их не реализовали. А IE9 исправил некоторые ошибки, но в общем не добавил ничего нового. + +Так как большинство сайтов хотят поддерживать IE<10, то на практике кросс-доменные запросы редко используют, предпочитая другие способы кросс-доменной коммуникации. Например, динамически создаваемый тег `SCRIPT` или вспомогательный `IFRAME` с другого домена. Мы разберём эти подходы в последующих главах. + +[smart header="Как разрешить кросс-доменные запросы от доверенного сайта в IE<10?"] + +Разрешить кросс-доменные запросы можно в настройках IE, во вкладке "Безопасность", включив пункт "Доступ к источникам данных за пределами домена". + +Обычно это делается не для всех сайтов, а для зоны "Надёжные узлы", после чего в неё вносится доверенный сайт. Теперь он может делать кросс-доменные запросы `XMLHttpRequest`. + +Этот способ можно применить для корпоративных сайтов, а также в тех случаях, когда посетитель заведомо вам доверяет, но почему-то (компьютер на работе, админ запрещает ставить другой браузер?) хочет использовать именно IE. Например, он может предлагаться в качестве дополнительной инструкции "как заставить этот сервис работать под IE". +[/smart] + +[smart header="В IE разрешён другой порт"] +В кросс-доменные ограничения IE не включён порт. + +То есть, можно сделать запрос с `http://javascript.ru` на `http://javascript.ru:8080`, и в IE он не будет считаться кросс-доменным. + +Это позволяет решить некоторые задачи, связанные с взаимодействием различных сервисов в рамках одного сайта. + +[/smart] + +Есть и другие способы обойти кросс-доменные ограничения, например использование тега `SCRIPT` или `IFRAME`. Их детали мы рассмотрим в следующих главах, так как здесь мы рассматриваем именно `XMLHttpRequest`. + + +**Расширенные возможности, описанные далее, поддерживаются всеми современными браузерами, кроме IE<10.** + +## Запросы от имени пользователя + +Чтобы браузер передал вместе с запросом куки и, при необходимости, HTTP-авторизацию, поставьте запросу `xhr.withCredentials = true`: + +```js +var xhr = new XMLHttpRequest(); // XDomainRequest не поддерживает это +xhr.withCredentials = true; + +... +``` + +Далее -- всё как обычно, дополнительных действий со стороны клиента не требуется. + +### Защита от CSRF + +Кросс-доменный `XMLHttpRequest` с куками, безусловно, опаснее, чем анонимный. + +Представьте себе -- человек нечаянно попадает на страницу злого хакера. А она, используя JavaScript, отправляет кросс-доменный запрос на почтовый сервер, например GMail. + +Если с запросом отправятся куки, то получится, что этот запрос выполнился "от имени посетителя". + +Подобные атаки давно существуют в интернете и называются [CSRF-атака](http://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D0%BB%D0%BA%D0%B0_%D0%BC%D0%B5%D0%B6%D1%81%D0%B0%D0%B9%D1%82%D0%BE%D0%B2%D1%8B%D1%85_%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2). Обычно они выполняются при помощи динамической генерации о отправки POST-формы. "Злая" страница может создать произвольную форму и отправить её на почтовый сервер. При этом с формой передадутся куки, то есть авторизация посетителя. Если почтовый сервер полагается только на них, то он выполнит запрошенное действие от имени пользователя. + +**Чтобы защититься от CSRF-атак, почтовый сервер при генерации формы отправки сообщения добавляет в неё специальный ключ:** + +```html +
                        +*!* + +*/!* + ... +
                        +``` + +Этот ключ генерируется случайным образом и привязывается к сессии посетителя. Если посетитель честно открыл браузер и отправил форму со страницы, то в ней будет этот ключ. А "злая" страница этот ключ не знает, ведь его сгенерировал сервер. Даже открыв страницу с почтового сервера в `IFRAME`, она не сможет получить информацию со страницы -- `IFRAME`-то на другом домене. + +...А без ключа она не сможет отправить сообщение. Сервер ведь его проверяет. Правильный ключ доказывает, что форма отправлена именно со страницы почтового сервера. + +В случае с кросс-доменным `XMLHttpRequest` ситуация принципиально меняется и атака становится куда опаснее! + +Если сервер разрешает делать к себе `XMLHttpRequest` с `withCredentials`, то эту защиту легко обойти! Ведь `XMLHttpRequest` не просто делает запрос, а возвращает ответ, к которому JavaScript, (в отличие от результата отправки формы на другой домен) имеет полный доступ. + +Например, "злая" страница может запросить страницу с формой отправки, найти в `responseText` нужный `` -- и вуаля, ключ есть, можно делать новый запрос, уже с гарантией прохождения. + +**Поэтому для запросов с `withCredentials` предусмотрено дополнительное подтверждение со стороны сервера.** + +Он должен показать, что разрешает делать не просто запросы, а запросы от имени посетителя. Как мы видим, это очень серьёзное разрешение. + +**При запросе с `withCredentials` сервер должен вернуть уже не один, а два заголовка: `Access-Control-Allow-Origin: домен` и `Access-Control-Allow-Credentials: true`.** + +Пример заголовков: + +```js +HTTP/1.1 200 OK +Content-Type:text/html; charset=UTF-8 +*!* +Access-Control-Allow-Origin: http://javascript.ru +Access-Control-Allow-Credentials: true +*/!* +``` + +Использование звёздочки `*` в `Access-Control-Allow-Origin` при этом запрещено. + +Если заголовков не будет, то браузер не даст JavaScript'у доступ к ответу сервера. И получится, что запрос -- не опаснее обычной CSRF-атаки. + +## Чтение заголовков + +Чтобы клиент мог прочитать HTTP-заголовок, сервер должен указать его имя в `Access-Control-Expose-Headers`. + +Например: + +``` +HTTP/1.1 200 OK +Content-Type:text/html; charset=UTF-8 +*!* +X-Uid: 123 +X-Authorization: abcdefabcdef +Access-Control-Allow-Origin: http://javascript.ru +Access-Control-Expose-Headers: X-Uid, X-Authentication +*/!* +``` + +Это ограничение относится ко всем заголовкам, кроме "простых": + +``` +Cache-Control +Content-Language +Content-Type +Expires +Last-Modified +Pragma +``` + +**...То есть, `Content-Type` получить всегда можно, а доступ к специфическим заголовкам нужно открывать явно.** + +## Нестандартные методы и заголовки + +В кросс-доменном `XMLHttpRequest` можно указать не только `GET/POST`, но и любой другой метод, например `PUT`, `DELETE`. + +Когда-то никто и не думал, что страница сможет сделать такие запросы. Поэтому ряд веб-сервисов написаны в предположении, что "если метод -- нестандартный, то это не браузер". Некоторые веб-сервисы даже учитывают это при проверке прав доступа. + +**Чтобы пресечь любые недопонимания, браузер использует предзапрос в случаях:** + +
                          +
                        • Если метод -- не GET и не POST
                        • +
                        • Если метод -- POST, но заголовок `Content-Type` имеет значение отличное от `application/x-www-form-urlencoded`, `multipart/form-data` или `text/plain`, например `application/xml`.
                        • +
                        • Если устанавливаются другие HTTP-заголовки
                        • +
                        + +...Любое из условий выше ведёт к тому, что браузер cделает два HTTP-запроса. + +Первый запрос называется "предзапрос" (английский термин "preflight"), он использует метод `OPTIONS`. Его задача -- спросить сервер, разрешает ли он использовать выбранный метод и заголовки. + +Название метода браузер передаёт в `Access-Control-Request-Method`, а если в `XMLHttpRequest` добавлены особые заголовки, то и их тоже -- в `Access-Control-Request-Headers`. + +### Пример запроса COPY + +Рассмотрим запрос `COPY`, который используется в протоколе[WebDAV](http://www.webdav.org/specs/rfc2518.html) для управления файлами через HTTP: + +```js +var xhr = new XMLHttpRequest(); + +xhr.open('COPY', 'http://site.com/~ilya', true); +xhr.setRequestHeader('Destination', 'http://site.com/~ilya.bak'); + +xhr.onload = ... +xhr.onerror = ... + +xhr.send('...'); +``` + +Браузер сначала шлёт предзапрос `OPTIONS`: + +``` +OPTIONS /~ilya HTTP/1.1 +Host: site.com +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Encoding: gzip,deflate +Connection: keep-alive +*!* +Origin: http://javascript.ru +Access-Control-Request-Method: COPY +Access-Control-Request-Headers: Destination +*/!* +``` + +На этот запрос сервер должен ответить статусом 200, указав заголовки `Access-Control-Allow-Method: метод` и, если есть заголовки, `Access-Control-Allow-Headers: разрешённые заголовки`. + +Дополнительно он может указать `Access-Control-Max-Age: sec`, где `sec` -- количество секунд, на которые нужно закэшировать разрешение. Тогда при последующих вызовах метода браузер уже не будет делать предзапрос. + +В протоколе WebDav разрешены многие методы и заголовки, которые и перечислим в ответе: + +``` +HTTP/1.1 200 OK +Content-Type: text/plain +*!*Access-Control-Allow-Methods:*/!* PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST +*!*Access-Control-Allow-Headers:*/!* Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control +*!*Access-Control-Max-Age:*/!* 86400 +``` + +Браузер видит, что метод `COPY` -- в числе разрешённых и заголовок `Destination` -- тоже, и дальше он шлёт уже основной запрос. + +При этом ответ на предзапрос он закэширует на 86400 сек (ровно сутки), так что следующие вызовы `COPY` сразу отправят основной запрос. + +Основной запрос браузера: + +``` +COPY /~ilya HTTP/1.1 +Host: site.com +Content-Type: text/html; charset=UTF-8 +*!* +Destination: http://site.com/~ilya.bak +Origin: http://javascript.ru +*/!* +``` + +Ответ сервера, согласно спецификации [WebDav COPY](http://www.webdav.org/specs/rfc2518.html#rfc.section.8.8.8): + +``` +HTTP/1.1 207 Multi-Status +Content-Type: text/xml; charset="utf-8" +Content-Length: ... +*!* +Access-Control-Allow-Origin: http://javascript.ru +*/!* + + + + ... + +``` + +Таким образом, через `XMLHttpRequest` можно удобно использовать любые методы и заголовки, главное чтобы сервер это разрешал. \ No newline at end of file diff --git a/3-more/10-ajax/7-xhr-onprogress/article.md b/3-more/10-ajax/7-xhr-onprogress/article.md new file mode 100644 index 00000000..19485a16 --- /dev/null +++ b/3-more/10-ajax/7-xhr-onprogress/article.md @@ -0,0 +1,160 @@ +# XMLHttpRequest: индикация прогресса + +Запрос `XMLHttpRequest` состоит из двух фаз: +
                          +
                        1. На первой фазе данные загружаются на сервер. Эта фаза может быть долгой для POST-запросов.
                        2. +
                        3. После того, как данные загружены, браузер скачивает ответ с сервера. Если он большой, то это может занять существенное время.
                        4. +
                        + +Для каждой стадии предусмотрены события, "рассказывающие" о процессе выполнения. +[cut] +## XMLHttpRequestUpload + +На стадии загрузки обработчики нужно ставить на объект `xhr.upload`. Например: + +```js +xhr.upload.onprogress = function(event) { + alert('Загружено ' + event.loaded + ' байт из '+ event.total); +} + +xhr.upload.onload = function() { + alert('Данные полностью загружены на сервер!'); +} +``` + +Объект `xhr.upload` ничего не делает, у него нет методов, он предназначен исключительно для обработки событий при загрузке. + +После того, как загрузка завершена, будет начато скачивание ответа. + +**Обработчик `xhr.onprogress` даёт прогресс на фазе скачивания, а за ней наступает фаза закачки, для которой работает xhr.upload.onprogress.** + +## Загрузка файла с индикатором прогресса + +Современный `XMLHttpRequest` позволяет отправить на сервер всё, что угодно. Текст, файл, форму. + +Мы, для примера, рассмотрим загрузку файла с индикацией прогресса. Это требует от браузера поддержки [File API](http://www.w3.org/TR/FileAPI/), то есть исключает IE<10. + +File API позволяет получить доступ к содержимому файла, который перенесён в браузер при помощи Drag'n'Drop или выбран в поле формы. + +Форма для выбора файла: + +```html +
                        + + +
                        +``` + +Обработчик на её `submit`: + +```js +var form = document.forms.upload; +form.onsubmit = function() { + var file = this.elements.file1.files[0]; + if (file) *!*upload(file, onSuccess, onError, onProgress)*/!*; + return false; +} +``` + +Все, больше нам здесь File API не нужно. Мы получили файл из формы и отправляем его: + +```js +function upload(file, onSuccess, onError, onProgress) { + + var xhr = new XMLHttpRequest(); + + xhr.onload = xhr.onerror = function() { + if(this.status != 200 || this.responseText != 'OK') { + onError(this); + return; + } + onSuccess(); + }; + +*!* + xhr.upload.onprogress = function(event) { + onProgress(event.loaded, event.total); + } +*/!* + + xhr.open("POST", "upload.php", true); + xhr.send(file); + +} +``` + +## Демо + +Полный пример, основанный на коде выше, в котором функции `onSuccess`, `onError`, `onProgress` выводят результат на экран: + +[iframe border=1 src="progress" link zip] + +## Событие onprogress в деталях + +Событие `onprogress` имеет одинаковый вид при закачке на сервер (`xhr.upload.onprogress`) и при получении ответа (`xhr.onprogress`). + +Оно получает объект `event` типа [ProgressEvent](http://www.w3.org/TR/progress-events/) со свойствами: + +
                        +
                        `loaded`
                        +
                        Сколько байт загружено. + +Имеется в виду только тело запроса, заголовки не учитываются.
                        +
                        `lengthComputable`
                        +
                        Если `true`, то известна полное количество байт и оно хранится в свойстве `total`. +
                        +
                        `total`
                        +
                        Общее количество байт для загрузки, если известно. + +При HTTP-запросах оно передаётся в заголовке `Content-Length`. +
                          +
                        • При закачке на сервер браузер всегда точно знает размер пересылаемого фрагмента, поэтому отправляет этот заголовок и учитывает его в `xhr.upload.onprogress`.
                        • +
                        • При скачивании данных -- уже задача сервера поставить этот заголовок, если конечно это возможно.
                        • +
                        +
                        +
                        + +Ещё особенности, которые необходимо учитывать при использовании `onprogress`: + +
                          +
                        • **Событие происходит при каждом полученном/отправленном байте, но не чаще чем раз в 50мс.** + +Это обозначено в [спецификации progress notifications](http://www.w3.org/TR/XMLHttpRequest/#make-progress-notifications). +
                        • +
                        • **При получении данных доступен `xhr.responseText`.** + +Можно заглянуть в него и прочитать текущие, неоконченные данные. Они будут оборваны на каком-то символе, на каком именно -- предсказать сложно, это зависит от сети.
                        • +
                        • **При посылке данных не гарантируется обработка загрузки сервером.** + +Событие `xhr.upload.onprogress` срабатывает, когда данные отправлены браузером. Но оно не гарантирует, что сервер получил, обработал и записал данные на диск. + +Поэтому прогресс-индикатор, получаемый при его помощи, носит приблизительный и оптимистичный характер.
                        • +
                        + + + + + +## Серверная часть: файлы и формы + +При вызове `xhr.send(file)`, файл пересылается в теле запроса, как будто отправлен через форму. + +Если нужно дополнительно передать имя файла или что-то ещё -- это можно сделать в заголовках, через `xhr.setRequestHeader`. И, конечно же, серверный фреймворк должен понимать, как это обработать. + +А что, если серверный фреймворк умеет нормально работать только с обычными формами, а как-то модифицировать его ну совсем нет желания и времени? + +Нам поможет объект [FormData](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/FormData/Using_FormData_Objects): + +```js +var formData = new FormData(); +formData.append("file", file); +xhr.send(formData); +``` + +Данные будут отправлены в кодировке `multipart/form-data`. Серверный фреймворк обработает это как обычную форму. + + + +[head] + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/7-xhr-onprogress/progress/.zip b/3-more/10-ajax/7-xhr-onprogress/progress/.zip new file mode 100755 index 00000000..99a80913 --- /dev/null +++ b/3-more/10-ajax/7-xhr-onprogress/progress/.zip @@ -0,0 +1,2 @@ + + diff --git a/3-more/10-ajax/7-xhr-onprogress/progress/index.html b/3-more/10-ajax/7-xhr-onprogress/progress/index.html new file mode 100755 index 00000000..f7bfde96 --- /dev/null +++ b/3-more/10-ajax/7-xhr-onprogress/progress/index.html @@ -0,0 +1,59 @@ + + + + +
                        + + +
                        + +
                        Прогресс загрузки
                        + + + + diff --git a/3-more/10-ajax/7-xhr-onprogress/progress/upload.php b/3-more/10-ajax/7-xhr-onprogress/progress/upload.php new file mode 100755 index 00000000..6e594cb7 --- /dev/null +++ b/3-more/10-ajax/7-xhr-onprogress/progress/upload.php @@ -0,0 +1,3 @@ +OK \ No newline at end of file diff --git a/3-more/10-ajax/8-xhr-resume/article.md b/3-more/10-ajax/8-xhr-resume/article.md new file mode 100644 index 00000000..7049d282 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/article.md @@ -0,0 +1,318 @@ +# XMLHttpRequest: возобновляемая закачка + +Современный `XMLHttpRequest` даёт возможность загружать файл как угодно: во множество потоков, с догрузкой, с подсчётом контрольной суммы и т.п. + +Здесь мы рассмотрим общий подход к организации загрузки, а его уже можно расширять, адаптировать к своему фреймворку и так далее. + +Поддержка -- все браузеры кроме IE<10. + +[cut] + +## Неточный upload.onprogress + +Ранее мы рассматривали загрузку с индикатором прогресса. Казалось бы, сделать возобновляемую загрузку на его основе очень просто. + +Есть же `xhr.upload.onprogress` -- ставим на него обработчик, по свойству `loaded` события `onprogress` смотрим, сколько байт загрузилось. А при обрыве -- возобновляем загрузку с последнего байта. + +[smart] +К счастью, отослать на сервер нужный отрезок файла -- не проблема, [File API](http://www.w3.org/TR/FileAPI/) позволяет прочитать выбранный участок из файла и отправить его. + +Примерно так: + +```js +var slice = file.slice(10, 100); // прочитать байты с 10го по 99й включительно + +xhr.send(slice); // ... и отправить эти байты в запросе. +``` + +[/smart] + +..Но такая модель не жизнеспособна! + +Всё дело в том, что `upload.onprogress` срабатывает, когда байты *отправлены*, но были ли они получены сервером -- браузер не знает. Может, их прокси-сервер забуферизовал, может серверный процесс "упал" в процессе обработки, может соединение порвалось и байты так и не дошли до получателя. + +**Поэтому `onprogress` годится лишь для красивенького рисования прогресса.** + +Для загрузки нам нужно точно знать количество загруженных байт. Это может сообщить только сервер. + +## Общий вид загрузки + +Мы разберём несколько способов надёжной загрузки. + +У них будет общим объект `Uploader` для загрузки, который имеет примерно следующий вид: + +```js +function Uploader(file, onSuccess, onFail, onProgress) { + + var uploadId = Math.random(); + var startByte = 0; + + var fileId = file.name + '-' + file.size + '-' + (+file.lastModifiedDate); + + var errorCount = 0; + var MAX_ERROR_COUNT = 6; + + function upload() { + ... + } + + function pause() { + ... + } + + function resume() { + ... + } + + this.upload = upload; + this.pause = pause; + this.resume = resume; + +} +``` + +Это -- самый общий интерфейс, разберём его подробнее. + +Аргументы: +
                        +
                        `file`
                        +
                        Объект File API. Может быть получен из формы, либо как результат Drag'n'Drop.
                        +
                        `onSuccess`, `onFail`, `onProgress`
                        +
                        Функции-коллбэки, которые будут вызываться в процессе (`onProgress`) и при окончании загрузки.
                        +
                        + +Используемые переменные: + +
                        +
                        `uploadId`
                        +
                        Уникальный идентификатор загрузки. Генерируется случайно. Передаётся на сервер в начале загрузки и при её возобновлении, чтобы сервер знал, что это тот же файл.
                        +
                        `startByte`
                        +
                        С какого байта загружать. Изначально -- с нулевого.
                        +
                        `fileId`
                        +
                        Идентификатор файла, основанный на его имени, размере и дате модификации. + +По нему текущее состояние загрузки (`uploadId` и `startByte`) можно сохранить в `localStorage`, так что даже при закрытии браузера, а затем открытии и повторной загрузке того же файла -- браузер сможет продолжить загрузку.
                        +
                        `errorCount / MAX_ERROR_COUNT`
                        +
                        Число ошибок / максимальное число непрерывных ошибок, после которого загрузка считается проваленной.
                        +
                        + +Использование: + +```js +var uploader = new Uploader(form.elements.file.files[0], ...); + +uploader.upload(); // загружает с возобновлением +uploader.pause(); // пауза +uploader.resume(); // возобновление загрузки +``` + +## Способ 1: спрашиваем сервер + +Первый способ: + +
                          +
                        1. Функция `upload` загружает файл обычным образом. При ошибке, если это ещё возможно, вызываем `resume`: + +```js +function upload() { + + xhrUpload = new XMLHttpRequest(); + xhrUpload.onload = xhrUpload.onerror = function() { + + if (this.status == 200) { + // успешное завершение загрузки + onSuccess(); + return; + } + + // что-то не так + if (errorCount++ < MAX_ERROR_COUNT) { + setTimeout(resume, 1000); // через 1 сек пробуем ещё раз + } else { + onError(this.statusText); + } + }; + + xhrUpload.open("POST", "upload", true); + // какой файл догружаем /загружаем + xhrUpload.setRequestHeader('X-Upload-Id', uploadId); + // с какого байта (сервер сверит со своим размером для надёжности) + xhrUpload.setRequestHeader('X-Start-Byte', startByte); + // полный размер загружаемого файла + xhrUpload.setRequestHeader('X-File-Size', file.size); + + xhrUpload.upload.onprogress = function(e) { + errorCount = 0; + onProgress( startByte + e.loaded, startByte + e.total); + } + + // отослать, начиная с байта startByte + xhrUpload.send(file.slice(startByte)); +} +``` + +
                        2. +
                        3. При ошибке загрузка возобновляется. Но достоверной информацией о том, сколько байт получено, обладает лишь сервер. + +Функция `resume` спрашивает его об этом запросом `XMLHttpRequest`, а затем возобновляет загрузку: + +```js +function resume() { + xhrStatus = new XMLHttpRequest(); + + xhrStatus.onload = xhrStatus.onerror = function() { + if (this.status == 200) { + startByte = +this.responseText || 0; + upload(); + return; + } + + // что-то не так + if (errorCount++ < MAX_ERROR_COUNT) { + setTimeout(resume, 1000); // через 1 сек пробуем ещё раз + } else { + onError(this.statusText); + } + }; + + xhrStatus.open("GET", "status", true); + xhrStatus.setRequestHeader('X-Upload-Id', uploadId); + xhrStatus.send(''); +} +``` + +
                        4. +
                        5. Пауза-возобновление делаются просто: + +```js +function pause() { + // обрываем все запросы + xhrStatus && xhrStatus.abort(); + xhrUpload && xhrUpload.abort(); +} +``` + +
                        6. +
                        + +### Демо + +[iframe src="upload-resume" link zip border="1" height="160"] + +Полный код включает также сервер на Node.JS с функциям `onUpload` -- начало и возобновление загрузки, а также `onStatus` -- для получения состояния загрузки. + +## Способ 2: статус с сервера + +Предыдущий способ -- прост и хорош. Но для лучшего понимания рассмотрим ещё одну реализацию, которая более точна. + +Событие `upload.onprogress`, которое используется для отображения прогресса загрузки -- неточное. Как мы говорили, оно использует данные браузера, а он не может знать, были ли данные успешно получены и, что самое главное, корректно обработаны сервером. + +Может быть такое, что индикатор показывает "100%", а на самом деле последний фрагмент ещё не загрузился. + +В большинстве случаев эта неточность вполне допустима. Но бывают ситуации, когда мы не имеем права показывать посетителю неточную информацию. + +**Описанный далее способ гарантирует точную информацию о процессе загрузке, цена -- дополнительный запрос.** + +[warn header="Почему нужен ещё один запрос?"] +Первая идея, которая может прийти в голову -- это получать информацию о состоянии закачки из `xhr.onprogress`, то есть сервер должен при каждом полученном пакете байт писать полученную длину в ответ. + +Звучит хорошо, но, к сожалению, в `XMLHttpRequest` браузер *сначала* закачивает данные, а *потом* читает. То есть, сначала сработают события `xhr.upload.onprogress`, `xhr.upload.onload`, а уже затем пойдут события на получение данных: `xhr.onprogress`, `xhr.onload` и другие. Браузер получит ответ сервера целиком уже после окончания загрузки, а нам прогресс нужен -- во время загрузки. +[/warn] + +Будем использовать два запроса: в одном -- закачивать, в другом -- читать прогресс с сервера. + +
                          +
                        • Функция `runUpload` будет открывать запрос `xhrUpload`, который отправляет данные на сервер. У него даже не будет `onprogress`, всю информацию о загрузке мы будем получать напрямую от сервера.
                        • +
                        • Функция `trackStatus` будет открывать второй, параллельный, запрос `xhrStatus` и в нём, через `onprogress`, получать данные с сервера.
                        • +
                        + +Серверный код будет читать из `xhrUpload`, записывать данные на диск и тут же рапортовать об этом в `xhrStatus`. + +Код соответствующей функции `upload`: + +```js +//+ src="browser.js" +``` + +Детали `trackStatus`: +
                          +
                        • В ответ на запрос `xhrStatus` сервер присылает информацию о загрузке с данным идентификатором, дописывая в ответ при поступлении (и, если угодно, записи) очередного пакета -- общий размер загруженных данных. + +Ответ сервера выглядит так: + +``` +1234-56789-123456-200123-423456-... +``` + +Это означает, что сервер сначала считал `1234` байта, затем получил ещё пакет и стало `56789`..И на текущий момент можно быть уверенными, что `423456` байт загружены. +
                        • +
                        • Функция `getLastProgress` читает из текущего ответа сервера последнее число. Обратим внимание, что она не может найти последний `-` и взять всё, что после него, т.к. последнее число, возможно, недогружено. + +Поэтому из ответа выше она возьмёт `200123`.
                        • +
                        • Загрузка считается завершённой только после получения `xhrStatus.onload`.
                        • +
                        + +Важна тонкость здесь -- в том, что запросы независимы. Любой из них может прийти на сервер первым. Возможно и такое, что закачка завершится до того, как запрос на статус достигнет сервера. + +### Демо + +Пример вместе с кодом сервера на NodeJS: + +[iframe src="upload-2way" link zip border="1" height="160"] + +К этому коду можно легко добавить любую логику возобновления загрузки, подсчёта контрольной суммы и т.п. Ведь мы всегда точно знаем, до какого момента сервер получил файл. + +## Способ 3: загружаем по частям + +Есть ещё один способ. Я привожу его здесь для полноты, так как не раз видел его в статьях в интернет. + +Он заключается в том, чтобы разбить файл на части и отсылать их одну за другой. + +Примерно так: + +```js +function upload() { + // каждая часть 64000 байт, + // можно делать меньше для маленьких файлов и больше для быстрых соединений + var chunk = file.slice(startByte, startByte+64000); + + // на текущий момент уже загрузили from байт + onProgress(from, file.size); + + var xhr = new XMLHttpRequest(); + + xhr.onload = function() { + // если всё закачали или сервер ответил ошибку - завершение + // если нет - вызвать загрузку следующей части + upload(file, startByte + 64000); + }; + + xhr.onerror = function() { + setTimeout(upload, 1000); + }; + + // ... + xhr.send(chunk); +} +``` + +Пример заголовков и алгоритма для возобновляемой загрузки: [Спецификация Nginx Upload](http://www.grid.net.ru/nginx/resumable_uploads.ru.html). По ней работает модуль [Nginx Upload Module](http://www.grid.net.ru/nginx/upload.ru.html), но он, к сожалению, нестабилен, на момент написания этих строк замечены падения сервера, да и автор не горит желанием исправлять ошибки и отвечать на вопросы. + +Если вы вдруг видите преимущество этого способа перед описанными выше -- напишите об этом в комментариях. + +## Итого + +Мы рассмотрели 3 способа загрузки файлов через `XMLHttpRequest`: + +
                          +
                        • Простой способ, с неточной индикацией прогресса.
                        • +
                        • Более сложный способ, с увеличенным трафиком, но точной индикацией прогресса.
                        • +
                        • Загрузка по частям.
                        • +
                        + +Все их можно расширить каким угодно образом, включая: +
                          +
                        • Параллельную загрузку одного файла в несколько потоков.
                        • +
                        • Хранение состояния в `localStorage`.
                        • +
                        • Подсчёт контрольной суммы при загрузке. Для быстрой проверки рекомендуется алгоритм Adler32 (это быстрый вариант CRC-32 для длинных строк), для безопасной -- MD5, и то и другое реализуется на JavaScript.
                        • +
                        diff --git a/3-more/10-ajax/8-xhr-resume/browser.js b/3-more/10-ajax/8-xhr-resume/browser.js new file mode 100755 index 00000000..87914d84 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/browser.js @@ -0,0 +1,61 @@ +function Uploader(file, onSuccess, onError, onProgress) { + + // идентификатор загрузки, чтобы стыковать два потока + var uploadId = Math.random(); + + var xhrUpload; + var xhrStatus; + + function upload() { + runUpload(); + trackStatus(); + } + + // *!* Закачка: поток НА сервер */!* + function runUpload() { + xhrUpload = new XMLHttpRequest(); + xhrUpload.onload = xhrUpload.onerror = function() { + xhrStatus.abort(); + if (this.status != 200) { + onError("Upload error " + this.statusText); + } else { + onSuccess(); // не ждем xhrStatus, сервер не обязан хранить статус после завершения upload + } + + }; + xhrUpload.open("POST", "upload", true); + xhrUpload.setRequestHeader('X-Upload-Id', uploadId); + xhrUpload.send(file); + } + + // *!* Получать статус: поток С сервера */!* + function trackStatus() { + xhrStatus = new XMLHttpRequest(); + xhrStatus.onprogress = function() { + onProgress( getLastProgress(xhrStatus.responseText), file.size ); + } + + xhrStatus.onerror = function() { + onError("Upload status error " + this.statusText); + xhrUpload.abort(); + } + + xhrStatus.open("POST", "status-stream", true); + xhrStatus.setRequestHeader('X-Upload-Id', uploadId); + xhrStatus.send(''); + } + + // *!* Функция для чтения количества байт из статуса сервера 12-345-... */!* + function getLastProgress(response) { + // читаем число между последним и предпоследним знаком - + var lastDelimiter = response.lastIndexOf('-'); + if (lastDelimiter < 0) return 0; + + var prevDelimiter = response.lastIndexOf('-', lastDelimiter - 1); + return response.slice(prevDelimiter+1, lastDelimiter); + } + + // внешний интерфейс + this.upload = upload; + +} diff --git a/3-more/10-ajax/8-xhr-resume/upload-2way/.zip b/3-more/10-ajax/8-xhr-resume/upload-2way/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-2way/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/8-xhr-resume/upload-2way/browser.js b/3-more/10-ajax/8-xhr-resume/upload-2way/browser.js new file mode 100755 index 00000000..87914d84 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-2way/browser.js @@ -0,0 +1,61 @@ +function Uploader(file, onSuccess, onError, onProgress) { + + // идентификатор загрузки, чтобы стыковать два потока + var uploadId = Math.random(); + + var xhrUpload; + var xhrStatus; + + function upload() { + runUpload(); + trackStatus(); + } + + // *!* Закачка: поток НА сервер */!* + function runUpload() { + xhrUpload = new XMLHttpRequest(); + xhrUpload.onload = xhrUpload.onerror = function() { + xhrStatus.abort(); + if (this.status != 200) { + onError("Upload error " + this.statusText); + } else { + onSuccess(); // не ждем xhrStatus, сервер не обязан хранить статус после завершения upload + } + + }; + xhrUpload.open("POST", "upload", true); + xhrUpload.setRequestHeader('X-Upload-Id', uploadId); + xhrUpload.send(file); + } + + // *!* Получать статус: поток С сервера */!* + function trackStatus() { + xhrStatus = new XMLHttpRequest(); + xhrStatus.onprogress = function() { + onProgress( getLastProgress(xhrStatus.responseText), file.size ); + } + + xhrStatus.onerror = function() { + onError("Upload status error " + this.statusText); + xhrUpload.abort(); + } + + xhrStatus.open("POST", "status-stream", true); + xhrStatus.setRequestHeader('X-Upload-Id', uploadId); + xhrStatus.send(''); + } + + // *!* Функция для чтения количества байт из статуса сервера 12-345-... */!* + function getLastProgress(response) { + // читаем число между последним и предпоследним знаком - + var lastDelimiter = response.lastIndexOf('-'); + if (lastDelimiter < 0) return 0; + + var prevDelimiter = response.lastIndexOf('-', lastDelimiter - 1); + return response.slice(prevDelimiter+1, lastDelimiter); + } + + // внешний интерфейс + this.upload = upload; + +} diff --git a/3-more/10-ajax/8-xhr-resume/upload-2way/index.html b/3-more/10-ajax/8-xhr-resume/upload-2way/index.html new file mode 100755 index 00000000..cba2dbee --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-2way/index.html @@ -0,0 +1,47 @@ + + + + + + + +
                        + + +
                        + +
                        Индикация прогресса
                        + + + + diff --git a/3-more/10-ajax/8-xhr-resume/upload-2way/index.js b/3-more/10-ajax/8-xhr-resume/upload-2way/index.js new file mode 100755 index 00000000..b49cac30 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-2way/index.js @@ -0,0 +1,118 @@ +var http = require('http'); +var static = require('node-static'); +var fileServer = new static.Server('.'); +var events = require('events'); +var fs = require('fs'); +var path = require('path'); + +var uploads = {}; + +var filesDir = '/tmp'; + +function onUpload(req, res) { + + var uploadId = req.headers['x-upload-id']; + if (!uploadId) { + res.writeHead(400, "No upload id"); + res.end(); + } + + // инициализация новой загрузки + var upload = uploads[uploadId] = new events.EventEmitter(); + upload.bytesWritten = 0; + + var filePath = path.join(filesDir, uploadId); + + // будем сохранять в файл с именем uploadId + var fileStream = fs.createWriteStream(filePath); + + // отправить тело запроса в файл + req.pipe(fileStream); + + // при записи очередного фрагмента событие progress + fileStream.on('drain', function() { + upload.bytesWritten = fileStream.bytesWritten; + upload.emit("progress"); + }); + + // в конце -- событие end + fileStream.on('close', function() { + upload.finished = true; + upload.emit("end"); + }); + + // при ошибках - завершение запроса + fileStream.on('error', function (err) { + res.writeHead(500, "File error"); + res.end('error'); + }); + + // в конце запроса - очистка + req.on('end', function() { + res.end(); + delete uploads[uploadId]; + // в этой демке мы не оставляем файл на диске, + // в реальности здесь будет обработка успешной загрузки + fs.unlinkSync(filePath); + }); + +} + +function onStatusStream(req, res) { + + var uploadId = req.headers['x-upload-id']; + var upload = uploads[uploadId]; + + // нет такой загрузки? + // подождём немного, может почему-то запрос на статус дошёл до сервера раньше, + // и она ещё не началась + if (!upload) { + var interval = setTimeout(function() { + onStatusStream(req, res) + }, 500); + return; + } + + // ..при окончании запроса на статус - прекращаем ждать + req.on('end', function() { + clearInterval(interval); + }); + + // есть загрузка + + // сразу же пишем текущий прогресс + res.write(upload.bytesWritten+'-'); + + // и по мере загрузки дописываем + upload.on('progress', function() { + res.write(upload.bytesWritten+'-'); + }); + + upload.on('end', function() { + res.end(); + }); +} + + + +function accept(req, res) { + if (req.url == '/status-stream') { + onStatusStream(req, res); + } else if (req.url == '/upload' && req.method == 'POST') { + onUpload(req, res); + } else { + fileServer.serve(req, res); + } + +} + + +// ----------------------------------- + +if (!module.parent) { + http.createServer(accept).listen(8080); + console.log('Сервер запущен на порту 8080'); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/8-xhr-resume/upload-resume/.zip b/3-more/10-ajax/8-xhr-resume/upload-resume/.zip new file mode 100755 index 00000000..d3f5a12f --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-resume/.zip @@ -0,0 +1 @@ + diff --git a/3-more/10-ajax/8-xhr-resume/upload-resume/browser.js b/3-more/10-ajax/8-xhr-resume/upload-resume/browser.js new file mode 100755 index 00000000..87b24c17 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-resume/browser.js @@ -0,0 +1,90 @@ +function Uploader(file, onSuccess, onFail, onProgress) { + + // fileId уникальным образом идентифицирует файл + // можно добавить идентификатор сессии посетителя, но он и так будет в заголовках + // использование: можно добавить в localstorage состояние загрузки по этому ключу + var fileId = file.name + '-' + file.size + '-' + (+file.lastModifiedDate); + + // uploadId уникальным образом идентифицирует попытку загрузки + var uploadId = Math.random(); + var errorCount = 0; + var MAX_ERROR_COUNT = 6; // максимальное количество ошибок, между которыми нет успешной загрузки + var startByte = 0; + + var xhrUpload; + var xhrStatus; + + function resume() { + //console.log("resume: check status"); + xhrStatus = new XMLHttpRequest(); + + xhrStatus.onload = xhrStatus.onerror = function() { + if (this.status == 200) { + startByte = +this.responseText || 0; + //console.log("resume: startByte=" + startByte); + upload(); + return; + } + + // что-то не так + if (errorCount++ < MAX_ERROR_COUNT) { + setTimeout(resume, 1000); // через 1 сек пробуем ещё раз + } else { + onError(this.statusText); + } + }; + + xhrStatus.open("GET", "status", true); + xhrStatus.setRequestHeader('X-Upload-Id', uploadId); + xhrStatus.send(''); + } + + + function upload() { + + xhrUpload = new XMLHttpRequest(); + xhrUpload.onload = xhrUpload.onerror = function() { + //console.log("upload end status:" + this.status + " text:"+this.statusText); + + if (this.status == 200) { + // успешное завершение загрузки + onSuccess(); + return; + } + + // что-то не так + if (errorCount++ < MAX_ERROR_COUNT) { + setTimeout(resume, 1000); // через 1 сек пробуем ещё раз + } else { + onError(this.statusText); + } + }; + + xhrUpload.open("POST", "upload", true); + // какой файл догружаем /загружаем + xhrUpload.setRequestHeader('X-Upload-Id', uploadId); + // с какого байта (сервер сверит со своим размером для надёжности) + xhrUpload.setRequestHeader('X-Start-Byte', startByte); + // полный размер загружаемого файла + xhrUpload.setRequestHeader('X-File-Size', file.size); + + xhrUpload.upload.onprogress = function(e) { + errorCount = 0; + onProgress( startByte + e.loaded, startByte + e.total); + } + + // отослать, начиная с байта startByte + xhrUpload.send(file.slice(startByte)); + } + + function pause() { + xhrStatus && xhrStatus.abort(); + xhrUpload && xhrUpload.abort(); + } + + + this.upload = upload; + this.pause = pause; + this.resume = resume; + +} diff --git a/3-more/10-ajax/8-xhr-resume/upload-resume/index.html b/3-more/10-ajax/8-xhr-resume/upload-resume/index.html new file mode 100755 index 00000000..c5a6aba3 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-resume/index.html @@ -0,0 +1,51 @@ + + + + + + + +
                        + + +
                        + + + + + +
                        Индикация прогресса
                        + + + + diff --git a/3-more/10-ajax/8-xhr-resume/upload-resume/index.js b/3-more/10-ajax/8-xhr-resume/upload-resume/index.js new file mode 100755 index 00000000..fa69eb42 --- /dev/null +++ b/3-more/10-ajax/8-xhr-resume/upload-resume/index.js @@ -0,0 +1,115 @@ +var http = require('http'); +var static = require('node-static'); +var fileServer = new static.Server('.'); +var fs = require('fs'); +var path = require('path'); + +var uploads = {}; + +var filesDir = '/tmp'; + +function onUpload(req, res) { + + var uploadId = req.headers['x-upload-id']; + var startByte = req.headers['x-start-byte']; + + if (!uploadId) { + res.writeHead(400, "No upload id"); + res.end(); + } + + var filePath = path.join(filesDir, uploadId); // можно положить файл и в другое место + + //console.log("onUpload uploadId: ", uploadId); + // инициализация новой загрузки + if (!uploads[uploadId]) uploads[uploadId] = {}; + var upload = uploads[uploadId]; + + console.log("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte) + + // если байт 0, то создать новый файл, иначе проверить размер и дописать + if (startByte == 0) { + upload.bytesReceived = 0; + var fileStream = fs.createWriteStream(filePath, {flags: 'w'}); + console.log("New file created: " + filePath); + } else { + if (upload.bytesReceived != startByte) { + res.writeHead(400, "Wrong start byte"); + res.end(upload.bytesReceived); + return; + } + fileStream = fs.createWriteStream(filePath, {flags: 'a'}); + console.log("File reopened: " + filePath); + } + + + req.on('data', function(data) { + upload.bytesReceived += data.length; + }); + + // отправить тело запроса в файл + req.pipe(fileStream); + + // в конце -- событие end + fileStream.on('close', function() { + if (upload.bytesReceived == req.headers['x-file-size']) { + // полностью загрузили + console.log("File finished"); + delete uploads[uploadId]; + + fs.unlinkSync(filePath); // удаляю загруженный файл (а мог бы делать с ним что-то еще) + + res.end("Success " + upload.bytesReceived); + } else { + // соединение оборвано, дескриптор закрылся но файл оставляем + console.log("File unfinished, stopped at " + upload.bytesReceived); + res.end(); + } + }); + + // при ошибках - завершение запроса + fileStream.on('error', function (err) { + console.log("fileStream error"); + res.writeHead(500, "File error"); + res.end(); + }); + +} + +function onStatus(req, res) { + var uploadId = req.headers['x-upload-id']; + var upload = uploads[uploadId]; + console.log("onStatus uploadId:", uploadId, " upload:", upload); + if (!upload) { + res.writeHead(404); + res.end("No such upload"); + return; + } + + res.end(String(upload.bytesReceived)); +} + + +function accept(req, res) { + if (req.url == '/status') { + onStatus(req, res); + } else if (req.url == '/upload' && req.method == 'POST') { + onUpload(req, res); + } else { + fileServer.serve(req, res); + } + +} + + + + +// ----------------------------------- + +if (!module.parent) { + http.createServer(accept).listen(8080); + console.log('Сервер запущен на порту 8080'); +} else { + exports.accept = accept; +} + diff --git a/3-more/10-ajax/9-websockets/article.md b/3-more/10-ajax/9-websockets/article.md new file mode 100644 index 00000000..15e93f13 --- /dev/null +++ b/3-more/10-ajax/9-websockets/article.md @@ -0,0 +1,494 @@ +# WebSocket + +Протокол `WebSocket` (стандарт [RFC 6455](http://tools.ietf.org/html/rfc6455)) предназначен для решения любых задач и снятия ограничений обмена данными между браузером и сервером. + +Он позволяет пересылать любые данные, на любой домен, безопасно и почти без лишнего сетевого трафика. + +[cut] + +## Пример браузерного кода + +Для открытия соединения достаточно создать объект `WebSocket`, указав в нём специальный протокол `ws`.: + +```js +var socket = new WebSocket("*!*ws*/!*://javascript.ru/ws"); +``` + +У объекта `socket` есть четыре коллбэка: один при получении данных и три -- при изменениях в состоянии соединения: + +```js + +*!*socket.onopen*/!* = function() { + alert("Соединение установлено."); +}; + +*!*socket.onclose*/!* = function(event) { + if (event.wasClean) { + alert('Соединение закрыто чисто'); + } else { + alert('Обрыв соединения'); // например, "убит" процесс сервера + } + alert('Код: ' + event.code + ' причина: ' + event.reason); +}; + +*!*socket.onmessage*/!* = function(event) { + alert("Получены данные " + event.data); +}; + +*!*socket.onerror*/!* = function(error) { + alert("Ошибка " + error.message); +}; +``` + +**Для посылки данных используется метод `socket.send(data)`. Пересылать можно любые данные.** + +Например, строку: + +```js +socket.send("Привет"); +``` + +...Или файл, выбранный в форме: + +```js +socket.send(*!*form.elements[0].file*/!*); +``` + +Просто, не правда ли? Выбираем, что переслать, и `socket.send()`. + +**Для того, чтобы коммуникация была успешной, сервер должен поддерживать протокол WebSocket.** + +Чтобы лучше понимать происходящее -- посмотрим, как он устроен. + +## Установление WebSocket-соединения + +Протокол `WebSocket` работает *над* HTTP. + +Это означает, что при соединении браузер отправляет специальные заголовки, спрашивая: "поддерживает ли сервер WebSocket?". + +Если сервер в ответных заголовках отвечает "да, поддерживаю", то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего. + +### Установление соединения + +Пример запроса от браузера при создании нового объекта `new WebSocket("ws://server.example.com/chat")`: + +``` +GET /chat HTTP/1.1 +Host: server.example.com +Upgrade: websocket +Connection: Upgrade +Origin: http://javascript.ru +Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== +Sec-WebSocket-Version: 13 +``` + +Описания заголовков: +
                        +
                        GET, Host
                        +
                        Стандартные HTTP-заголовки из URL запроса
                        +
                        Upgrade, Connection
                        +
                        Указывают, что браузер хочет перейти на websocket.
                        +
                        Origin
                        +
                        Протокол, домен и порт, откуда отправлен запрос.
                        +
                        Sec-WebSocket-Key
                        +
                        Случайный ключ, который генерируется браузером: 16 байт в кодироке [Base64](http://ru.wikipedia.org/wiki/Base64).
                        +
                        Sec-WebSocket-Version
                        +
                        Версия протокола. Текущая версия: 13.
                        +
                        + +Все заголовки, кроме `GET` и `Host`, браузер генерирует сам, без возможности вмешательства JavaScript. + +[smart header="Такой XMLHttpRequest создать нельзя"] +Создать подобный XMLHttpRequest-запрос (подделать `WebSocket`) невозможно, по одной простой причине: указанные выше заголовки запрещены к установке методом `setRequestHeader`. +[/smart] + +**Сервер может проанализировать эти заголовки и решить, разрешает ли он `WebSocket` с данного домена `Origin`.** + +Ответ сервера, если он понимает и разрешает `WebSocket`-подключение: + +``` +HTTP/1.1 101 Switching Protocols +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= +``` + +Здесь строка `Sec-WebSocket-Accept` представляет собой перекодированный по специальному алгоритму ключ `Sec-WebSocket-Key`. Браузер использует её для проверки, что ответ предназначается именно ему. + +Затем данные передаются по специальному протоколу, структура которого ("фреймы") изложена далее. И это уже совсем не HTTP. + +### Расширения и подпротоколы +Также возможны дополнительные заголовки `Sec-WebSocket-Extensions` и `Sec-WebSocket-Protocol`, описывающие расширения и подпротоколы (subprotocol), которые поддерживает данный клиент. + +Посмотрим разницу между ними на двух примерах: +
                          +
                        • Заголовок `Sec-WebSocket-Extensions: deflate-frame` означает, что браузер поддерживает модификацию протокола, обеспечивающую сжатие данных. + +Это говорит не о самих данных, а об улучшении способа их передачи. Браузер сам формирует этот заголовок.
                        • +
                        • Заголовок `Sec-WebSocket-Protocol: soap, wamp` говорит о том, что по WebSocket браузер собирается передавать не просто какие-то данные, а данные в протоколах [SOAP](http://ru.wikipedia.org/wiki/SOAP) или WAMP ("The WebSocket Application Messaging Protocol"). Стандартные подпротоколы регистрируются в специальном каталоге [IANA](http://www.iana.org/assignments/websocket/websocket.xml). + +Этот заголовок браузер поставит, если указать второй необязательный параметр `WebSocket`: + +```js + +var socket = new WebSocket("*!*ws*/!*://javascript.ru/ws", ["soap", "wamp"]); +``` + +
                        • +
                        + +При наличии таких заголовков сервер может выбрать расширения и подпротоколы, которые он поддерживает, и ответить с ними. + +Например, запрос: + +``` +GET /chat HTTP/1.1 +Host: server.example.com +Upgrade: websocket +Connection: Upgrade +Origin: http://javascript.ru +Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== +Sec-WebSocket-Version: 13 +*!* +Sec-WebSocket-Extensions: deflate-frame +Sec-WebSocket-Protocol: soap, wamp +*/!* +``` + +Ответ: + +``` +HTTP/1.1 101 Switching Protocols +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= +*!* +Sec-WebSocket-Extensions: deflate-frame +Sec-WebSocket-Protocol: soap +*/!* +``` + +В ответе выше сервер указывает, что поддерживает расширение `deflate-frame`, а из запрошенных подпротоколов -- только SOAP. + +### WSS + +Соединение `WebSocket` можно открывать как `WS://` или как `WSS://`. Протокол `WSS` представляет собой WebSocket над HTTPS. + +**Кроме большей безопасности, у `WSS` есть важное преимущество перед обычным `WS` -- большая вероятность соединения.** + +Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP -- нет. + +Если между клиентом и сервером есть прокси, то в случае с HTTP все WebSocket-заголовки и данные передаются через него. Прокси имеет к ним доступ, ведь они никак не шифруются, и может расценить происходящее как нарушение протокола HTTP, обрезать заголовки или оборвать передачу. + +А в случае с `WSS` весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через `WSS` выше, чем через `WS`. + +## Формат данных + +Полное описание протокола содержится в [RFC 6455](http://tools.ietf.org/html/rfc6455). + +Здесь представлено частичное описание с комментариями самых важных его частей. Если вы хотите понять стандарт, то рекомендуется сначала прочитать это описание. + +### Описание фрейма + +В протоколе WebSocket предусмотрены несколько видов пакетов ("фреймов"). + +Они делятся на два больших типа: фреймы с данными ("data frames") и управляющие ("control frames"), предназначенные для проверки связи (PING) и закрытия соединения. + +Фрейм, согласно стандарту, выглядит так: + +
                        +    0                   1                   2                   3
                        +    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
                        +   +-+-+-+-+-------+-+-------------+-------------------------------+
                        +   |F|R|R|R| опкод |М| Длина тела  |    Расширенная длина тела     |
                        +   |I|S|S|S|(4бита)|А|   (7бит)    |            (1 байт)           |
                        +   |N|V|V|V|       |С|             |(если длина тела==126 или 127) |
                        +   | |1|2|3|       |К|             |                               |
                        +   | | | | |       |А|             |                               |  
                        +   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
                        +   |  Продолжение расширенной длины тела, если длина тела = 127    |
                        +   + - - - - - - - - - - - - - - - +-------------------------------+
                        +   |                               |  Ключ маски, если МАСКА = 1   |
                        +   +-------------------------------+-------------------------------+
                        +   | Ключ маски (продолжение)      |       Данные фрейма ("тело")  |
                        +   +-------------------------------- - - - - - - - - - - - - - - - +
                        +   :                     Данные продолжаются ...                   :
                        +   + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
                        +   |                     Данные продолжаются ...                   |
                        +   +---------------------------------------------------------------+
                        +
                        + +С виду -- не очень понятно, во всяком случае, для большинства людей. + +**Позвольте пояснить: читать следует слева-направо, сверху-вниз, каждая горизонтальная полоска это 32 бита.** + +То есть, вот первые 32 бита: + +
                        +    0                   1                   2                   3
                        +    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
                        +   +-+-+-+-+-------+-+-------------+-------------------------------+
                        +   |F|R|R|R| опкод |М| Длина тела  |    Расширенная длина тела     |
                        +   |I|S|S|S|(4бита)|А|   (7бит)    |            (1 байт)           |
                        +   |N|V|V|V|       |С|             |(если длина тела==126 или 127) |
                        +   | |1|2|3|       |К|             |                               |
                        +   | | | | |       |А|             |                               |  
                        +   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
                        +
                        + +Сначала идёт бит FIN (вертикальная надпись на рисунке), затем биты RSV1, RSV2, RSV3 (их смысл раскрыт ниже), затем "опкод", "МАСКА" и, наконец, "Длина тела", которая занимает 7 бит. Затем, если "Длина тела" равна 126 или 127, идёт "Расширенная длина тела", потом (на следующей строке, то есть после первых 32 бит) будет её продолжение, ключ маски, и потом данные. + +А теперь -- подробное описание частей фрейма, то есть как именно передаются сообщения: + +
                        +
                        FIN: 1 бит
                        +
                        Одно сообщение, если оно очень длинное (вызовом `send` можно передать хоть целый файл), может состоять из множества фреймов ("быть фрагментированным"). + +У всех фреймов, кроме последнего, этот фрагмент установлен в `0`, у последнего -- в `1`. + +Если сообщение состоит из одного-единственного фрейма, то он в нём `FIN` равен `1`.
                        +
                        RSV1, RSV2, RSV3: 1 бит каждый
                        +
                        В обычном WebSocket равны `0`, предназначены для расширений протокола. Расширение может записать в эти биты свои значения.
                        +
                        Опкод: 4 бита
                        +
                        Задаёт тип фрейма, который позволяет интерпретировать находящиеся в нём данные. Возможные значения: +
                          +
                        • `0x1` обозначает текстовый фрейм.
                        • +
                        • `0x2` обозначает двоичный фрейм.
                        • +
                        • `0x3-7` зарезервированы для будущих фреймов с данными.
                        • +
                        • `0x8` обозначает закрытие соединения этим фреймом.
                        • +
                        • `0x9` обозначат PING.
                        • +
                        • `0xA` обозначат PONG.
                        • +
                        • `0xB-F` зарезервированы для будущих управляющих фреймов.
                        • +
                        • `0x0` обозначает фрейм-продолжение для фрагментированного сообщения. Он интерпретируется, исходя из ближайшего предыдущего ненулевого типа.
                        • +
                        +
                        +
                        Маска: 1 бит
                        +
                        Если этот бит установлен, то данные фрейма маскированы. Более подробно маску и маскирование мы рассмотрим далее.
                        +
                        Длина тела: 7 битов, 7+16 битов, или 7+64 битов
                        +
                        Если значение поле "Длина тела" лежит в интервале `0-125`, то оно обозначает длину тела (используется далее). +Если `126`, то следующие 2 байта интерпретируются как 16-битное беззнаковое целое число, содержащее длину тела. +Если `127`, то следующие 8 байт интерпретируются как 64-битное беззнаковое целое, содержащее длину. + +Такая хитрая схема нужна, чтобы минимизировать накладные расходы. Для сообщений длиной `125` байт и меньше хранение длины потребует всего 7 битов, для бóльших (до 65536) -- 7 битов + 2 байта, ну а для ещё бóльших -- 7 битов и 8 байт. Этого хватит для хранения длины сообщения размером в гигабайт и более.
                        +
                        Ключ маски: 4 байта.
                        +
                        Если бит `Маска` установлен в 0, то этого поля нет. Если в `1` то эти байты содержат маску, которая налагается на тело (см. далее).
                        +
                        Данные фрейма (тело)
                        +
                        Состоит из "данных расширений" и "данных приложения", которые идут за ними. Данные расширений определяются конкретными расширениями протокола и по умолчанию отсутствуют. Длина тела должна быть равна указанной в заголовке.
                        +
                        + +### Примеры + +Некоторые примеры сообщений: + +
                          +
                        • Нефрагментированное текстовое сообщение `Hello` без маски: + +``` +0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (содержит "Hello") +``` + +В заголовке первый байт содержит `FIN=1` и `опкод=0x1` (получается `10000001` в двоичной системе, то есть `0x81` -- в 16-ричной), далее идёт длина `0x5`, далее текст. +
                        • +
                        • Фрагментированное текстовое сообщение `Hello World` из трёх частей, без маски, может выглядеть так: + +``` +0x01 0x03 0x48 0x65 0x6c 0x6c 0x6f (содержит "Hello") +0x00 0x01 0x20 (содержит " ") +0x80 0x05 0x57 0x6f 0x72 0x6c 0x64 (содержит "World") +``` + +
                            +
                          1. У первого фрейма `FIN=0` и текстовый опкод `0x1`.
                          2. +
                          3. У второго `FIN=0` и опкод `0x0`. При фрагментации сообщения, у всех фреймов, кроме первого, опкод пустой (он один на всё сообщение).
                          4. +
                          5. У третьего, последнего фрейма `FIN=1`.
                          6. +
                          +
                        • +
                        + +А теперь посмотрим на все те замечательные возможности, которые даёт этот формат фрейма. + +### Фрагментация + +Позволяет отправлять сообщения в тех случаях, когда на момент начала посылки полный размер ещё неизвестен. + +Например, идёт поиск в базе данных и что-то уже найдено, а что-то ещё может быть позже. + +
                          +
                        • У всех сообщений, кроме последнего, бит `FIN=0`.
                        • +
                        • Опкод указывается только у первого, у остальных он должен быть равен `0x0`.
                        • +
                        + +### PING / PONG + +В протокол встроена проверка связи при помощи управляющих фреймов типа PING и PONG. + +Тот, кто хочет проверить соединение, отправляет фрейм PING с произвольным телом. Его получатель должен в разумное время ответить фреймом PONG с тем же телом. + +Этот функционал встроен в браузерную реализацию, так что браузер ответит на PING сервера, но управлять им из JavaScript нельзя. + +**Иначе говоря, сервер всегда знает, жив ли посетитель или у него проблема с сетью.** + +### Чистое закрытие + +При закрытии соединения сторона, желающая это сделать (обе стороны в WebSocket равноправны) отправляет закрывающий фрейм (опкод `0x8`), в теле которого указывает причину закрытия. + +В браузерной реализации эта причина будет содержаться в свойстве `reason` события `onclose`. + +**Наличие такого фрейма позволяет отличить "чистое закрытие" от обрыва связи.** + +В браузерной реализации событие `onclose` при чистом закрытии имеет `event.wasClean = true`. + +### Коды закрытия + +Коды закрытия вебсокета `event.code`, чтобы не путать их с HTTP-кодами, состоят из 4 цифр: + +
                        +
                        `1000`
                        +
                        Нормальное закрытие.
                        +
                        `1001`
                        +
                        Удалённая сторона "исчезла". Например, процесс сервера убит или браузер перешёл на другую страницу.
                        +
                        `1002`
                        +
                        Удалённая сторона завершила соединение в связи с ошибкой протокола.
                        +
                        `1003`
                        +
                        Удалённая сторона завершила соединение в связи с тем, что она получила данные, которые не может принять. Например, сторона, которая понимает только текстовые данные, может закрыть соединение с таким кодом, если приняла бинарное сообщение.
                        +
                        + +### Атака "отравленный кэш" + +В ранних реализациях WebSocket существовала уязвимость, называемая "отравленный кэш" (cache poisoning). + +**Она позволяла атаковать кэширующие прокси-сервера, в частности, корпоративные.** + +Атака осуществлялась так: + +
                          +
                        1. Хакер заманивает доверчивого посетителя (далее Жертва) на свою страницу.
                        2. +
                        3. Страница открывает `WebSocket`-соединение на сайт хакера. Предполагается, что Жертва сидит через прокси. Собственно, на прокси и направлена эта атака.
                        4. +
                        5. Страница формирует специального вида WebSocket-запрос, который (и здесь самое главное!) ряд прокси серверов не понимают. + +Они пропускают начальный запрос через себя (который содержит `Connection: upgrade`) и думают, что далее идёт уже следующий HTTP-запрос. + +..Но на самом деле там данные, идущие через вебсокет! И обе стороны вебсокета (страница и сервер) контролируются Хакером. Так что хакер может передать в них нечто похожее на GET-запрос к известному ресурсу, например `http://code.jquery.com/jquery.js`, а сервер ответит "якобы кодом jQuery" с кэширующими заголовками. + +Прокси послушно проглотит этот ответ и закэширует "якобы jQuery".
                        6. +
                        7. В результате при загрузке последующих страниц любой пользователь, использующий тот же прокси, что и Жертва, получит вместо `http://code.jquery.com/jquery.js` хакерский код.
                        8. +
                        + +Поэтому эта атака и называется "отравленный кэш". + +**Такая атака возможна не для любых прокси, но при анализе уязвимости было показано, что она не теоретическая, и уязвимые прокси действительно есть.** + +Поэтому придумали способ защиты -- "маску". + + +### Маска для защиты от атаки + +Для того, чтобы защититься от атаки, и придумана маска. + +*Ключ маски* -- это случайное 32-битное значение, которое варьируется от пакета к пакету. Тело сообщения проходит через XOR `^` с маской, а получатель восстанавливает его повторным XOR с ней (можно легко доказать, что `(x ^ a) ^ a == x`). + +Маска служит двум целям: +
                          +
                        1. Она генерируется браузером. Поэтому теперь хакер не сможет управлять реальным содержанием тела сообщения. После накладывания маски оно превратиться в бинарную мешанину.
                        2. +
                        3. Получившийся пакет данных уже точно не может быть воспринят промежуточным прокси как HTTP-запрос.
                        4. +
                        + +**Наложение маски требует дополнительных ресурсов, поэтому протокол WebSocket не требует её.** + +Если по этому протокола связываются два клиента (не обязательно браузеры), доверяющие друг другу и посредникам, то можно поставить бит `Маска` в `0`, и тогда ключ маски не указывается. + + +## Пример + +Рассмотрим прототип чата на WebSocket и Node.JS. + +HTML: посетитель отсылает сообщения из формы и принимает в `div` + +```html + +
                        + + +
                        + + +
                        +``` + +Код на клиенте: + +```js +// создать подключение +var socket = new WebSocket("ws://localhost:8081"); + +// отправить сообщение из формы publish +document.forms.publish.onsubmit = function() { + var outgoingMessage = this.message.value; + + socket.send(outgoingMessage); + return false; +}; + +// обработчик входящих сообщений +socket.onmessage = function(event) { + var incomingMessage = event.data; + showMessage(incomingMessage); +}; + +// показать сообщение в div#subscribe +function showMessage(message) { + var messageElem = document.createElement('div'); + messageElem.appendChild(document.createTextNode(message)); + document.getElementById('subscribe').appendChild(messageElem); +} +``` + +Серверный код можно писать на любой платформе. В нашем случае это будет Node.JS, с использованием модуля [ws](http://einaros.github.com/ws/): + +```js +var WebSocketServer = new require('ws'); + +// подключенные клиенты +var clients = {}; + +// WebSocket-сервер на порту 8081 +var webSocketServer = new WebSocketServer.Server({port: 8081}); +webSocketServer.on('connection', function(ws) { + + var id = Math.random(); + clients[id] = ws; + console.log("новое соединение " + id); + + ws.on('message', function(message) { + console.log('получено сообщение ' + message); + + for(var key in clients) { + clients[key].send(message); + } + }); + + ws.on('close', function() { + console.log('соединение закрыто ' + id); + delete clients[id]; + }); + +}); +``` + +Рабочий пример можно скачать: [websocket.zip](/zip/tutorial/ajax/websocket.zip). Понадобится поставить два модуля: `npm install node-static && npm install ws`. +## Итого + +WebSocket -- современное средство коммуникации. Кросс-доменное, универсальное, безопасное. + +На текущий момент он работает в браузерах IE10+, FF11+, Chrome 16+, Safari 6+, Opera 12.5+. В более старых версиях FF, Chrome, Safari, Opera есть поддержка черновых редакций протокола. + +Там, где вебсокеты не работают -- обычно используют другие транспорты, например `IFRAME`. Вы найдёте их в других статьях этого раздела. + +Есть и готовые библиотеки, реализующие функционал COMET с использованием сразу нескольких транспортов, из которых вебсокет имеет приоритет. Как правило, библиотеки состоят из двух частей: клиентской и серверной. + +Например, для Node.JS одной из самых известных библиотек является [Socket.IO](http://socket.io). + +К недостаткам библиотек следует отнести то, что некоторые продвинутые возможности WebSocket, такие как двухсторонний обмен бинарными данными, в них недоступны. С другой -- в большинстве случаев стандартного текстового обмена вполне достаточно. +[head] + +[/head] \ No newline at end of file diff --git a/3-more/10-ajax/index.md b/3-more/10-ajax/index.md new file mode 100644 index 00000000..5956b3cd --- /dev/null +++ b/3-more/10-ajax/index.md @@ -0,0 +1,5 @@ +# AJAX + +Современный `XMLHttpRequest`, включая поддержку докачки, индикации прогресса, кросс-доменных запросов. + +WebSocket. Альтернативные методы общения с сервером при помощи JSONP и IFRAME'ов. \ No newline at end of file diff --git a/3-more/11-css-for-js/1-css-why/article.md b/3-more/11-css-for-js/1-css-why/article.md new file mode 100644 index 00000000..baf3958b --- /dev/null +++ b/3-more/11-css-for-js/1-css-why/article.md @@ -0,0 +1,73 @@ +# О чём пойдёт речь + +Неужели мы сейчас будем учить CSS? Ничего подобного. Предполагается, что вы *уже* знаете CSS, во всяком случае понимаете его на таком уровне, который позволяет делать Web-страницы. + +[cut] +Особенность квалификации JavaScript-разработчика заключается в том, что он не обязан выбирать цвета, рисовать иконки, "делать красиво". Он также не обязан верстать макет в HTML, разве что если является по совместительству специалистом-верстальщиком. + +**Вот что он должен уметь абсолютно точно -- так это и разработать такую структуру HTML/CSS для элементов управления, которая не сломается, и с которой ему же потом удобно будет взаимодействовать**. + +Это требует **отличного знания CSS в области позиционирования** элементов, включая тонкости работы `display`, `margin`, `border`, `outline`, `position`, `float`, `border-box` и остальных свойств, а также подходы к построению структуры компонент (CSS layouts). + +Многое можно сделать при помощи JavaScript. И зачастую, не зная CSS, так и делают. Но мы на это ловиться не будем. + +[summary] +Если что-то можно сделать через CSS -- лучше делать это через CSS. +[/summary] + +Причина проста -- как бы ни был сложен CSS, JavaScript ещё сложнее. С этим можно поспорить, особенно если достать из нафталина IE 6,7 (IE5.5 не завалялся?) и показать, что CSS там ну совсем никакой. Да и в IE8 тоже есть забавные баги. + +**Как правило, даже если CSS на вид сложнее -- поддерживать и развивать его проще, чем JS**. Поэтому овчинка стоит выделки. + +Кроме того, есть ещё одно наблюдение. + +[summary] +Знание JavaScript не может заменить знание CSS. +[/summary] + +Жить становится приятнее и проще, если есть хорошее знание и CSS и JavaScript. + +## Чек-лист + +Ниже находится "чек-лист". Если хоть одно свойство незнакомо -- это стоп-сигнал для дальнейшего чтения этого раздела. + +
                          +
                        • Блочная модель, включая: +
                            +
                          • `margin`, `padding`, `border`, `overflow`
                          • +
                          • а также `height/width` и `min-height/min-width`.
                          • +
                          +
                        • +
                        • Текст: +
                            +
                          • `font`
                          • `line-height`.
                          • +
                        • +
                        • Различные курсоры `cursor`.
                        • +
                        • Позиционирование: +
                          • `position`, `float`, `clear`, `display`, `visibility`
                          • +
                          • Центрирование при помощи CSS
                          • +
                          • Перекрытие `z-index` и прозрачность `opacity`
                          • +
                          +
                        • +
                        • Cелекторы: +
                          • Приоритет селекторов
                          • +
                          • Селекторы `#id`, `.class`, `a > b`
                          • +
                          +
                        • +
                        • Сброс браузерных стилей, reset.css +
                        • +
                        + +## Почитать + +Книжек много, но хороших -- как всегда, мало. + +С уверенностью могу рекомендовать следующие: + + + +Дальнейшие статьи раздела не являются *учебником* CSS, поэтому пожалуйста, изучите эту технологию до них. Это *очерки для лучшей систематизации и дополнения* уже существующих знаний. \ No newline at end of file diff --git a/3-more/11-css-for-js/10-box-sizing/article.md b/3-more/11-css-for-js/10-box-sizing/article.md new file mode 100644 index 00000000..123c953f --- /dev/null +++ b/3-more/11-css-for-js/10-box-sizing/article.md @@ -0,0 +1,178 @@ +# Свойство "box-sizing" + +Свойство `box-sizing` может принимать одно из двух значений -- `border-box` или `content-box`. В зависимости от выбранного значения браузер по-разному трактует значение свойств `width/height`. +[cut] +## Значения box-sizing + + + +
                        +
                        `content-box`
                        +
                        Это значение по умолчанию. В этом случае свойства `width/height` обозначают то, что находится *внутри `padding`*.
                        +
                        `border-box`
                        +
                        Значения `width/height` задают высоту/ширину *всего элемента*.
                        +
                        + +Для большей наглядности посмотрим на картинку этого `div` в зависимости от `box-sizing`: + +```css +div { + width: 200px; + height: 100px; +*!* + box-sizing: border-box (вверху) | content-box (внизу); +*/!* + + padding: 20px; + border:5px solid brown; +} +``` + + + +В верхнем случае браузер нарисовал весь элемент размером в `width x height`, в нижнем -- интерпретировал `width/height` как размеры внутренней области. + +Исторически сложилось так, что по умолчанию принят `content-box`, а `border-box` некоторые браузеры используют если не указан `DOCTYPE`, в режиме совместимости. + +Но есть как минимум один случай, когда явное указание `border-box` может быть полезно: растягивание элемента до ширины родителя. + +## Пример: подстроить ширину к родителю + +Задача: подогнать элемент по ширине внешнего элемента, чтобы он заполнял всё его пространство. Без привязки к конкретному размеру элемента в пикселях. + +Например, мы хотим, чтобы элементы формы ниже были одинакового размера: + +```html + + + +
                        +*!* + + + +*/!* +
                        +``` + +Как сделать, чтобы элементы растянулись чётко по ширине `FORM`? Попробуйте добиться этого самостоятельно, перед тем как читать дальше. + +### Попытка width:100% + +Первое, что приходит в голову -- поставить всем `INPUT'ам` ширину `width: 100%`. + +Попробуем: + +```html + + + +
                        + + + +
                        +``` + +Как видно, не получается. **Элементы вылезают за пределы родителя.** + +**Причина -- ширина элемента 100% по умолчанию относится к внутренней области, не включающей `padding` и `border`.** То есть, внутренняя область растягивается до `100%` родителя, и к ней снаружи прибавляются `padding/border`, которые и вылезают. + +Есть два решения этой проблемы. + +### Решение: дополнительный элемент + +Можно убрать `padding/border` у элементов `INPUT/SELECT` и завернуть каждый из них в дополнительный `DIV`, который будет обеспечивать дизайн: + +```html + + + +
                        +
                        +
                        +
                        +
                        +``` + +В принципе, это работает. Но нужны дополнительные элементы. А если мы делаем дерево или большую редактируемую таблицу, да и вообще -- любой интерфейс, где элементов и так много, то лишние нам точно не нужны. + +Кроме того, такое решение заставляет пожертвовать встроенным в браузер дизайном элементов `INPUT/SELECT`. + +### Решение: box-sizing + +Существует другой способ, гораздо более естественный, чем предыдущий. + +**При помощи `box-sizing: border-box` мы можем сказать браузеру, что ширина, которую мы ставим, относится к элементу полностью, включая `border` и `padding`**: + +```html + + + +
                        + + + +
                        +``` + +Мы сохранили "родную" рамку вокруг `INPUT/SELECT` и не добавили лишних элементов. Всё замечательно. + +Свойство `box-sizing` поддерживается в IE начиная с версии 8. \ No newline at end of file diff --git a/3-more/11-css-for-js/10-box-sizing/border-box.png b/3-more/11-css-for-js/10-box-sizing/border-box.png new file mode 100755 index 00000000..89ab8342 Binary files /dev/null and b/3-more/11-css-for-js/10-box-sizing/border-box.png differ diff --git a/3-more/11-css-for-js/11-margin/1-failing-margins/solution.md b/3-more/11-css-for-js/11-margin/1-failing-margins/solution.md new file mode 100644 index 00000000..a2899ad1 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/1-failing-margins/solution.md @@ -0,0 +1,5 @@ +Ошибка заключается в том, что `margin` при задании в процентах высчитавается *относительно ширины*. Так написано [в стандарте](http://www.w3.org/TR/CSS2/box.html#margin-properties). + +При этом не важно, какой отступ: левый, правый. верхний или нижний. Все они в процентах отсчитываются от ширины. Из-за этого и ошибка. + +Ситуацию можно исправить, например, заданием `margin-top/margin-bottom` в пикселях, если это возможно или, в качестве альтернативы, использовать другие средства, в частности, `position` или `padding-top/padding-bottom` на родителе. \ No newline at end of file diff --git a/3-more/11-css-for-js/11-margin/1-failing-margins/task.md b/3-more/11-css-for-js/11-margin/1-failing-margins/task.md new file mode 100644 index 00000000..e796ec03 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/1-failing-margins/task.md @@ -0,0 +1,43 @@ +# Нерабочие margin? + +[importance 3] + +В примере ниже находится блок `.block` фиксированной высоты, а в нём -- прямоугольник `.spacer`. + +При помощи `margin-left: 20%` и `margin-right: 20%`, прямоугольник центрирован в родителе по горизонтали. Это работает. + +Далее делается попытка при помощи свойств `height: 80%`, `margin-top: 10%` и `margin-bottom: 10%` расположить прямоугольник в центре по вертикали, чтобы сам элемент занимал `80%` высоты родителя, а сверху и снизу был одинаковый отступ. + +Однако, как видите, это не получается. Почему? Как поправить? + +```html + + + + +
                        +
                        +
                        +``` + diff --git a/3-more/11-css-for-js/11-margin/2-position-text-into-input/solution.md b/3-more/11-css-for-js/11-margin/2-position-text-into-input/solution.md new file mode 100644 index 00000000..27896351 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/2-position-text-into-input/solution.md @@ -0,0 +1,30 @@ +# Подсказка +Надвиньте элемент с текстом на `INPUT` при помощи отрицательного `margin`. + +# Решение + +Надвинем текст на `INPUT` при помощи отрицательного `margin-top`. Поднять следует на одну строку, т.е. на `1.25em`, можно для красоты чуть больше -- `1.3em`: + +Также нам понадобится обнулить "родной" `margin` у `INPUT`, чтобы не сбивал вычисления. + +```html + + + + +
                        Скажи пароль, друг
                        +``` + +[edit src="solution"]Полный код решения[/edit] \ No newline at end of file diff --git a/3-more/11-css-for-js/11-margin/2-position-text-into-input/solution.view/index.html b/3-more/11-css-for-js/11-margin/2-position-text-into-input/solution.view/index.html new file mode 100755 index 00000000..66a45b36 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/2-position-text-into-input/solution.view/index.html @@ -0,0 +1,27 @@ + + + + + + + + +
                        Добро пожаловать
                        + + +
                        Скажи пароль, друг
                        + +
                        .. и заходи
                        + + diff --git a/3-more/11-css-for-js/11-margin/2-position-text-into-input/source.view/index.html b/3-more/11-css-for-js/11-margin/2-position-text-into-input/source.view/index.html new file mode 100755 index 00000000..1690e4ef --- /dev/null +++ b/3-more/11-css-for-js/11-margin/2-position-text-into-input/source.view/index.html @@ -0,0 +1,17 @@ + + + + + + + +
                        Добро пожаловать
                        + + + +
                        Скажи пароль, друг
                        + +
                        .. и заходи
                        + + + diff --git a/3-more/11-css-for-js/11-margin/2-position-text-into-input/task.md b/3-more/11-css-for-js/11-margin/2-position-text-into-input/task.md new file mode 100644 index 00000000..f778dba8 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/2-position-text-into-input/task.md @@ -0,0 +1,15 @@ +# Расположить текст внутри INPUT + +[importance 5] + +Создайте `` с цветной подсказкой внутри (должен правильно выглядеть, не обязан работать): + +[iframe src="solution" height=90 border="1"] + +В дальнейшем мы сможем при помощи JavaScript сделать, чтобы текст при клике пропадал. Получится красивая подсказка. + +[edit src="source" task/] + +P.S. Обратите внимание: `type="password"`! То есть, просто `value` использовать нельзя, будут звёздочки. Кроме того, подсказка, которую вы реализуете, может быть как угодно стилизована. + +P.P.S. Вокруг `INPUT` с подсказкой не должно быть лишних отступов, блоки до и после продолжают идти в обычном потоке. diff --git a/3-more/11-css-for-js/11-margin/article.md b/3-more/11-css-for-js/11-margin/article.md new file mode 100644 index 00000000..2adf5f78 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/article.md @@ -0,0 +1,170 @@ +# Свойство "margin" + +Свойство `margin` задаёт отступы вокруг элемента. У него есть несколько особенностей, которые мы здесь рассмотрим. +[cut] +## Объединение отступов + +Вертикальные отступы поглощают друг друга, горизонтальные -- нет. + +Например, вот документ с вертикальными и горизонтальными отступами: + +```html + + +

                        + + Горизонтальный 20px + ← 40px → + 20px Отступ + +

                        +

                        Вертикальный 20px

                        + +``` + +Расстояние по горизонтали между элементами `SPAN` равно `40px`, так как горизонтальные отступы по `20px` сложились. + +А вот по вертикали расстояние от `SPAN` до `P` равно `20px`: из двух вертикальных отступов выбирается больший `max(20px, 15px) = 20px` и применяется. + +## Отрицательные margin-top/left + +Отрицательные значения `margin-top/margin-left` смещают элемент со своего обычного места. + +В CSS есть другой способ добиться похожего эффекта -- а именно, `position:relative`. Но между ними есть одно принципиальное различие. + +**При сдвиге через `margin` соседние элементы занимают освободившееся пространство, в отличие от `position: relative`, при котором элемент визуально сдвигается, но место, где он был, остается "занятым".** + +То есть, элемент продолжает полноценно участвовать в потоке. + +## Пример: вынос заголовка + +Например, есть документ с информационными блоками: + +```html + + + +
                        +

                        Общие положения

                        + +

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

                        +
                        + +
                        +

                        Общие обязанности водителей

                        + +

                        Водитель механического транспортного средства обязан иметь при себе и по требованию сотрудников милиции передавать им для проверки:

                        +
                          +
                        • водительское удостоверение на право управления транспортным средством соответствующей категории;
                        • +
                        • ...и так далее...
                        • +
                        +
                        +``` + +**Использование отрицательного `margin-top` позволяет вынести заголовки над блоком**. + +```css +/* вверх чуть больше, чем на высоту строки (1.25em) */ +h2 { + margin-top: -1.3em; +} +``` + +Результат: +[iframe src="h2-margin-top" height=330 border=1 link] + +А вот, что бы было при использовании `position`: + +```css +h2 { + position: relative; + top: -1.3em; +} +``` + +Результат: +[iframe src="h2-margin-top-position" height=330 border=1 link] + +**При использовании `position`, в отличие от `margin`, на месте заголовков, внутри блоков, осталось пустое пространство.** + +## Пример: вынос отчерка + +Организуем информацию чуть по-другому. Пусть после каждого заголовка будет отчерк: + +```html +
                        +

                        Заголовок

                        +
                        + +

                        Текст Текст Текст.

                        +
                        +``` + +Пример документа с такими отчерками: + +[iframe src="hr-margin-left-src" height=320 border=1 link] + +Для красоты мы хотим, чтобы отчерк `HR` начинался левее, чем основной текст. Отрицательный `margin-left` нам поможет: + +```css +hr.margin { margin-left: -2em; } + +/* для сравнения */ +hr.position { position: relative; left: -2em; } +``` + +Результат: +[iframe src="hr-margin-left" height=320 border=1 link] + +Обратите внимание на разницу между методами сдвига! +
                          +
                        • `hr.margin` сначала сдвинулся, а потом нарисовался до конца блока.
                        • +
                        • `hr.position` сначала нарисовался, а потом сдвинулся -- в результате справа осталось пустое пространство.
                        • +
                        + +Уже отсюда видно, что отрицательные `margin` -- исключительно полезное средство позиционирования! + +## Отрицательные margin-right/bottom + +Отрицательные `margin-right/bottom` ведут себя по-другому, чем `margin-left/top`. Они не сдвигают элемент, а "укорачивают" его. + +То есть, хотя сам размер блока не уменьшается, но следующий элемент будет думать, что он меньше на указанное в `margin-right/bottom` значение. + +Например, в примере ниже вторая строка налезает на первую: + +```html + +
                        + Первый +
                        + +
                        + Второй div думает, что высота первого на 0.5em меньше +
                        +``` + +Это используют, в частности для красивых вносок, с приданием иллюзии глубины. + +Например: + +[iframe src="negative-margin-bottom" border=1 height=200 link="Посмотреть в отдельном окне" edit] + +## Итого + +
                          +
                        • Отрицательные `margin-left/top` сдвигают элемент влево-вверх. Остальные элементы это учитывают, в отличие от сдвига через `position`.
                        • +
                        • Отрицательные `margin-right/bottom` заставляют другие элементы думать, что блок меньше по размеру справа-внизу, чем он на самом деле.
                        • +
                        • + +Отличная статья на тему отрицательных `margin`: [The Definitive Guide to Using Negative Margins](http://coding.smashingmagazine.com/2009/07/27/the-definitive-guide-to-using-negative-margins/) + diff --git a/3-more/11-css-for-js/11-margin/h2-margin-top-position.view/index.html b/3-more/11-css-for-js/11-margin/h2-margin-top-position.view/index.html new file mode 100755 index 00000000..83833dd9 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/h2-margin-top-position.view/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
                          +

                          Общие положения

                          + +

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

                          +
                          + +
                          +

                          Общие обязанности водителей

                          + +

                          Водитель механического транспортного средства обязан иметь при себе и по требованию сотрудников милиции передавать им для проверки:

                          +
                            +
                          • водительское удостоверение на право управления транспортным средством соответствующей категории;
                          • +
                          • ...и так далее...
                          • +
                          +
                          + + + + diff --git a/3-more/11-css-for-js/11-margin/h2-margin-top.view/index.html b/3-more/11-css-for-js/11-margin/h2-margin-top.view/index.html new file mode 100755 index 00000000..267a8bdf --- /dev/null +++ b/3-more/11-css-for-js/11-margin/h2-margin-top.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + +
                          +

                          Общие положения

                          + +

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

                          +
                          + +
                          +

                          Общие обязанности водителей

                          + +

                          Водитель механического транспортного средства обязан иметь при себе и по требованию сотрудников милиции передавать им для проверки:

                          +
                            +
                          • водительское удостоверение на право управления транспортным средством соответствующей категории;
                          • +
                          • ...и так далее...
                          • +
                          +
                          + + + + diff --git a/3-more/11-css-for-js/11-margin/hr-margin-left-src.view/index.html b/3-more/11-css-for-js/11-margin/hr-margin-left-src.view/index.html new file mode 100755 index 00000000..f7368e2d --- /dev/null +++ b/3-more/11-css-for-js/11-margin/hr-margin-left-src.view/index.html @@ -0,0 +1,38 @@ + + + + + + + + + +
                          + +

                          Общие положения

                          +
                          + +

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

                          +
                          + +
                          +

                          Общие обязанности водителей

                          +
                          + +

                          Водитель механического транспортного средства обязан иметь при себе и по требованию сотрудников милиции передавать им для проверки:

                          +
                            +
                          • водительское удостоверение на право управления транспортным средством соответствующей категории;
                          • +
                          • ...и так далее...
                          • +
                          +
                          + + + + diff --git a/3-more/11-css-for-js/11-margin/hr-margin-left.view/index.html b/3-more/11-css-for-js/11-margin/hr-margin-left.view/index.html new file mode 100755 index 00000000..0fa86c8d --- /dev/null +++ b/3-more/11-css-for-js/11-margin/hr-margin-left.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + +
                          + +

                          Общие положения (hr.margin)

                          +
                          + +

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

                          +
                          + +
                          +

                          Общие обязанности водителей (hr.position)

                          +
                          + +

                          Водитель механического транспортного средства обязан иметь при себе и по требованию сотрудников милиции передавать им для проверки:

                          +
                            +
                          • водительское удостоверение на право управления транспортным средством соответствующей категории;
                          • +
                          • ...и так далее...
                          • +
                          +
                          + + + + diff --git a/3-more/11-css-for-js/11-margin/negative-margin-bottom.view/index.html b/3-more/11-css-for-js/11-margin/negative-margin-bottom.view/index.html new file mode 100755 index 00000000..d6652970 --- /dev/null +++ b/3-more/11-css-for-js/11-margin/negative-margin-bottom.view/index.html @@ -0,0 +1,41 @@ + + + + + + + +
                          У DIV'а с холодильниками стоит margin-bottom: -1em
                          + +
                          + Наши холодильники - самые лучшие холодильники в мире! + Наши холодильники - самые лучшие холодильники в мире! + Наши холодильники - самые лучшие холодильники в мире! + Наши холодильники - самые лучшие холодильники в мире! +
                          + + + Так считают: 5 человек + + + + Оставьте свой отзыв! + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/article.md b/3-more/11-css-for-js/12-space-under-img/article.md new file mode 100644 index 00000000..837fb7c0 --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/article.md @@ -0,0 +1,119 @@ +# Лишнее место под IMG + +Иногда под `IMG` "вдруг" появляется лишнее место. Посмотрим на эти грабли внимательнее, почему такое бывает и как этого избежать. +[cut] +## Демонстрация проблемы +Например: + +```html + + + + + +
                          + +
                          +``` + +[iframe src="inline"] + +Посмотрите внимательно! Вы видите расстояние между рамками снизу? Это потому, что **браузер резервирует дополнительное место под инлайновым элементом, чтобы туда выносить "хвосты" букв**. + +Вот так выглядит та же картинка с выступающим вниз символом рядом: +[iframe src="inline-p"] + +В примере картинка обёрнута в красный `TD`. Таблица подстраивается под размер содержимого, так что проблема явно видна. Но она касается не только таблицы. Аналогичная ситуация возникнет, если вокруг `IMG` будет другой элемент с явно указанным размером, "облегающий" картинку по высоте. Браузер постарается зарезервировать место под `IMG`, хотя оно там не нужно. + +## Решение: сделать элемент блочным + +Самый лучший способ избежать этого -- поставить `display: block` таким картинкам: + +```html + + + + + +
                          + +
                          +``` + +[iframe src="block"] + +Под блочным элементом ничего не резервируется. Проблема исчезла. + +## Решение: задать vertical-align + +А что, если мы, по каким-то причинам, *не хотим* делать элемент блочным? + +Существует ещё один способ избежать проблемы -- использовать свойство [vertical-align](http://www.w3.org/TR/CSS2/visudet.html#propdef-vertical-align). + +**Если установить `vertical-align` в `top`, то инлайн-элемент будет отпозиционирован по верхней границе текущей строки.** + +При этом браузер не будет оставлять место под изображением, так как запланирует продолжение строки сверху, а не снизу: + +```html + + + + + +
                          + +
                          +``` + +[iframe src="valign"] + +А вот, как браузер отобразит соседние символы в этом случае: `pp` + +[iframe src="valign-p"] + +С другой стороны, сама строка никуда не делась, изображение по-прежнему является её частью, а браузер планирует разместить другое текстовое содержимое рядом, хоть и сверху. Поэтому если изображение маленькое, то произойдёт дополнение пустым местом до высоты строки: + +Например, для ``: + +[iframe src="valign-small"] + +Таким образом, требуется уменьшить еще и `line-height` контейнера. Окончательное решение для маленьких изображений с `vertical-align`: + +```html + + + + + +
                          + +
                          +``` + +Результат: + +[iframe src="valign-small-lh"] + +## Итого + +
                            +
                          • Пробелы под картинками появляются, чтобы оставить место под "хвосты" букв в строке. Строка "подразумевается", т.к. `display:inline`.
                          • +
                          • Можно избежать пробела, если изменить `display`, например, на `block`.
                          • +
                          • Альтернатива: `vertical-align:top` (или `bottom`), но для маленьких изображений может понадобиться уменьшить `line-height`, чтобы контейнер не оставлял место под строку.
                          • +
                          \ No newline at end of file diff --git a/3-more/11-css-for-js/12-space-under-img/block.view/index.html b/3-more/11-css-for-js/12-space-under-img/block.view/index.html new file mode 100755 index 00000000..36f2b1cb --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/block.view/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
                          + +
                          + + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/inline-p.view/index.html b/3-more/11-css-for-js/12-space-under-img/inline-p.view/index.html new file mode 100755 index 00000000..8b82f65a --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/inline-p.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
                          + p +
                          + + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/inline.view/index.html b/3-more/11-css-for-js/12-space-under-img/inline.view/index.html new file mode 100755 index 00000000..a788e0b5 --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/inline.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
                          + +
                          + + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/valign-p.view/index.html b/3-more/11-css-for-js/12-space-under-img/valign-p.view/index.html new file mode 100755 index 00000000..b0a96c06 --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/valign-p.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
                          + pp +
                          + + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/valign-small-lh.view/index.html b/3-more/11-css-for-js/12-space-under-img/valign-small-lh.view/index.html new file mode 100755 index 00000000..436c0a28 --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/valign-small-lh.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
                          + +
                          + + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/valign-small.view/index.html b/3-more/11-css-for-js/12-space-under-img/valign-small.view/index.html new file mode 100755 index 00000000..0c2dbadf --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/valign-small.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
                          + +
                          + + + + + diff --git a/3-more/11-css-for-js/12-space-under-img/valign.view/index.html b/3-more/11-css-for-js/12-space-under-img/valign.view/index.html new file mode 100755 index 00000000..ad82b4e0 --- /dev/null +++ b/3-more/11-css-for-js/12-space-under-img/valign.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
                          + +
                          + + + + + diff --git a/3-more/11-css-for-js/13-overflow/article.md b/3-more/11-css-for-js/13-overflow/article.md new file mode 100644 index 00000000..4a0cf229 --- /dev/null +++ b/3-more/11-css-for-js/13-overflow/article.md @@ -0,0 +1,162 @@ +# Свойство "overflow" + +Свойство `overflow` управляет тем, как ведёт себя содержимое блочного элемента, если его размер превышает допустимую длину/ширину. +[cut] +Обычно блок увеличивается в размерах при добавлении в него элементов, заключая в себе всех потомков. + +Но что, если высота/ширина указаны явно? Тогда блок не может увеличиться, и содержимое "переполняет" блок. Его отображение в этом случае задаётся свойством `overflow`. + +Возможные значения +
                            +
                          • `visible` (по умолчанию)
                          • +
                          • `hidden`
                          • +
                          • `scroll`
                          • +
                          • `auto`
                          • +
                          + +## visible + +Если не ставить `overflow` явно или поставить `visible`, то содержимое отображается за границами блока. + +Например: + +```html + + + +
                          + visible ЭтотТекстВылезаетСправаЭтотТекстВылезаетСправа + Этот текст вылезает снизу Этот текст вылезает снизу + Этот текст вылезает снизу Этот текст вылезает снизу +
                          +``` + +Как правило, такое переполнение указывает на ошибку в вёрстке. Если содержимое может быть больше контейнера -- используют другие значения. + +## hidden + +Переполняющее содержимое не отображается. + +```html + + + +
                          + hidden ЭтотТекстОбрезанСправаЭтотТекстОбрезанСправа + Этот текст будет обрезан снизу Этот текст будет обрезан снизу + Этот текст будет обрезан снизу Этот текст будет обрезан снизу +
                          +``` + +Вылезающее за границу содержимое становится недоступно. + +Это свойство иногда используют от лени, когда какой-то элемент дизайна немного вылезает за границу, и вместо исправления вёрстки его просто скрывают. Как правило, долго оно не живёт, вёрстку всё равно приходится исправлять. + +## auto + +При переполнении отображается полоса прокрутки. + +```html + + + +
                          + auto ЭтотТекстДастПрокруткуСправаЭтотТекстДастПрокруткуСправа + Этот текст даст прокрутку снизу Этот текст даст прокрутку снизу + Этот текст даст прокрутку снизу +
                          +``` + +## scroll + +Полоса прокрутки отображается всегда. + +```html + + + +
                          + scroll + Переполнения нет. +
                          +``` + +То же самое, что `auto`, но полоса прокрутки видна всегда, даже если переполнения нет. + +## overflow-x, overflow-y + +Можно указать поведение блока при переполнении по ширине в `overflow-x` и высоте -- в `overflow-y`: + +```html + + + +
                          + ПоШиринеПолосаПрокруткиAutoПоШиринеПолосаПрокруткиAuto + Этот текст вылезает снизу Этот текст вылезает снизу + Этот текст вылезает снизу Этот текст вылезает снизу +
                          +``` + +## Итого + +Свойства `overflow-x/overflow-y` (или оба одновременно: `overflow`) задают поведение контейнера при переполнении: + +
                          +
                          `visible`
                          +
                          По умолчанию, содержимое вылезает за границы блока.
                          +
                          `hidden`
                          +
                          Переполняющее содержимое невидимо.
                          +
                          `auto`
                          +
                          Полоса прокрутки при переполнении.
                          +
                          `scroll`
                          +
                          Полоса прокрутки всегда.
                          +
                          + +Кроме того, значение `overflow: auto | hidden` изменяет поведение контейнера, содержащего `float'ы`. Так как элемент с `float` находится вне потока, то обычно контейнер не выделяет под него место. Но если стоит такой `overflow`, то место выделяется, т.е. контейнер растягивается. Более подробно этот вопрос рассмотрен в статье [](/float). \ No newline at end of file diff --git a/3-more/11-css-for-js/14-height-percent/article.md b/3-more/11-css-for-js/14-height-percent/article.md new file mode 100644 index 00000000..f14e71b1 --- /dev/null +++ b/3-more/11-css-for-js/14-height-percent/article.md @@ -0,0 +1,140 @@ +# Особенности свойства "height" в % + +Обычно свойство `height`, указанное в процентах, означает высоту относительно внешнего блока. + +Однако, всё не так просто. Интересно, что для произвольного блочного элемента `height` в процентах работать не будет! + +[cut] + +Чтобы лучше понимать ситуацию, рассмотрим пример. + +## Пример + +Наша цель -- получить вёрстку такого вида: + +[iframe src="height-percent" height=160 link edit] + +**При этом блок с левой стрелкой должен быть отдельным элементом внутри контейнера.** + +Это удобно для интеграции с JavaScript, чтобы отлавливать на нём клики мыши. + +То есть, HTML-код требуется такой: + +```html +
                          +
                          + +
                          +
                          + ...Текст... +
                          +
                          +``` + +Как это реализовать? Подумайте перед тем, как читать дальше... + +Придумали?... Если да -- продолжаем. + +Есть разные варианты, но, возможно, вы решили сдвинуть `.toggler` влево, при помощи `float:left` (тем более что он фиксированной ширины) и увеличить до `height: 100%`, чтобы он занял всё пространство по вертикали. + +**Вы ещё не видите подвох? Смотрим внимательно, что будет происходить с `height: 100%`...** + +## Демо height:100% + float:left + +CSS: + +```css +.container { + border: 1px solid black; +} + +.content { + *!*/* margin-left нужен, так как слева от содержимого будет стрелка */*/!* + margin-left: 35px; +} + +.toggler { + *!*/* Зададим размеры блока со стрелкой */*/!* + height: 100%; + width: 30px; + float: left; + + background: #EEE url("arrow_left.png") center center no-repeat; + border-right: #AAA 1px solid; + cursor: pointer; +} +``` + +А теперь -- посмотрим этот вариант в действии: + +[iframe src="height-percent-float" height=160 link edit] + +Как видно, блок со стрелкой вообще исчез! Куда же он подевался? + +Ответ нам даст спецификация CSS 2.1 [пункт 10.5](http://www.w3.org/TR/CSS2/visudet.html#propdef-height). + +**"Если высота внешнего блока вычисляется по содержимому, то высота в % не работает, и заменяется на `height:auto`. Кроме случая, когда у элемента стоит `position:absolute`."** + +В нашем случае высота `.container` как раз определяется по содержимому, поэтому для `.toggler` проценты не действуют, а размер вычисляется как при `height:auto`. + +Какая же она -- эта автоматическая высота? Вспоминаем, что обычно размеры `float` определяются по содержимому ([10.3.5](http://www.w3.org/TR/CSS2/visudet.html#float-width)). А содержимого-то в `.toggler` нет, так что высота нулевая. Поэтому этот блок и не виден. + +Если бы мы точно знали высоту внешнего блока и добавили её в CSS -- это решило бы проблему. + +Например: + +```css +.container { + height: 200px; /* теперь height в % внутри будет работать */ +} +``` + +Результат: + +[iframe src="height-percent-float-exact" height="230" link edit] + +...Но в данном случае этот путь неприемлем! Ведь мы не знаем, сколько будет текста и какой будет итоговая высота. + +Поэтому решим задачу по-другому. + +## Правильно: height:100% + position:absolute + +Проценты будут работать, если поставить `.toggler` свойство `position: absolute` и спозиционировать его в левом-верхнем углу `.container` (у которого сделать `position:relative`): + +```css +.container { + *!*position: relative;*/!* + border: 1px solid black; +} + +.content { + margin-left: 35px; +} + +.toggler { +*!* + position: absolute; + left: 0; + top: 0; +*/!* + + height: 100%; + width: 30px; + cursor: pointer; + + border-right: #AAA 1px solid; + background: #EEE url("arrow_left.png") center center no-repeat; +} +``` + +Результат: + +[iframe src="height-percent" height=160 link edit] + +## Итого + +
                            +
                          • Свойство `height`, указанное в %, работает только если для внешнего блока указана высота.
                          • +
                          • Стандарт CSS 2.1 предоставляет обход этой проблемы, отдельно указывая, что проценты работают при `position:absolute`. На практике это часто выручает. +
                          • +
                          \ No newline at end of file diff --git a/3-more/11-css-for-js/14-height-percent/height-percent-float-exact.view/index.html b/3-more/11-css-for-js/14-height-percent/height-percent-float-exact.view/index.html new file mode 100755 index 00000000..f25aecb4 --- /dev/null +++ b/3-more/11-css-for-js/14-height-percent/height-percent-float-exact.view/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
                          +
                          +
                          +

                          Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                          + +

                          В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                          +
                          +
                          + + + + diff --git a/3-more/11-css-for-js/14-height-percent/height-percent-float.view/index.html b/3-more/11-css-for-js/14-height-percent/height-percent-float.view/index.html new file mode 100755 index 00000000..b3e0e6d1 --- /dev/null +++ b/3-more/11-css-for-js/14-height-percent/height-percent-float.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + +
                          +
                          +
                          +

                          Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                          + +

                          В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                          +
                          +
                          + + + + diff --git a/3-more/11-css-for-js/14-height-percent/height-percent.view/index.html b/3-more/11-css-for-js/14-height-percent/height-percent.view/index.html new file mode 100755 index 00000000..869bedf8 --- /dev/null +++ b/3-more/11-css-for-js/14-height-percent/height-percent.view/index.html @@ -0,0 +1,42 @@ + + + + + + + + + +
                          +
                          +
                          +

                          Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                          + +

                          В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                          +
                          +
                          + + + diff --git a/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/index.html b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/index.html new file mode 100755 index 00000000..b46adbd7 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/index.html @@ -0,0 +1,76 @@ + + + + + + + + + + + +

                          Сообщения:

                          +
                            +
                          • Сообщение 1
                          • +
                          • Сообщение 2
                          • +
                          • Сообщение 3
                          • +
                          • Сообщение 4
                          • +
                          • Сообщение 5
                          • +
                          • ...
                          • +
                          + + +Ссылка на архив +..И на PDF + + + + + diff --git a/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/solution.md b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/solution.md new file mode 100644 index 00000000..e77a01b9 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/solution.md @@ -0,0 +1,6 @@ + + +```html + +``` + diff --git a/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/source.view/index.html b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/source.view/index.html new file mode 100755 index 00000000..2d00407a --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/source.view/index.html @@ -0,0 +1,84 @@ + + + + + + + + + +

                          Сообщения:

                          +
                            +
                          • Сообщение 1
                          • +
                          • Сообщение 2
                          • +
                          • Сообщение 3
                          • +
                          • Сообщение 4
                          • +
                          • Сообщение 5
                          • +
                          • ...
                          • +
                          + + +Ссылка на архив +..И на PDF + + + + + diff --git a/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/task.md b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/task.md new file mode 100644 index 00000000..46322989 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/1-select-elements-selector/task.md @@ -0,0 +1,42 @@ +# Выберите элементы селектором + +[importance 5] + +HTML-документ: + +```html + + + + +

                          Сообщения:

                          +
                            +
                          • Сообщение 1
                          • +
                          • Сообщение 2
                          • +
                          • Сообщение 3
                          • +
                          • Сообщение 4
                          • +
                          • Сообщение 5
                          • +
                          • ...
                          • +
                          + + +Ссылка на архив +..И на PDF +``` + +Задания: +
                            +
                          1. Выбрать `input` типа `checkbox`.
                          2. +
                          3. Выбрать `input` типа `checkbox`, НЕ отмеченный.
                          4. +
                          5. Найти все элементы с `id=message` или `message-*`.
                          6. +
                          7. Найти все элементы с `id=message-*`.
                          8. +
                          9. Найти все ссылки с расширением `href="...zip"`.
                          10. +
                          11. Найти все элементы с атрибутом `data-action`, содержащим `delete` в списке (через пробел).
                          12. +
                          13. Найти все элементы, у которых ЕСТЬ атрибут `data-action`, но он НЕ содержит `delete` в списке (через пробел).
                          14. +
                          15. Выбрать все чётные элементы списка `#messages`.
                          16. +
                          17. Выбрать один элемент сразу за заголовком `h3#widget-title` на том же уровне вложенности.
                          18. +
                          19. Выбрать все ссылки, следующие за заголовком `h3#widget-title` на том же уровне вложенности.
                          20. +
                          21. Выбрать ссылку внутри последнего элемента списка `#messages`.
                          22. +
                          + +[edit src="source"]Исходный документ с вспомогательной функцией `test` для проверки[/edit] \ No newline at end of file diff --git a/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/solution.md b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/solution.md new file mode 100644 index 00000000..06e97d0a --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/solution.md @@ -0,0 +1,30 @@ +# Выбор элементов + +Для выбора элементов, начиная с первого, можно использовать селектор [nth-child](http://css-tricks.ru/Articles/Details/HowNthChildWorks). + +Его вид: `li:nth-child(n+2)`, т.к. `n` идёт от нуля, соответственно первым будет второй элемент (`n=0`), что нам и нужно. + +# Решение + +Отступ, размером в одну строку, при `line-height: 1.5` -- это `1.5em`. + +Правило: + +```css +li:nth-child(n+2) { + margin-top: 1.5em; +} +``` + +Полный код [edit src="solution"]в песочнице[/edit]. + +# Ещё решение + +Ещё один вариант селектора: `li + li` + +```css +li + li { + margin-top: 1.5em; +} +``` + diff --git a/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/solution.view/index.html b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/solution.view/index.html new file mode 100755 index 00000000..e7e8feea --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/solution.view/index.html @@ -0,0 +1,32 @@ + + + + + + + + +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. + + + diff --git a/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/source.view/index.html b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/source.view/index.html new file mode 100755 index 00000000..eced2bc6 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/source.view/index.html @@ -0,0 +1,28 @@ + + + + + + + + +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. + + + diff --git a/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/task.md b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/task.md new file mode 100644 index 00000000..9e0e654a --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/2-margin-between-pairs-size-1/task.md @@ -0,0 +1,38 @@ +# Отступ между элементами, размер одна строка + +[importance 4] + +Есть список `UL/LI`. + +```html +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. +``` + +Размеры шрифта и строки заданы стилем: + +```css +body { + font: 14px/1.5 Georgia, serif; +} +``` + +**Сделайте, чтобы между элементами был вертикальный отступ.** +
                            +
                          • Размер отступа: ровно 1 строка.
                          • +
                          • Нужно добавить только одно правило CSS с одним псевдоселектором, можно использовать CSS3.
                          • +
                          • Не должно быть лишних отступов сверху и снизу списка.
                          • +
                          + +Результат: +[iframe src="solution" border=1 link] + +[edit src="source" task/] \ No newline at end of file diff --git a/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/index.html b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/index.html new file mode 100755 index 00000000..2839e0dd --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/index.html @@ -0,0 +1,32 @@ + + + + + + + + +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. + + + diff --git a/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/solution.md b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/solution.md new file mode 100644 index 00000000..1d4b8551 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/solution.md @@ -0,0 +1,26 @@ +# Селектор + +Для отступа между парами, то есть перед каждым нечётным элементом, можно использовать селектор [nth-child](http://css-tricks.ru/Articles/Details/HowNthChildWorks). + +Селектор будет `li:nth-child(odd)`, к нему нужно ещё добавить отсечение первого элемента: `li:nth-child(odd):not(:first-child)`. + +Можно поступить и по-другому: `li:nth-child(2n+3)` выберет все элементы для `n=0,1,2...`, то есть 3й, 5й и далее, те же, что и предыдущий селектор. Немного менее очевидно, зато короче. + +# Правило + +Отступ, размером в одну строку, при `line-height: 1.5` -- это `1.5em`. + +Поставим отступ перед каждым *нечётным* элементом, кроме первого: + +```css +li:nth-child(odd):not(:first-child) { + margin-top: 1.5em; +} +``` + +Получится так: + +```html + +``` + diff --git a/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/solution.view/index.html b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/solution.view/index.html new file mode 100755 index 00000000..2839e0dd --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/solution.view/index.html @@ -0,0 +1,32 @@ + + + + + + + + +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. + + + diff --git a/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/source.view/index.html b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/source.view/index.html new file mode 100755 index 00000000..eced2bc6 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/source.view/index.html @@ -0,0 +1,28 @@ + + + + + + + + +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. + + + diff --git a/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/task.md b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/task.md new file mode 100644 index 00000000..d5b0e778 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/3-margin-between-pairs/task.md @@ -0,0 +1,38 @@ +# Отступ между парами, размером со строку + +[importance 4] + +Есть список `UL/LI`. + +```html +Текст вверху без отступа от списка. +
                            +
                          • Маша
                          • +
                          • Паша
                          • +
                          • Даша
                          • +
                          • Женя
                          • +
                          • Саша
                          • +
                          • Гоша
                          • +
                          +Текст внизу без отступа от списка. +``` + +Размеры шрифта и строки заданы стилем: + +```css +body { + font: 14px/1.5 Georgia, serif; +} +``` + +**Сделайте, чтобы между каждой парой элементов был вертикальный отступ.** +
                            +
                          • Размер отступа: ровно 1 строка.
                          • +
                          • Нужно добавить только одно правило CSS, можно использовать CSS3.
                          • +
                          • Не должно быть лишних отступов сверху и снизу списка.
                          • +
                          + +Результат: +[iframe src="solution" border=1 link] + +[edit src="source" task/] \ No newline at end of file diff --git a/3-more/11-css-for-js/15-css-selectors/article.md b/3-more/11-css-for-js/15-css-selectors/article.md new file mode 100644 index 00000000..9a023542 --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/article.md @@ -0,0 +1,255 @@ +# Знаете ли вы селекторы? + +CSS3-селекторы -- фундаментально полезная вещь. + +Даже если вы почему-то (старый IE?) не пользуетесь ими в CSS, есть много фреймворков для их кросс-браузерного использования CSS3 из JavaScript. + +Поэтому эти селекторы необходимо знать. +[cut] + +## Основные виды селекторов + +Основных видов селекторов всего несколько: + +
                            +
                          • `*` -- любые элементы.
                          • +
                          • `div` -- элементы с таким тегом.
                          • +
                          • `#id` -- элемент с данным `id`.
                          • +
                          • `.class` -- элементы с таким классом.
                          • +
                          • `[name="value"]` -- селекторы на атрибут (см. далее).
                          • +
                          • `:visited` -- "псевдоклассы", остальные разные условия на элемент (см. далее).
                          • +
                          + +**Селекторы можно комбинировать, записывая последовательно, без пробела:** + +
                            +
                          • `.c1.c2` -- элементы одновременно с двумя классами `c1` и `c2`
                          • +
                          • `a#id.c1.c2:visited` -- элемент `a` с данным `id`, классами `c1` и `c2`, и псевдоклассом `visited`
                          • +
                          + +## Отношения + +В CSS3 предусмотрено четыре вида отношений между элементами. + +Самые известные вы наверняка знаете: +
                            +
                          • `div p` -- элементы `p`, являющиеся потомками `div`.
                          • +
                          • `div > p` -- только непосредственные потомки
                          • +
                          + +Есть и два более редких: +
                            +
                          • `div ~ p` -- правые соседи: все `p` на том же уровне вложенности, которые идут после `div`.
                          • +
                          • `div + p` -- первый правый сосед: `p` на том же уровне вложенности, который идёт сразу после `div` (если есть).
                          • +
                          + +Посмотрим их на примере HTML: + +```html +

                          Балтославянские языки

                          + +
                            + ...Вложенный OL/LI список языков... +
                          +``` + +CSS-селекторы: + +```css +##languages li { + color: brown; /* потомки #languages, подходящие под селектор LI */ +} + +##languages > li { + color: black; /* первый уровень детей #languages подходящих под LI */ +} + +##e-slavic { font-style: italic; } + +*!* +##e-slavic ~ li { /* правые соседи #e-slavic с селектором LI */ + color: red; +} +*/!* + +##latvian { + font-style: italic; +} + +##latvian * { /* потомки #latvian, подходяще под * (т.е. любые) */ + font-style: normal; +} + +*!* +##latvian + li { /* первый правый сосед #latvian с селектором LI */ + color: green; +} +*/!* +``` + +Результат: +[iframe src="relation" border="1" edit link] + + +## Фильтр по месту среди соседей + +При выборе элемента можно указать его место среди соседей. + +Список псевдоклассов для этого: + +
                            +
                          • `:first-child` -- первый потомок своего родителя.
                          • +
                          • `:last-child` -- последний потомок своего родителя.
                          • +
                          • `:only-child` -- единственный потомок своего родителя, соседних элементов нет.
                          • +
                          • `:nth-child(a)` -- потомок номер `a` своего родителя, например `:nth-child(2)` -- второй потомок. Нумерация начинается с `1`.
                          • +
                          • `:nth-child(an+b)` -- расширение предыдущего селектора через указание номера потомка формулой, где `a,b` -- константы, а под `n` подразумевается любое целое число. + +Этот псевдокласс будет фильтровать все элементы, которые попадают под формулу при каком-либо `n`. Например: +
                              +
                            • `:nth-child(2n)` даст элементы номер `2`, `4`, `6`..., то есть чётные.
                            • +
                            • `:nth-child(2n+1)` даст элементы номер `1`, `3`..., то есть нечётные.
                            • +
                            • `:nth-child(3n+2)` даст элементы номер `2`, `5`, `8` и так далее.
                            • +
                            +
                          • +
                          + +Пример использования для выделения в списке: +[iframe src="nthchild" border="1" edit link] + +```css +/*+ hide="CSS к примеру выше" */ +li:nth-child(2n) { /* чётные */ + background: #eee; +} + +li:nth-child(3) { /* 3-ий потомок */ + color: red; +} +``` + +
                            +
                          • `:nth-last-child(a)`, `:nth-last-child(an+b)` -- то же самое, но отсчёт начинается с конца, например `:nth-last-child(2)` -- второй элемент с конца.
                          • +
                          + +## Фильтр по месту среди соседей с тем же тегом + +Есть аналогичные псевдоклассы, которые учитывают не всех соседей, а только с тем же тегом: + +
                            +
                          • `:first-of-type`
                          • +
                          • `:last-of-type`
                          • +
                          • `:only-of-type`
                          • +
                          • `:nth-of-type`
                          • +
                          • `:nth-last-of-type`
                          • +
                          + +Они имеют в точности тот же смысл, что и обычные `:first-child`, `:last-child` и так далее, но во время подсчёта игнорируют элементы с другими тегами, чем тот, к которому применяется фильтр. + +Пример использования для раскраски списка `DT` "через один" и предпоследнего `DD`: + +[iframe src="nthchild-type" border="1" edit link] + +```css +/*+ hide="CSS к примеру выше" */ +dt:nth-of-type(2n) { + /* чётные dt (соседи с другими тегами игнорируются) */ + background: #eee; +} + +dd:nth-last-of-type(2) { + /* второй dd снизу */ + color: red; +} +``` + +Как видим, селектор `dt:nth-of-type(2n)` выбрал каждый второй элемент `dt`, причём другие элементы (`dd`) в подсчётах не участвовали. + +## Селекторы атрибутов + +
                          +
                          На атрибут целиком
                          +
                          +
                            +
                          • `[attr]` -- атрибут установлен,
                          • +
                          • `[attr="val"]` -- атрибут равен `val`.
                          • +
                          +
                          +
                          На начало атрибута
                          +
                          +
                            +
                          • `[attr^="val"]` -- атрибут начинается с `val`, например `"value"`.
                          • +
                          • `[attr|="val"]` -- атрибут равен `val` *или* начинается с `val-`, например равен `"val-1"`.
                          • +
                          +
                          +
                          На содержание +
                          +
                            +
                          • `[attr*="val"]` -- атрибут содержит подстроку `val`, например равен `"myvalue"`.
                          • +
                          • `[attr~="val"]` -- атрибут содержит `val` как одно из значений через пробел. +Например: `[attr~="delete"]` верно для `"edit delete"` и неверно для `"undelete"` или `"no-delete"`.
                          • +
                          +
                          +
                          На конец атрибута
                          +
                          +
                            +
                          • `[attr$="val"]` -- атрибут заканчивается на `val`, например равен `"myval"`.
                          • +
                          +
                          +
                          + +## Другие псевдоклассы + +
                            +
                          • `:not(селектор)` -- все, кроме подходящих под селектор.
                          • +
                          • `:focus` -- в фокусе.
                          • +
                          • `:hover` -- под мышью.
                          • +
                          • `:empty` -- без детей (даже без текстовых).
                          • +
                          • `:checked`, `:disabled`, `:enabled` -- состояния `INPUT`.
                          • +
                          • `:target` -- этот фильтр сработает для элемента, `ID` которого совпадает с анкором `#...` текущего URL. + +Например, если на странице есть элемент с `id="intro"`, то правило `:target { color: red }` подсветит его в том случае, если текущий URL имеет вид `http://...#intro`. +
                          • +
                          + +## Псевдоэлементы ::before, ::after + +"Псевдоэлементы" -- различные вспомогательные элементы, которые браузер записывает или может записать в документ. + +При помощи *псевдоэлементов* `::before` и `::after` можно добавлять содержимое в начало и конец элемента: + +```html + + + +Обратите внимание: содержимое добавляется внутрь LI. + +
                            +
                          • Первый элемент
                          • +
                          • Второй элемент
                          • +
                          +``` + +Псевдоэлементы `::before`/`::after` добавили содержимое в начало и конец каждого `LI`. + +[smart header="`:before` или `::before`?"] +Когда-то все браузеры реализовали эти псевдоэлементы с одним двоеточием: `:after/:before`. + +Стандарт с тех пор изменился и сейчас все, кроме IE8, понимают также современную запись с двумя двоеточиями. А для IE8 нужно по-прежнему одно. + +Поэтому если вам важна поддержка IE8, то имеет смысл использовать одно двоеточие. + +Версии IE7- не понимают этих селекторов. +[/smart] + +## Практика + +Вы можете использовать информацию выше как справочную для решения задач ниже, которые уже реально покажут, владеете вы CSS-селекторами или нет. + diff --git a/3-more/11-css-for-js/15-css-selectors/nthchild-type.view/index.html b/3-more/11-css-for-js/15-css-selectors/nthchild-type.view/index.html new file mode 100755 index 00000000..2aec672a --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/nthchild-type.view/index.html @@ -0,0 +1,38 @@ + + + + + + + +
                          +
                          Первый dt
                          +
                          Описание dd
                          +
                          Второй dt dt:nth-of-type(2n)
                          +
                          Описание dd
                          +
                          Третий dt
                          +
                          Описание dd dd:nth-last-of-type(2)
                          +
                          Четвёртый dt dt:nth-of-type(2n)
                          +
                          Описание dd
                          +
                          + + + + diff --git a/3-more/11-css-for-js/15-css-selectors/nthchild.view/index.html b/3-more/11-css-for-js/15-css-selectors/nthchild.view/index.html new file mode 100755 index 00000000..ca5d343c --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/nthchild.view/index.html @@ -0,0 +1,32 @@ + + + + + + + +
                            +
                          • Древнерусский язык
                          • +
                          • Древненовгородский диалект li:nth-child(2n)
                          • +
                          • Западнорусский письменный язык li:nth-child(3)
                          • +
                          • Украинский язык li:nth-child(2n)
                          • +
                          • Белорусский язык
                          • +
                          • Другие языки li:nth-child(2n)
                          • +
                          + + + diff --git a/3-more/11-css-for-js/15-css-selectors/relation.view/index.html b/3-more/11-css-for-js/15-css-selectors/relation.view/index.html new file mode 100755 index 00000000..6f97aedc --- /dev/null +++ b/3-more/11-css-for-js/15-css-selectors/relation.view/index.html @@ -0,0 +1,59 @@ + + + + + + + +

                          Балтославянские языки

                          + +
                            +
                          1. Славянские языки +
                              +
                            1. Славянские микроязыки
                            2. +
                            3. Праславянский язык
                            4. +
                            5. Восточнославянские языки #e-slavic
                            6. +
                            7. Западнославянские языки #e-slavic ~ li
                            8. +
                            9. Южнославянские языки #e-slavic ~ li
                            10. +
                            11. ... #e-slavic ~ li
                            12. +
                            +
                          2. +
                          3. Балтийские языки +
                              +
                            1. Литовский язык
                            2. +
                            3. Латышский язык #latvian +
                              1. Латгальский язык #latvian *
                              +
                            4. +
                            5. Прусский язык #latvian + li
                            6. +
                            7. ... (следующий элемент уже не #latvian + li)
                            8. +
                            +
                          4. +
                          + + + diff --git a/3-more/11-css-for-js/16-css-no-ie6/article.md b/3-more/11-css-for-js/16-css-no-ie6/article.md new file mode 100644 index 00000000..22174be7 --- /dev/null +++ b/3-more/11-css-for-js/16-css-no-ie6/article.md @@ -0,0 +1,46 @@ +# CSS без IE6(7) + +CSS-возможности, которыми мы можем пользоваться, если НЕ поддерживаем IE6. +[cut] +
                          +
                          Селекторы атрибутов:
                          +
                          +
                            +
                          • `[attr]` -- атрибут установлен,
                          • +
                          • `[attr="val"]` -- атрибут равен `val`,
                          • +
                          • `[attr^="val"]` -- атрибут начинается с `val`, например `"value"`.
                          • +
                          • `[attr*="val"]` -- атрибут содержит `val`, например равен `"myvalue"`.
                          • +
                          • `[attr$="val"]` -- атрибут заканчивается на `val`, например равен `"myval"`.
                          • +
                          • `[attr~="val"]` -- атрибут содержит `val` как одно из значений через пробел, например: `[data-actions~="edit"]` верно для значения `data-actions="edit delete"`.
                          • +
                          • `[attr|="val"]` -- атрибут равен `val` *или* начинается с `val-`, например равен `"val-1"`.
                          • +
                          + +
                          +
                          Селекторы элементов:
                          +
                          + + +
                            +
                          • `ul > li` -- непосредственный потомок,
                          • +
                          • `.prev + .me` -- выбирает `.me`, которые стоят сразу после `.prev`, т.е. "правый брат".
                          • +
                          • `.prev ~ .me` -- выбирает `.me`, которые стоят после `.prev`, но не обязательно сразу после, между ними могут быть другие элементы,
                          • +
                          • `.a.b` -- несколько классов одновременно,
                          • +
                          • `:hover` -- курсор над элементом (в IE6 работает только с `A`), +
                          • `:first-child` -- первый потомок в своём родителе.
                          • +
                          + +Внимание, IE7 не пересчитывает стили при изменении окружающих элементов для селекторов `.prev + .me`, `.prev` и `:first-child`. Иными словами, не обновляет стиль при добавлении/удалении соседей через JavaScript. +
                          +
                          Свойства:
                          +
                          +
                            +
                          • `min-width/min-height` -- минимальная ширина/высота
                          • +
                          • `max-width/max-height` -- максимальная ширина/высота
                          • +
                          • `position: fixed`
                          • +
                          +
                          +
                          + +Здесь перечислены в основном возможности. Разумеется, была поправлена и масса багов. + +При отказе от поддержки IE7, и, тем более, IE8, список ещё шире и включает в себя почти весь CSS 2.1. diff --git a/3-more/11-css-for-js/17-css-sprite/article.gif b/3-more/11-css-for-js/17-css-sprite/article.gif new file mode 100755 index 00000000..8f44ebf9 Binary files /dev/null and b/3-more/11-css-for-js/17-css-sprite/article.gif differ diff --git a/3-more/11-css-for-js/17-css-sprite/article.md b/3-more/11-css-for-js/17-css-sprite/article.md new file mode 100644 index 00000000..ddb7bf1c --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/article.md @@ -0,0 +1,203 @@ +# CSS-спрайты + +CSS-спрайт -- способ объединить много изображений в одно, чтобы: +
                            +
                          1. Cократить количество обращений к серверу.
                          2. +
                          3. Загрузить несколько изображений сразу, включая те, которые понадобятся в будущем.
                          4. +
                          5. Если у изображений сходная палитра, то объединённое изображение будет меньше по размеру, чем совокупность исходных картинок.
                          6. +
                          +[cut] +Рассмотрим, как это работает, на примере дерева: + +```html +
                            +
                          • +
                            +
                            Раздел 1
                            В две строки
                            +
                              +
                            • +
                              +
                              Раздел 1.1 в одну строку
                              +
                            • +
                            • +
                              +
                              Страница 1.2
                              в две строки
                              +
                            • +
                            +
                          • +
                          • +
                            +
                            Раздел 2
                            В две строки
                            +
                          • +
                          +``` + +[iframe src="sprite-tree-src" border=1 height=200 link edit] + +Сейчас "плюс", "минус" и "статья" -- три отдельных изображения. Объединим их в спрайт. + + + +## Шаг 1. Использовать background + +Первый шаг к объединению изображений в "спрайт" -- показывать их через `background`., а не через тег `IMG`. + +В данном случае он уже сделан. Стиль для дерева: + +```css +.icon { + width: 16px; + height: 16px; + float: left; +} + +.open .icon { + cursor: pointer; + background: url(minus.gif); +} + +.closed .icon { + cursor: pointer; + background: url(plus.gif); +} + +.leaf .icon { + cursor: text; + background: url(article.gif); +} +``` + +## Шаг 2. Объединить изображения + +Составим из нескольких изображений одно `icons.gif`, расположив их, например, по вертикали. + +Из , и получится одна картинка: + +## Шаг 3. Показать часть спрайта в "окошке" + +А теперь самое забавное. Размер `DIV'а` для иконки -- жёстко фиксирован: + +```css +.icon { +*!* + width: 16px; + height: 16px; +*/!* + float: left; +} +``` + +Это значит, что если поставить `background'ом` объединённую картинку, то вся она не поместится, будет видна только верхняя часть: + +[iframe src="sprite-tree-1" height=60 border=1] + +Если бы высота иконки была больше, например, `16x48`, как в примере ниже, то было бы видно и остальное: + +[iframe src="height48" height=80 border=1] +..Но так как там всего `16px`, то помещается только одно изображение. + +## Шаг 4. Сдвинуть спрайт + +Сдвиг фона `background-position` позволяет выбирать, какую именно часть спрайта видно. + +В спрайте `icons.gif` изображения объединены так, что сдвиг на `16px` покажет следующую иконку: + +```css +.icon { + width: 16px; + height: 16px; + float: left; + background: url(icons.gif) no-repeat; +} + +.open .icon { + background-position: 0 -16px; /* вверх на 16px */ + cursor: pointer; +} + +.closed .icon { + background-position: 0 0px; /* по умолчанию */ + cursor: pointer; +} + +.leaf .icon { + background-position: 0 -32px; /* вверх на 32px */ + cursor: text; +} +``` + +Результат: + +[iframe src="sprite-tree" border=1 height=200 link edit] + +
                            +
                          • В спрайт могут объединяться изображения разных размеров, т.е. сдвиг может быть любым.
                          • +
                          • Сдвигать можно и по горизонтали и по вертикали.
                          • +
                          + +### Отступы + +Обычно отступы делаются `margin/padding`, но иногда их бывает удобно предусмотреть в спрайте. + +Тогда если элемент немного больше, чем размер изображения, то в "окошке" не появится лишнего. + +Пример спрайта с отступами: + + + +Иконка RSS находится в нём на координатах `(90px, 40px)`: + + + +Это значит, что чтобы показать эту иконку, нужно сместить фон: + +```css +background-position: -90px -40px; +``` + +При этом в левом-верхнем углу фона как раз и будет эта иконка: + +[iframe src="sprite-example" border=1] + +Элемент, в котором находится иконка (в рамке), больше по размеру, чем картинка. + +Его стиль: + +```css +.rss { + width: 35px; /* ширина/высота больше чем размер иконки */ + height: 35px; + border: 1px solid black; + float: left; + background-image: url(sprite.png); + background-position: -90px -40px; +} +``` + +Если бы в спрайте не было отступов, то в такое большое "окошко" наверняка влезли бы другие иконки. + + +## Итого + +[smart header="Когда использовать для изображений `IMG`, а когда -- `CSS background`?"] + +Решение лучше всего принимать, исходя из принципов семантической вёрстки. + +Задайте вопрос -- что здесь делает изображение? Является ли оно самостоятельным элементом страницы (фотография, аватар посетителя), или же оформляет что-либо (иконка узла дерева)? + +Элемент `IMG` следует использовать в первом случае, а для оформления у нас есть CSS. +[/smart] + + +Спрайты позволяют: +
                            +
                          1. Cократить количество обращений к серверу.
                          2. +
                          3. Загрузить несколько изображений сразу, включая те, которые понадобятся в будущем.
                          4. +
                          5. Если у изображений сходная палитра, то объединённое изображение будет меньше по размеру, чем совокупность исходных картинок.
                          6. +
                          + +Если фоновое изображение нужно повторять по горизонтали или вертикали, то спрайты тоже подойдут -- изображения в них нужно располагать в этом случае так, чтобы при повторении не были видны соседи, т.е., соответственно, вертикально или горизонтально, но не "решёткой". + +Далее мы встретимся со спрайтами при создании интерфейсов, чтобы кнопка при наведении меняла своё изображение. Один спрайт будет содержать все состояния кнопки, а переключение внешнего вида -- осуществляться при помощи сдвига `background-position`. + +Для автоматизированной сборки спрайтов используются специальные инструменты, например SmartSprites. \ No newline at end of file diff --git a/3-more/11-css-for-js/17-css-sprite/height48.view/index.html b/3-more/11-css-for-js/17-css-sprite/height48.view/index.html new file mode 100755 index 00000000..0425249f --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/height48.view/index.html @@ -0,0 +1,34 @@ + + + + + + + + +
                            +
                          • +
                            +
                            Пример раздела
                            +
                          • +
                          + + + diff --git a/3-more/11-css-for-js/17-css-sprite/icons.gif b/3-more/11-css-for-js/17-css-sprite/icons.gif new file mode 100755 index 00000000..9c24751e Binary files /dev/null and b/3-more/11-css-for-js/17-css-sprite/icons.gif differ diff --git a/3-more/11-css-for-js/17-css-sprite/minus.gif b/3-more/11-css-for-js/17-css-sprite/minus.gif new file mode 100755 index 00000000..e4e6e6eb Binary files /dev/null and b/3-more/11-css-for-js/17-css-sprite/minus.gif differ diff --git a/3-more/11-css-for-js/17-css-sprite/plus.gif b/3-more/11-css-for-js/17-css-sprite/plus.gif new file mode 100755 index 00000000..9f52ccdd Binary files /dev/null and b/3-more/11-css-for-js/17-css-sprite/plus.gif differ diff --git a/3-more/11-css-for-js/17-css-sprite/sprite-example/index.html b/3-more/11-css-for-js/17-css-sprite/sprite-example/index.html new file mode 100755 index 00000000..4c0e07f7 --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/sprite-example/index.html @@ -0,0 +1,25 @@ + + + + + + + + + +
                          +Очень интересная новость Очень интересная новость Очень интересная новость Очень интересная новость Очень интересная новость +Очень интересная новость Очень интересная новость Очень интересная новость Очень интересная новость Очень интересная новость + + + + diff --git a/3-more/11-css-for-js/17-css-sprite/sprite-tree-1/height48.view/index.html b/3-more/11-css-for-js/17-css-sprite/sprite-tree-1/height48.view/index.html new file mode 100755 index 00000000..0425249f --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/sprite-tree-1/height48.view/index.html @@ -0,0 +1,34 @@ + + + + + + + + +
                            +
                          • +
                            +
                            Пример раздела
                            +
                          • +
                          + + + diff --git a/3-more/11-css-for-js/17-css-sprite/sprite-tree-1/index.html b/3-more/11-css-for-js/17-css-sprite/sprite-tree-1/index.html new file mode 100755 index 00000000..2809caa9 --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/sprite-tree-1/index.html @@ -0,0 +1,34 @@ + + + + + + + + +
                            +
                          • +
                            +
                            Пример раздела
                            +
                          • +
                          + + + diff --git a/3-more/11-css-for-js/17-css-sprite/sprite-tree-src.view/index.html b/3-more/11-css-for-js/17-css-sprite/sprite-tree-src.view/index.html new file mode 100755 index 00000000..a84f874d --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/sprite-tree-src.view/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + +
                            +
                          • +
                            +
                            Раздел 1
                            В две строки
                            +
                              +
                            • +
                              +
                              Раздел 1.1 в одну строку
                              +
                            • +
                            • +
                              +
                              Страница 1.2
                              в две строки
                              +
                            • +
                            +
                          • +
                          • +
                            +
                            Раздел 2
                            В две строки
                            +
                          • +
                          + + + diff --git a/3-more/11-css-for-js/17-css-sprite/sprite-tree.view/index.html b/3-more/11-css-for-js/17-css-sprite/sprite-tree.view/index.html new file mode 100755 index 00000000..95e50b77 --- /dev/null +++ b/3-more/11-css-for-js/17-css-sprite/sprite-tree.view/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + +
                            +
                          • +
                            +
                            Раздел 1
                            В две строки
                            +
                              +
                            • +
                              +
                              Раздел 1.1 в одну строку
                              +
                            • +
                            • +
                              +
                              Страница 1.2
                              в две строки
                              +
                            • +
                            +
                          • +
                          • +
                            +
                            Раздел 2
                            В две строки
                            +
                          • +
                          + + + diff --git a/3-more/11-css-for-js/17-css-sprite/sprites-example-lines2.png b/3-more/11-css-for-js/17-css-sprite/sprites-example-lines2.png new file mode 100755 index 00000000..8a8db21c Binary files /dev/null and b/3-more/11-css-for-js/17-css-sprite/sprites-example-lines2.png differ diff --git a/3-more/11-css-for-js/17-css-sprite/sprites-example.png b/3-more/11-css-for-js/17-css-sprite/sprites-example.png new file mode 100755 index 00000000..fbd5eaac Binary files /dev/null and b/3-more/11-css-for-js/17-css-sprite/sprites-example.png differ diff --git a/3-more/11-css-for-js/18-css-format/article.md b/3-more/11-css-for-js/18-css-format/article.md new file mode 100644 index 00000000..947ef731 --- /dev/null +++ b/3-more/11-css-for-js/18-css-format/article.md @@ -0,0 +1,145 @@ +# Правила форматирования CSS + +Для того, чтобы CSS легко читался, полезно соблюдать пять правил форматирования. +[cut] + +## Каждое свойство -- на отдельной строке + +Так -- неверно: + +```css +##snapshot-box h2 { padding: 0 0 6px 0; font-weight: bold; position: absolute; left: 0; top: 0; } +``` + +Так -- правильно: + +```css +##snapshot-box h2 { + position: absolute; + left: 0; + top: 0; + padding: 0 0 6px 0; + font-weight: bold; +} +``` + +Цель -- лучшая читаемость, проще найти и поправить свойство. + +## Каждый селектор -- на отдельной строке + +Неправильно: + +```css +##snapshot-box h2, #profile-box h2, #order-box h2 { + padding: 0 0 6px 0; + font-weight: bold; +} +``` + +Правильно: + +```css +##snapshot-box h2, +##profile-box h2, +##order-box h2 { + padding: 0 0 6px 0; + font-weight: bold; +} +``` + +Цель -- лучшая читаемость, проще найти селектор. + +## Свойства, сильнее влияющие на документ, идут первыми + +Рекомендуется располагать свойства в следующем порядке: +
                            +
                          1. Сначала положение элемента относительно других: +`position`, `left/right/top/bottom`, `float`, `clear`, `z-index`.
                          2. +
                          3. Затем размеры и отступы: +`width`, `height`, `margin`, `padding`...
                          4. +
                          5. Рамка `border`, она частично относится к размерам.
                          6. +
                          7. Общее оформление содержимого: +`list-style-type`, `overflow`...
                          8. +
                          9. Цветовое и стилевое оформление: +`background`, `color`, `font`...
                          10. +
                          + +**Общая логика сортировки: "от общего -- к локальному и менее важному".** + +При таком порядке свойства, определяющие структуру документа, будут видны наиболее отчётливо, в начале, а самые незначительно влияющие (например цвет) -- в конце. + +Например: + +```css +##snapshot-box h2 { + position: absolute; /* позиционирование */ + left: 0; + top: 0; + + padding: 0 0 6px 0; /* размеры и отступы */ + + color: red; /* стилевое оформление */ + font-weight: bold; +} +``` + +
                        • +
                        • **Свойство без префикса пишется последним.** + +Например: + +```css +-webkit-box-shadow:0 0 100px 20px #000; +box-shadow:0 0 100px 20px #000; +``` + +Это нужно, чтобы стандартная (окончательная) реализация всегда была важнее, чем временные браузерные. +
                        • + +## Организация CSS-файлов проекта + +Стили можно разделить на две основные группы: +
                            +
                          1. **Блоки-компоненты имеют свой CSS.** Например, CSS для диалогового окна, CSS для профиля посетителя, CSS для меню. + +Такой CSS идёт "в комплекте" с модулем, его удобно выделять в отдельные файлы и подключать через `@import`. + +Конечно, при разработке будет много CSS-файлов, но при выкладке проекта система сборки и сжатия CSS заменит директивы `@import` на их содержимое, объединяя все CSS в один файл. +
                          2. +
                          3. **Страничный и интегрирующий CSS**. +Этот CSS описывает общий вид страницы, расположение компонент и их дополнительную стилизацию, зависящую от места на странице и т.п. + +```css +.tab .profile { /* профиль внутри вкладки */ + float: left; + width: 300px; + height: 200px; +} +``` + +Важные страничные блоки можно выделять особыми комментариями: + +```css +/** =========================== + * Профиль посетителя + * =========================== +*/ + +.profile { + border: 1px solid gray; +} + +.profile h2 { + color: blue; + font-size: 1.8em; +} +``` + +
                          4. +
                          + + +CSS-препроцессоры, такие как [SASS](http://sass-lang.com/), [LESS](http://lesscss.org/), [Stylus](http://learnboost.github.com/stylus/), [Prefix-free](http://leaverou.github.com/prefixfree/) делает написание CSS сильно удобнее.. + +Выберите один из них и используйте. Единственно, они добавляют дополнительную предобработку CSS, которую нужно учесть, и желательно, на сервере. + diff --git a/3-more/11-css-for-js/2-css-units/article.md b/3-more/11-css-for-js/2-css-units/article.md new file mode 100644 index 00000000..6dd2442d --- /dev/null +++ b/3-more/11-css-for-js/2-css-units/article.md @@ -0,0 +1,290 @@ +# Единицы измерения: "px", "em", "rem", "%" и другие + +В этом очерке я постараюсь не только рассказать о различных единицах измерения, но и построить общую картину -- что и когда выбирать. + +[cut] +## Абсолютные: px + +Пиксель `px` -- это самая базовая, абсолютная и окончательная единица измерения. + +Количество пикселей задаётся в настроках [разрешения экрана](http://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B7%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5_(%D0%BA%D0%BE%D0%BC%D0%BF%D1%8C%D1%8E%D1%82%D0%B5%D1%80%D0%BD%D0%B0%D1%8F_%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D0%BA%D0%B0), один `px` -- это как раз один такой пиксель на экране. Все значения браузер в итоге пересчитает в пиксели. + +Пиксели могут быть дробными, например размер можно задать в `16.5px`. Это совершенно нормально, браузер сам использует дробные пиксели для внутренних вычислений. К примеру, есть элемент шириной в `100px`, его нужно разделить на три части -- волей-неволей появляются `33.333...px`. + +Для мобильных устройств, у которых много пикселей на экране, но сам экран маленький, чтобы обеспечить читаемость, браузер автоматически применяет масштабирование. При окончательном отображении дробные пиксели, конечно же, округляются и становятся целыми. + +[compare] ++Главное достоинство пикселя -- чёткость и понятность +-Другие единицы измерения -- в некотором смысле "мощнее", они являются относительными и позволяют устанавливать соотношения между различными размерами +[/compare] + + +[warn header="Давно на свалке: `mm`, `cm`, `pt`, `pc`"] + +Существуют также "производные" от пикселя единицы измерения: `mm`, `cm`, `pt` и `pc`, но они давно отправились на свалку истории. + +Вот, если интересно, их значения: +
                            +
                          • `1mm` (мм) = `3.8px`
                          • +
                          • `1cm` (см) = `38px`
                          • +
                          • `1pt` (типографский пункт) = `4/3 px`
                          • +
                          • `1pc` (типографская пика) = `16px`
                          • +
                          +Так как браузер пересчитывает эти значения в пиксели, то смысла в их употреблении нет. +[/warn] + +[smart header="Почему сантиметре `cm` содержится ровно 38 пикселей?"] +В реальной жизни сантиметр -- это такой отрезок, эталон длины. А [пиксель](http://ru.wikipedia.org/wiki/%D0%9F%D0%B8%D0%BA%D1%81%D0%B5%D0%BB%D1%8C) может быть разным, в зависимости от экрана. + +Но в формулах выше под пикселем понимается "сферический пиксель в вакууме", точка на "стандартизованном экране", характеристики которого описаны в [спецификации](http://www.w3.org/TR/CSS2/syndata.html#length-units). + +Поэтому ни о каком соответствии `cm` реальному сантиметру здесь нет и речи. Полностью синтетическая и ненужная единица измерения. +[/smart] + + +## Относительно шрифта: em + +`1em` -- текущий размер шрифта. + +Можно брать любые пропорции от текущего шрифта: `2em`, `0.5em` и т.п. + +**Размеры в `em` -- *относительные*, они определяются по текущему контексту.** + +Например, давайте сравним `px` с `em` на таком примере: + +```html + +
                          + Страусы +
                          Живут также в Африке
                          +
                          +``` + +`24` пикселей -- и в Африке `24` пикселей, поэтому две строчки выше одинаковы. + +А вот аналогичный пример с `em` вместо `px`: + +```html + +
                          + Страусы +
                          Живут также в Африке
                          +
                          +``` + +Так как значение в `em` высчитывается относительно *текущего шрифта*, то вторая строка в `1.5` раза больше, чем первая. + +**Выходит, задавая размеры в `em`, мы задаём зависимость элементов от стиля родителя. Хочется увеличить или уменьшить всё вместе -- легко, просто поправим один стиль.** + + +[smart header="Также относительно шрифта: `ex`, `ch`"] +В спецификации указаны также единицы [ex](http://www.w3.org/TR/css3-values/#ex-unit) и [ch](http://www.w3.org/TR/css3-values/#ch-unit) ), которые означают размер в зависимости от таких характеристик шрифта как размер символа `"x"` и размер символа `"0"`. + +Эти размеры присутствуют всегда, даже если по коду этих символов в шрифте находятся другие значения, а не именно буква `"x"` и ноль `"0"`. + +Эти единицы используются чрезвычайно редко, так как "размер шрифта" `em` обычно вполне подходит. +[/smart] + +## Проценты % + +Проценты `%`, как и `em` -- относительные единицы. + +Вот пример с `%`, он выглядит в точности так же, как с `em`: + +```html + +
                          + Страусы +
                          Живут также в Африке
                          +
                          +``` + +Когда мы говорим "процент", то возникает вопрос -- "Процент от чего?" + +**Как правило, процент будет от значения свойства родителя с тем же названием, но не всегда.** + +Это очень важная особенность процентов, про которую, увы, часто забывают. + +Отличный источник информации по этой теме -- стандарт, [Visual formatting model details](http://www.w3.org/TR/CSS2/visudet.html). + +Некоторые примеры: +
                            +
                          • При установке свойства `margin-left` в `%`, процент берётся от *ширины* родительского блока (а вовсе не от его `margin-left`!)
                          • +
                          • При установке свойства `line-height` в `%`, процент берётся от текущего *размера шрифта* (а вовсе не от текущего `line-height`!)
                          • +
                          • Для `width/height` при `position:fixed`, процент берётся от ширины/высоты *окна* (а не родителя и не документа).
                          • +
                          • Кроме того, иногда `%` требует соблюдения дополнительных условий, за примером -- обратитесь к статье [](/height-percent).
                          • +
                          • Детали по `line-height` и размеру шрифта вы также можете найти в статье [](/font-size-line-height).
                          • +
                          + +## Микс px и em: rem + +Итак, что у нас есть: +
                            +
                          • `px` -- абсолютные, чёткие, понятные, не зависящие ни от чего.
                          • +
                          • `em` -- относительно размера шрифта.
                          • +
                          • `%` -- относительно такого же свойства родителя (а может и не родителя, а может и не такого же -- см. оговорки выше).
                          • +
                          + +Может быть, пора уже остановиться, может этого достаточно? + +Э-э, нет! Не все вещи делаются удобно. + +Вернёмся к теме шрифтов. Бывают задачи, когда мы хотим сделать на странице большие кнопки "Шрифт больше" и "Шрифт меньше". При нажатии на них будет срабатывать JavaScript, который будет увеличивать или уменьшать шрифт. + +Вообще-то это можно сделать без JavaScript, в браузере обычно есть горячие клавиши для масштабирования вроде [key Ctrl++], но они работают слишком тупо -- берут и увеличивают всю страницу, вместе с изображениями и другими элементами, которые масштабировать как раз не надо. А надо увеличить, к примеру, только шрифт, потому что посетитель хочет комфортнее читать. И увеличенный шрифт по-новому обтечёт элементы страницы. + +Какую единицу использовать для задания шрифтов? Лучше бы не `px`, ведь такие значения абслютны, если менять, то везде, вполне возможна ситуация, когда мы в одном правиле размер поменяли, а другое забыли. + +Следующие кандидаты -- `em` и `%`. + +Разницы между ними здесь нет, так как при задании `font-size` в процентах, эти проценты берутся от `font-size` родителя, то есть ведут себя так же, как и `em`. + +Вроде бы, использовать можно, однако есть проблема. + +Посмотрите на пример с вложенными `
                        • `: + +```html + + + +
                            +
                          • Собака +
                              +
                            • бывает +
                                +
                              • кусачей +
                                  +
                                • только +
                                    +
                                  • от жизни +
                                      +
                                    • собачей
                                    • +
                                    +
                                  • +
                                  +
                                • +
                                +
                              • +
                              +
                            • +
                            +
                          • +
                          +``` + +Проблема очевидна. Хотели, как лучше, а получилось... Мелковато. Каждый `
                        • ` получил размер шрифта `0.8` от внешнего, это не совсем то, чего мы бы здесь хотели. + +Время появиться единице `rem`, которая, можно сказать, придумана для таких случаев! + +**Единица `rem` задаёт размер относительно размера шрифта элемента ``.** + +Как правило, браузеры ставят этому элементу некоторый "разумный" (reasonable) размер по-умолчанию, который мы, конечно, можем переопределить и использовать `rem` для задания шрифтов внутри: + +```html + + + +
                          + + + +
                            +
                          • Собака +
                              +
                            • бывает +
                                +
                              • кусачей +
                                  +
                                • только +
                                    +
                                  • от жизни +
                                      +
                                    • собачей
                                    • +
                                    +
                                  • +
                                  +
                                • +
                                +
                              • +
                              +
                            • +
                            +
                          • +
                          + + +``` + +Получилось удобное масштабирование для шрифтов, не влияющее на другие элементы. + +Можно воспринимать `rem` как остановку посередине между `em` и `px`, потому что элементы в `rem` не зависят друг от друга и от контекста -- и этим похожи на `px`, а с другой стороны они все заданы относительно размера шрифта ``. + +[warn header="Не поддерживается в IE8-"] +Единица `rem` не поддерживается в IE8-. Для этих браузеров, которых становится всё меньше, есть два выхода: +
                            +
                          1. Либо использовать `em` и правила, страхующие от вложенности, вроде: + +```css +li { font-size: 0.8em } +li li { font-size: 0.8em } +li li li { font-size: 0.8em } +``` + +
                          2. +
                          3. Либо сделать два правила в стилях: первое указывает размер в `px` (для IE8-), а второе -- в `rem` (переопределит первое в современных браузерах) и отключить маштабирование шрифтов для IE8-. Это проще.
                          4. +
                          +[/warn] + +## Относительно экрана: vw, vh, vmin, vmax + +Во всех современных браузерах, исключая IE8-, поддерживаются новые единицы из черновика стандарта [CSS Values and Units 3](http://dev.w3.org/csswg/css3-values/): + +
                            +
                          • `vw` -- 1% ширины окна
                          • +
                          • `vh` -- 1% высоты окна
                          • +
                          • `vmin` -- наименьшее из (`vw`, `vh`), в IE9 обозначается `vm`
                          • +
                          • `vmax` -- наибольшее из (`vw`, `vh`)
                          • +
                          + +**Эти значения были созданы, в первую очередь, для поддержки мобильных устройств.** + +Их основное преимущество -- в том, что любые размеры, которые в них заданы, автоматически масштабируются при изменении размеров окна. + +Ниже написан текст с размером `5vh`: + +

                          Страусы кусачими не бывают.

                          + +**Вы сможете легко увидеть, как работает `vh`, если поменяете размер окна браузера. Текст будет расти/уменьшаться.** + +## Итого + +Мы рассмотрели единицы измерения: + +
                            +
                          • `px` -- абсолютные пиксели, к которым привязаны и потому не нужны `mm`, `cm`, `pt` и `pc`. Используется для максимально конкретного и точного задания размеров.
                          • +
                          • `em` -- задаёт размер относительно шрифта родителя, можно относительно конкретных символов: `"x"`(`ex`) и `"0"`(`ch`), используется там, где нужно упростить масштабирование компоненты.
                          • +
                          • `rem` -- задаёт размер относительно шрифта ``, используется для удобства глобального масштабирования: элементы которые планируется масштабировать, задаются в `rem`, а JS меняет шрифт у ``.
                          • +
                          • `%` -- относительно такого же свойства родителя (как правило, но не всегда), используется для ширин, высот и так далее, без него никуда, но надо знать, относительно чего он считает проценты.
                          • +
                          • `vw`, `vh`, `vmin`, `vmax` -- относительно размера экрана.
                          • +
                          + +Здесь я постарался разобрать их применение и преимущества для решения конкретных задач, но вы, конечно, сможете использовать их для других ситуаций. diff --git a/3-more/11-css-for-js/3-display/article.md b/3-more/11-css-for-js/3-display/article.md new file mode 100644 index 00000000..b0063e47 --- /dev/null +++ b/3-more/11-css-for-js/3-display/article.md @@ -0,0 +1,265 @@ +# Все значения свойства "display" + +Свойство `display` имеет много разных значений. Обычно, используются только три из них: `none`, `inline` и `block`, потому что когда-то браузеры другие не поддерживали. + +Но, после прихода IE7 и, особенно, IE8+, стало возможным использовать и другие значения тоже. Рассмотрим здесь весь список. + +## Значение none + +Самое простое значение. Элемент не показывается, вообще. Как будто его и нет. + +```html + +
                          +Невидимый div ( +
                          Я - невидим!
                          +) Стоит внутри скобок +
                          +``` + +## Значение block + +
                            +
                          • **Блочные элементы располагаются один над другим, вертикально** (если нет особых свойств позиционирования, например `float`).
                          • +
                          • **Блок стремится расшириться на всю доступную ширину. Можно указать ширину и высоту явно.**
                          • +
                          + +Это значение `display` многие элементы имеют по умолчанию: `DIV`, заголовок, параграф. + +```html + +
                          +
                          Первый
                          +
                          Второй
                          +
                          +``` + +Блоки прилегают друг к другу вплотную, если у них нет `margin`. + +## Значение inline + +
                            +
                          • **Элементы располагаются на той же строке, последовательно.**
                          • +
                          • **Ширина и высота элемента определяются по содержимому. Поменять их нельзя.**
                          • +
                          + +Например, инлайновые элементы по умолчанию: `SPAN`, ссылка. + +```html + + + Ширина + Игнорируется + +``` + +Если вы присмотритесь внимательно к примеру выше, то увидите, что между внутренними `SPAN` и `A` есть пробел. Это потому, что он есть в HTML. + +Если расположить элементы вплотную -- его не будет: + +```html + + + БезПробела + +``` + +Содержимое инлайн-элемента может переноситься на другую строку. + +При этом каждая строка в смысле отображения является отдельным прямоугольником ("line box"). **Так что инлайн-элемент состоит из объединения прямоугольников, но в целом, в отличие от блока, прямоугольником не является.** + +Например: + +```html + +... + Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля + Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля + Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля Ля +... +``` + +**Если инлайн-элемент граничит с блоком, то между ними обязательно будет перенос строки:** + +```html + +
                          + Инлайн +
                          Блок
                          + Инлайн +
                          +``` + +## Значение inline-block + +Это значение -- означает элемент, который продолжает находиться в строке (`inline`), но при этом может иметь важные свойства блока. + +Как и инлайн-элемент: +
                            +
                          • **Располагается в строке.**
                          • +
                          • **Размер устанавливается по содержимому.**
                          • +
                          + +Во всём остальном -- это блок, то есть: +
                            +
                          • **Элемент всегда прямоугольный.**
                          • +
                          • **Работают свойства `width/height`.**
                          • +
                          + +Это значение `display` используют, чтобы отобразить в одну строку блочные элементы, в том числе разных размеров. + +Например: + +```html + + + +
                            +
                          • Инлайн Блок
                            3 строки
                            высота/ширина явно не заданы
                          • +
                          • Инлайн
                            Блок 100x100
                          • +
                          • Инлайн
                            Блок 60x60
                          • +
                          • Инлайн
                            Блок 100x60
                          • +
                          • Инлайн
                            Блок 60x100
                          • +
                          +``` + +Свойство `vertical-align` позволяет выровнять такие элементы внутри внешнего блока: + +```html + + + +
                            +
                          • Инлайн Блок
                            3 строки
                            высота/ширина явно не заданы
                          • +
                          • Инлайн
                            Блок 100x100
                          • +
                          • Инлайн
                            Блок 60x60
                          • +
                          • Инлайн
                            Блок 100x60
                          • +
                          • Инлайн
                            Блок 60x100
                          • +
                          +``` + +Как и в случае с инлайн-элементами, пробелы между блоками появляются из-за пробелов в HTML. Если элементы списка идут вплотную, например, генерируются в JavaScript -- их не будет. + +**IE7 допускает это значение только для элементов, которые по умолчанию `inline`.** + +## Значения table-* + +Современные браузеры (не IE6,7) позволяют описывать таблицу любыми элементами, если поставить им соответствующие значения `display`. + +Для таблицы целиком `table`, для строки -- `table-row`, для ячейки -- `table-cell` и т.д. + +Пример использования: + +```html + +
                          +
                          + + +
                          +
                          + + +
                          +
                          +``` + +Важно то, что это действительно полноценная таблица. Используются табличные алгоритмы вычисления ширины и высоты элемента, [описанные в стандарте](http://www.w3.org/TR/CSS2/tables.html#width-layout). + +**Это хорошо для семантической вёрстки и позволяет избавиться от лишних тегов.** + +С точки зрения современного CSS, обычные `TABLE`, `TR`, `TD` и т.д. -- это просто элементы с предопределёнными значениями `display`: + +```css +table { display: table } +tr { display: table-row } +thead { display: table-header-group } +tbody { display: table-row-group } +tfoot { display: table-footer-group } +col { display: table-column } +colgroup { display: table-column-group } +td, th { display: table-cell } +caption { display: table-caption } +``` + +Очень подробно об алгоритмах вычисления размеров и отображении таблиц рассказывает стандарт [CSS 2.1 - Tables](http://www.w3.org/TR/CSS2/tables.html). + +### Вертикальное центрирование с table-cell + +Внутри ячеек свойство [vertical-align](http://www.w3.org/TR/CSS2/visudet.html#propdef-vertical-align) выравнивает содержимое по вертикали. + +Это можно использовать для центрирования: + +```html + + + +
                          +
                          Элемент
                          С неизвестной
                          Высотой
                          +
                          +``` + +CSS не требует, чтобы вокруг `table-cell` была структура таблицы: `table-row` и т.п. Может быть просто такой одинокий `DIV`, это допустимо. + +При этом он ведёт себя как ячейка `TD`, то есть подстраивается под размер содержимого и умеет вертикально центрировать его при помощи `vertical-align`. + +Значение `display: table-cell` поддерживается во всех браузерах, кроме IE<8. В IE6,7 можно использовать для центрирования CSS-выражения или реальную таблицу. + +## Значения list-item и run-in + +У свойства `display` есть и другие значения. Они используются реже, поэтому посмотрим на них кратко: + +
                          +
                          `list-item`
                          +
                          Этот display по умолчанию используется для элементов списка. Он добавляет к блоку содержимым ещё и блок с номером(значком) списка, который стилизуется стандартными списочными свойствами: + +```html + +
                          Пункт 1
                          +``` + +
                          +
                          `run-in`
                          +
                          Если после `run-in` идёт `block`, то `run-in` становится его первым инлайн-элементом, то есть отображается в начале `block`. + +Если ваш браузер поддерживает это значение,** то примере ниже `h3`, благодаря `display:run-in`, окажется визуально внутри `div`: + +```html + +

                          Про пчёл.

                          +
                          Пчёлы - отличные создания, они делают мёд.
                          +``` + +Если же вы видите две строки, то ваш браузер НЕ поддерживает `run-in`. + +Вот, для примера, правильный вариант отображения `run-in`, оформленный другим кодом: + +```html + +
                          +

                          Про пчёл.

                          Пчёлы - отличные создания, они делают мёд. +
                          +``` + +Если этот вариант отличается от того, что вы видите выше -- ваш браузер не поддерживает `run-in`. На момент написания этой статьи только IE поддерживал `display:run-in`. +
                          +
                          \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-float-diffsize.png b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-float-diffsize.png new file mode 100755 index 00000000..0670bc21 Binary files /dev/null and b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-float-diffsize.png differ diff --git a/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-float-diffsize.view/index.html b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-float-diffsize.view/index.html new file mode 100755 index 00000000..13ff2887 --- /dev/null +++ b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-float-diffsize.view/index.html @@ -0,0 +1,75 @@ + + + + + + + + + + + diff --git a/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-inline-block.view/index.html b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-inline-block.view/index.html new file mode 100755 index 00000000..72adde76 --- /dev/null +++ b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/gallery-inline-block.view/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + diff --git a/3-more/11-css-for-js/4-float/1-inline-block-vs-float/solution.md b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/solution.md new file mode 100644 index 00000000..9c05c9d8 --- /dev/null +++ b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/solution.md @@ -0,0 +1,44 @@ +# Наводящие вопросы + +
                            +
                          1. Что произойдёт, если контейнеру `UL` поставить рамку `border` -- в первом и во втором случае?
                          2. +
                          3. Что будет, если элементы `LI` различаются по размеру? Будут ли они корректно перенесены на новую строку в обоих случаях?
                          4. +
                          5. Как будут вести себя блоки, находящиеся под галереей?
                        + + +# Ответы + +Разница колоссальная. + +В первую очередь она в том, что `inline-block` продолжают участвовать в потоке, а `float` -- нет. + +Ответы на вопросы по примеру: + +
                        +
                        Что будет, если контейнеру `UL` поставить рамку `border`?
                        +
                        Контейнер не выделяет пространство под `float`. А больше там ничего нет. В результате он просто сожмётся в одну линию сверху. + +Попробуйте сами, добавьте рамку в [edit src="solution"]песочнице[/edit]. + +А в случае с `inline-block` всё будет хорошо, т.к. элементы остаются в потоке. +
                        +
                        Что будет, если элементы `LI` различаются по размеру? Будут ли они корректно перенесены на новую строку в обоих случаях?
                        +
                        При `float:left` элементы двигаются направо до тех пор, пока не наткнутся на границу внешнего блока (с учётом `padding`) или на другой `float`-элемент. + +Может получиться вот так: + + + +Вы можете увидеть это, открыв демо-галерею в отдельном окне и изменяя его размер: + +[demo src="gallery-float-diffsize"] + +При использовании `inline-block` таких странностей не будет, блоки перенесутся корректно на новую строку. И, кроме того, можно выровнять элементы по высоте при помощи `li { vertical-align:middle }`: + +[iframe height=500 src="gallery-inline-block" link edit] +
                        +
                        Как будут вести себя блоки, находящиеся под галереей?
                        +
                        В случае с `float` нужно добавить дополнительную очистку с `clear`, чтобы поведение было идентично обычному блоку. + +Иначе блоки, находящиеся под галереей, вполне могут "заехать" по вертикали на территорию галереи.
                        +
                        \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/1-inline-block-vs-float/solution.view/index.html b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/solution.view/index.html new file mode 100755 index 00000000..aadb464c --- /dev/null +++ b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/solution.view/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + diff --git a/3-more/11-css-for-js/4-float/1-inline-block-vs-float/task.md b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/task.md new file mode 100644 index 00000000..cb9ba2a7 --- /dev/null +++ b/3-more/11-css-for-js/4-float/1-inline-block-vs-float/task.md @@ -0,0 +1,16 @@ +# Разница inline-block и float + +[importance 5] + +Галерея изображений состоит из картинок в рамках с подписями (возможно, с другой дополнительной информацией). + +Пример галереи: +[iframe src="solution" link edit]. + +Технически вывод такой галереи можно реализовать при помощи списка UL/LI, где: +
                          +
                        1. каждый LI имеет `display:inline-block`
                        2. +
                        3. каждый LI имеет `float:left`
                        4. +
                        + +**Какие различия между этими подходами? Какой вариант выбрали бы вы?** diff --git a/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/solution.md b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/solution.md new file mode 100644 index 00000000..1009c29f --- /dev/null +++ b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/solution.md @@ -0,0 +1,3 @@ +Для решения можно применить принцип двухколоночной верстки `float` + `margin`. Иконка будет левой колонкой, а содержимое -- правой. + +[edit src="solution"/] \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/solution.view/index.html b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/solution.view/index.html new file mode 100755 index 00000000..1fc4c668 --- /dev/null +++ b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/solution.view/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + +
                          +
                        • +
                          +
                          Раздел 1
                          (занимает две строки)
                          +
                            +
                          • +
                            +
                            Раздел 1.1
                            +
                          • +
                          • +
                            +
                            Страница 1.2
                            (занимает две строки)
                            +
                          • +
                          +
                        • +
                        • +
                          +
                          Раздел 2
                          (занимает две строки)
                          +
                        • +
                        + + + diff --git a/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/source.view/index.html b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/source.view/index.html new file mode 100755 index 00000000..ab21f3b9 --- /dev/null +++ b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/source.view/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + +
                          +
                        • +
                          +
                          Раздел 1
                          (занимает две строки)
                          +
                            +
                          • +
                            +
                            Раздел 1.1
                            +
                          • +
                          • +
                            +
                            Страница 1.2
                            (занимает две строки)
                            +
                          • +
                          +
                        • +
                        • +
                          +
                          Раздел 2
                          (занимает две строки)
                          +
                        • +
                        + + + diff --git a/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/task.md b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/task.md new file mode 100644 index 00000000..c670c76e --- /dev/null +++ b/3-more/11-css-for-js/4-float/2-tree-with-multiline-nodes/task.md @@ -0,0 +1,20 @@ +# Дерево с многострочными узлами + +[importance 3] + +Сделайте дерево при помощи семантической вёрстки и CSS-спрайта с иконками (есть готовый). + +Выглядеть должно так (не кликабельно): + +[iframe src="solution" border="1" height=200 link edit] + +
                          +
                        • Поддержка многострочных названий узлов
                        • +
                        • Над иконкой курсор становится указателем.
                        • +
                        + +Исходный документ содержит список UL/LI и ссылку на картинку. + +[edit task src="source"/] + +P.S. Достаточно сделать HTML/CSS-структуру, действия добавим позже. \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/arrow-left.jpg b/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/arrow-left.jpg new file mode 100755 index 00000000..5c9532a4 Binary files /dev/null and b/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/arrow-left.jpg differ diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/arrow-right.jpg b/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/arrow-right.jpg new file mode 100755 index 00000000..b2ac9c82 Binary files /dev/null and b/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/arrow-right.jpg differ diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/index.html b/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/index.html new file mode 100755 index 00000000..0c718638 --- /dev/null +++ b/3-more/11-css-for-js/4-float/3-paginator-css/nav-div-wrong/index.html @@ -0,0 +1,39 @@ + + + + + + + + +Текст Сверху + + + +Текст Снизу + + + diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/solution.md b/3-more/11-css-for-js/4-float/3-paginator-css/solution.md new file mode 100644 index 00000000..f19a84c0 --- /dev/null +++ b/3-more/11-css-for-js/4-float/3-paginator-css/solution.md @@ -0,0 +1,99 @@ +HTML-структура: + +```html + +``` + +Стили: + +```css +.nav { + height: 40px; + width: 80%; + margin: auto; +} + +.nav .left { + float: left; + cursor: pointer; +} + +.nav .right { + float: right; + cursor: pointer; +} + +.nav .pages { + list-style: none; + text-align: center; + margin: 0; + padding: 0; +} + +.nav .pages li { + display: inline; + margin: 0 3px; + line-height: 40px; + cursor: pointer; +} +``` + +Основные моменты: +
                          +
                        • **Сначала идёт левая кнопка, затем правая, а лишь затем -- текст.** +Почему так, а не лево - центр - право? + +Дело в том, что `float` смещает элемент вправо относительно обычного места. А какое обычное место будет у правого `IMG` без `float`? + +Оно будет под списком, так как список -- блочный элемент, а `IMG` -- инлайн-элемент. При добавлении `float:right` элемент `IMG` сдвинется вправо, оставшись под списком. + +Код в порядке лево-центр-право (неправильный): + +```html + + +
                            (li) 1 2 3 4 5 6 7 8 9
                          + + +``` + +Его демо: +[iframe src="nav-div-wrong" border=1 height="140"] +Правильный порядок: лево-право-центр, тогда `float` останется на верхней строке. + +Код, который даёт правильное отображение: + +```html +
                          + + +
                            .. список ..
                          +
                          +``` + +Также можно расположить стрелки при помощи `position: absolute`. Тогда, чтобы текст при уменьшении размеров окна не налез на стрелки -- нужно добавить в контейнер левый и правый `padding`: + +Выглядеть будет примерно так: + +```html +
                          + +
                            (li) 1 2 3 4 5 6 7 8 9
                          + +
                          +``` + +
                        • + +
                        • **Центрирование одной строки по вертикали осуществляется указанием `line-height`, равной высоте.** + +Это красиво лишь для одной строки: если окно становится слишком узким, и строка вдруг разбивается на две -- получается некрасиво, хотя и читаемо. + +Если хочется сделать красивее для двух строк, то можно использовать другой способ центрирования.
                        • +
                        + +[edit src="solution"]Полное решение[/edit] \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/solution.view/index.html b/3-more/11-css-for-js/4-float/3-paginator-css/solution.view/index.html new file mode 100755 index 00000000..b4569092 --- /dev/null +++ b/3-more/11-css-for-js/4-float/3-paginator-css/solution.view/index.html @@ -0,0 +1,61 @@ + + + + + + + + +Текст Сверху + + + +Текст Снизу + + + + diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/source.view/index.html b/3-more/11-css-for-js/4-float/3-paginator-css/source.view/index.html new file mode 100755 index 00000000..3bcc167d --- /dev/null +++ b/3-more/11-css-for-js/4-float/3-paginator-css/source.view/index.html @@ -0,0 +1,39 @@ + + + + + + + + +Текст Сверху + + +Текст Снизу + + + + diff --git a/3-more/11-css-for-js/4-float/3-paginator-css/task.md b/3-more/11-css-for-js/4-float/3-paginator-css/task.md new file mode 100644 index 00000000..0c7536d2 --- /dev/null +++ b/3-more/11-css-for-js/4-float/3-paginator-css/task.md @@ -0,0 +1,19 @@ +# Постраничная навигация (CSS) + +[importance 5] + +Оформите навигацию, центрированную внутри `DIV'а`: + +[iframe src="solution" height="100" border="1"] + +Требования: +
                          +
                        • Левая стрелка -- слева, правая -- справа, список страниц -- по центру.
                        • +
                        • Список страниц центрирован вертикально.
                        • +
                        • Текст сверху и снизу ни на что не наползает.
                        • +
                        • Курсор при наведении на стрелку или элемент списка становится стрелкой `pointer`.
                        • +
                        + +[edit src="source" task/] + +P.S. Без использования таблиц. \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/4-add-border-keep-width/solution.md b/3-more/11-css-for-js/4-float/4-add-border-keep-width/solution.md new file mode 100644 index 00000000..1eee87d5 --- /dev/null +++ b/3-more/11-css-for-js/4-float/4-add-border-keep-width/solution.md @@ -0,0 +1,41 @@ +# Подсказка + +Используйте свойство `box-sizing`. + +# Решение + +Да, можно -- указываем `box-sizing: border-box` и добавляем свойства: + +```html + + + +
                        + Левая
                        Колонка +
                        +
                        + Правая
                        Колонка
                        ... +
                        +``` + diff --git a/3-more/11-css-for-js/4-float/4-add-border-keep-width/task.md b/3-more/11-css-for-js/4-float/4-add-border-keep-width/task.md new file mode 100644 index 00000000..4d28d59b --- /dev/null +++ b/3-more/11-css-for-js/4-float/4-add-border-keep-width/task.md @@ -0,0 +1,37 @@ +# Добавить рамку, сохранив ширину + +[importance 4] + +Есть две колонки `30%/70%`: + +```html + + + +
                        + Левая
                        Колонка +
                        +
                        + Правая
                        Колонка
                        ... +
                        +``` + +**Добавьте к правой колонке рамку `border-left` и отступ `padding-left`.** + +Двухколоночная вёрстка при этом не должна сломаться! + +Желательно не трогать свойство `width` ни слева ни справа и не создавать дополнительных элементов. + +Требуется поддержка всех современных браузеров, IE начиная с версии 8. \ No newline at end of file diff --git a/3-more/11-css-for-js/4-float/article.md b/3-more/11-css-for-js/4-float/article.md new file mode 100644 index 00000000..7e61ee77 --- /dev/null +++ b/3-more/11-css-for-js/4-float/article.md @@ -0,0 +1,468 @@ +# Свойство "float" + +Свойство `float` в CSS занимает особенное место. До его появления расположить два блока один слева от другого можно было лишь при помощи таблиц. Но в его работе есть ряд особенностей. Поэтому его иногда не любят, но при их понимании `float` станет вашим верным другом и помощником. + +Далее мы рассмотрим, как работает `float`, разберём решения сопутствующих проблем, а также ряд полезных рецептов. + +[cut] + +## Как работает float [#float-algorithm] + +Синтаксис: + +```css +float: left | right | none | inherit; +``` + +При применении этого свойства происходит следующее: + +
                          +
                        1. Элемент позиционируется как обычно, а затем *вынимается из документа потока* и сдвигается влево (для `left`) или вправо (для `right`) до того как коснётся либо границы родителя, либо другого элемента с `float`.
                        2. +
                        3. Если пространства по горизонтали не хватает для того, чтобы вместить элемент, то он сдвигается вниз до тех пор, пока не начнёт помещаться.
                        4. +
                        5. Другие непозиционированные блочные элементы без `float` ведут себя так, как будто элемента с `float` нет, так как он убран из потока.
                        6. +
                        7. Строки (inline-элементы), напротив, "знают" о `float` и обтекают элемент по сторонам.
                        8. +
                        + +Ещё детали: +
                          +
                        1. **Элемент при наличии `float` получает `display:block`.** То есть, указав элементу, у которого `display:inline` свойство `float: left/right`, мы автоматически сделаем его блочным. В частности, для него будут работать `width/height`. + +Исключением являются некоторые редкие `display` наподобие `inline-table` и `run-in` (см. [Relationships between 'display', 'position', and 'float'](http://www.w3.org/TR/CSS2/visuren.html#dis-pos-flo))
                        2. +
                        3. **Ширина `float`-блока определяется по содержимому.** (["CSS 2.1, 10.3.5"](http://www.w3.org/TR/CSS2/visudet.html#float-width)).
                        4. +
                        5. **Вертикальные отступы `margin` элементов с `float` не сливаются с отступами соседей,** в отличие от обычных блочных элементов.
                        6. +
                        + +Это пока только теория. Далее мы рассмотрим происходящее на примере. + +### Текст с картинками + +Одно из первых применений `float`, для которого это свойство когда-то было придумано -- это вёрстка текста с картинками, отжатыми влево или вправо. + +Например, вот страница о Винни-Пухе с картинками, которым поставлено свойство `float`: + + + +Её HTML-код ([edit src="winnie"]открыть[/edit]) выглядит примерно так: + +```html + +

                        Текст...

                        +

                        Текст...

                        + + +

                        Текст...

                        + + +

                        Текст...

                        +``` + +Каждая картинка, у которой есть `float`, обрабатывается в точности [по алгоритму](#float-algorithm), указанному выше. + +Посмотрим, например, как выглядело бы начало текста без float: + +[iframe src="winnie-nofloat" height=300 border=1 link edit] + + +
                          +
                        1. Элемент `IMG` вынимается из документа потока. Иначе говоря, последующие блоки начинают вести себя так, как будто его нет, и заполняют освободившееся место (изображение для наглядности полупрозрачно): + +[iframe src="winnie-nofloat-1" height=250 border=1 link edit] + +
                        2. +
                        3. Элемент `IMG` сдвигается максимально вправо(при `float:right`): + +[iframe src="winnie-nofloat-2" height=250 border=1 link edit] + +
                        4. +
                        5. Строки, в отличие от блочных элементов, "чувствуют" `float` и уступают ему место, обтекая картинку слева: + +[iframe src="winnie-nofloat-3" height=250 border=1 link edit] +
                        6. +
                        + +При `float:left` -- всё то же самое, только `IMG` смещается влево (или не смещается, если он и так у левого края), а строки -- обтекают справа + + +**Строки и инлайн-элементы смещаются, чтобы уступить место `float`. Обычные блоки -- ведут себя так, как будто элемента нет.** + +Чтобы это увидеть, добавим параграфам фон и рамку, а также сделаем изображение немного прозрачным: + +[iframe src="winnie-block-bg" height=300 border=1 link edit] + +Как видно из рисунка, параграфы проходят "за" `float`. При этом строки в них о `float'ах` знают и обтекают их, поэтому соответствующая часть параграфа пуста. + + +### Блок с float + +Свойство `float` можно поставить любому элементу, не обязательно картинке. При этом элемент станет блочным. + +Посмотрим, как это работает, на конкретной задаче -- сделать рамку с названием вокруг картинки с Винни. + +HTML будет такой: + +```html +

                        Винни-Пух

                        + +*!* +
                        + +
                        Кадр из советского мультфильма
                        +
                        +*/!* + +

                        Текст...

                        +``` + +..То есть, `div.left-picture` включает в себя картинку и заголовок к ней. Добавим стиль с `float`: + +```css +.left-picture { +*!* + float: left; +*/!* + + /* рамочка и отступ для красоты (не обязательно) */ + margin: 0 10px 5px 0; + text-align: center; + border: 1px solid black; +} +``` + +Результат: + +[iframe src="winnie-block" height=300 border=1 link edit] + +Заметим, что блок `div.left-picture` "обернул" картинку и текст под ней, а не растянулся на всю ширину. Это следствие того, что ширина блока с `float` определяется по содержимому. + +## Очистка под float + +Разберём еще одну особенность использования свойства `float`. + +Для этого выведем персонажей из мультфильма "Винни-Пух". Цель: + +[iframe src="winnie-clear-3" height=600 border=1 link edit] + +Реализуем её, шаг за шагом. + +### Шаг 1. Добавляем информацию + +Попробуем просто добавить Сову после Винни-Пуха: + +```html +

                        Винни-Пух

                        +
                        Картинка
                        +

                        ..Текст о Винни..

                        + +

                        Сова

                        +
                        Картинка
                        +

                        ..Текст о Сове..

                        +``` + +Результат [edit src="winnie-clear-1"]такого кода[/edit] будет странным, но предсказуемым: + +[iframe src="winnie-clear-1" border="1" height=500 link edit] + +Произошло следующее: +
                          +
                        • **Заголовок `

                          Сова

                          ` не заметил `float`** (он же блочный элемент) и расположился сразу после предыдущего параграфа `

                          ..Текст о Винни..

                          `.
                        • +
                        • После него идёт `float`-элемент -- картинка "Сова". Он был сдвинут влево. Согласно [алгоритму](#float-algorithm), он двигается до левой границы или до касания с другим `float`-элементом, что и произошло (картинка "Винни-Пух").
                        • +
                        • Так как у совы `float:left`, то **последующий текст обтекает её справа**.
                        • +
                        + +### Шаг 2. Свойство clear + +Мы, конечно же, хотели бы расположить заголовок "Сова" и остальную информацию ниже Винни-Пуха. + +Для решения возникшей проблемы придумано свойство `clear`. + +Синтаксис: + +```css +clear: left | right | both; +``` + +Применение этого свойства сдвигает элемент вниз до тех пор, пока не закончатся `float'ы` слева/справа/с обеих сторон. + +Применим его к заголовку `H2`: + +```css +h2 { + clear: left; +} +``` + +Результат [edit src="winnie-clear-2"]получившегося кода[/edit] будет ближе к цели, но всё еще не идеален: + + + +Элементы теперь в нужном порядке. Но куда пропал отступ `margin-top` у заголовка "Сова"? + +Теперь заголовок "Сова" прилегает снизу почти вплотную к картинке, с учётом её `margin-bottom`, но без своего большого отступа `margin-top`. + +Таково поведение свойства `clear`. Оно сдвинуло элемент `h2` вниз ровно настолько, чтобы элементов `float` не было *сбоку от его верхней границы*. + +Если посмотреть на элемент заголовка внимательно в инструментах разработчика, то можно заметить отступ `margin-top` у заголовка по-прежнему есть, но он располагается "за" элементом `float` и не учитывается при работе в `clear`. + +Чтобы исправить ситуацию, можно добавить перед заголовком пустой промежуточный элемент без отступов, с единственным свойством `clear:both`. Тогда уже под ним отступ заголовка будет работать нормально: + +```html +

                        Винни-Пух

                        +
                        Картинка
                        +

                        Текст

                        + +*!* +
                        +*/!* + +

                        Сова

                        +
                        Картинка
                        +

                        Текст

                        +``` + +Результат [edit src="winnie-clear-3"]получившегося кода[/edit]: + +[iframe src="winnie-clear-3" border="1" height=600 link edit] + +
                          +
                        • Свойство `clear` гарантировало, что `
                          ` будет под картинкой с `float`.
                        • +
                        • Заголовок `

                          Сова

                          ` идёт после этого `
                          `. Так что его отступ учитывается.
                        • +
                        + +## Заполнение блока-родителя + +Итак, мы научились располагать другие элементы *под* `float`. Теперь рассмотрим следующую особенность. + +**Из-за того, что блок с `float` удалён из потока, родитель не выделяет под него места.** + +Например, выделим для информации о Винни-Пухе красивый элемент-контейнер `
                        `: + +```html +
                        + +

                        Винни-Пух

                        + +
                        Картинка
                        + +

                        Текст.

                        +
                        +``` + +Стиль контейнера: + +```css +.hero { + background: #D2B48C; + border: 1px solid red; +} +``` + +Результат [edit src="winnie-clear-4"]получившегося кода[/edit]: + +[iframe src="winnie-clear-4" border="1" height=300 link edit] + +Элемент с `float` оказался выпавшим за границу родителя `.hero`. + +Чтобы этого не происходило, используют одну из следующих техник. + +### Поставить родителю float + +Элемент с `float` обязан расшириться, чтобы вместить вложенные `float`. + +Поэтому, если это допустимо, то установка `float` контейнеру всё исправит: + +```css +.hero { + background: #D2B48C; + border: 1px solid red; + *!* + float: left; + */!* +} +``` + +[iframe src="winnie-clearfill-float" border="1" height=300 link edit] + +Разумеется, не всегда можно поставить родителю `float`, так что смотрим дальше. + +### Добавить в родителя элемент с clear + +Добавим элемент `div style="clear:both"` в самый конец контейнера `.hero`. + +Он с одной стороны будет "нормальным" элементом, в потоке, и контейнер будет обязан выделить под него пространство, с другой -- он знает о `float` и сместится вниз. + +Соответственно, и контейнер вырастет в размере: + +```html +
                        + +

                        Винни-Пух

                        + +
                        Картинка
                        + +

                        Текст.

                        + +*!* +
                        +*/!* +
                        +``` + +Результат -- правильное отображение, как и в примере выше. [edit src="winnie-clearfill-div"]Открыть код[/edit]. + +Единственный недостаток этого метода -- лишний HTML-элемент в разметке. + +### Универсальный класс clearfix + +Чтобы не добавлять в HTML-код лишний элемент, можно задать его через `:after`. + +```css +.clearfix:after { + content: "."; /* добавить содержимое: "." */ + display: block; /* сделать блоком, т.к. inline не может иметь clear */ + clear: both; /* с обеих сторон clear */ + visibility: hidden; /* сделать невидимым, зачем нам точка внизу? */ + height: 0; /* сделать высоту 0, чтобы не занимал место */ +} +``` + +Добавив этот класс к родителю, получим тот же результат, что и выше. [edit src="winnie-clearfill-clearfix"]Открыть код[/edit]. + +Псевдоселектор `:after` не поддерживается в IE<8, но для старых IE сработает другое свойство: + +```css +.clearfix { + zoom: 1; /* спец-свойство IE */ +} +``` + +### overflow:auto/hidden + +Если добавить родителю `overflow: hidden` или `overflow: auto`, то всё станет хорошо. + +```css +.hero { +*!* + overflow: auto; +*/!* +} +``` + +Этот метод работает во всех браузерах. [edit src="winnie-clearfill-overflow"]Открыть код[/edit]. + +Несмотря на внешнюю странность, этот способ не является "хаком". Такое поведение прописано в спецификации CSS. + +Однако, установка `overflow` может привести к появлению полосы прокрутки, способ с дополнительным элементом (или `.clearfix:after`, если без IE<8) более безопасен. + + +## Еще применения float + +Одно применение -- для верстки текста, мы уже видели. Рассмотрим ещё несколько. + +### Галерея + +При помощи `float` можно сделать галерею изображений: + +```html + +``` + +Стиль: + +```css +.gallery li { + float: left; + width: 130px; + list-style: none; +} +``` + +Элементы `float:left` двигаются влево, а если это невозможно, то вниз, автоматически адаптируясь под ширину контейнера. + +Результат: + +[iframe src="gallery-float" border="1" height=550 link edit] + + + +### Вёрстка в несколько колонок + +Свойство `float` позволяет делать несколько вертикальных колонок. + +### float:left + float:right + +Например, для вёрстки в две колонки можно сделать два `
                        `. Первому указать `float:left` (левая колонка), а второму -- `float:right` (правая колонка). + +Чтобы они не ссорились, каждой колонке нужно дополнительно указать ширину: + +```html +
                        Шапка
                        +
                        Левая колонка
                        +
                        Правая колонка
                        + +``` + +Стили: + +```css +.column-left { + float: left; + width: 30%; +} + +.column-right { + float: left; + width: 70%; +} + +.footer { + clear: both; +} +``` + +Результат (добавлены краски): +[iframe src="two-columns" border="1" height=440 link edit] + +В эту структуру легко добавить больше колонок с разной шириной. Правой колонке можно было бы указать и `float:right`. + +### float + margin + +Ещё вариант -- сделать `float` для левой колонки, а правую оставить в потоке, но с отбивкой через `margin`: + +```css +.column-left { + float: left; + width: 30%; +} + +.column-right { + margin-left: 30%; +} + +.footer { + clear: both; +} +``` + +Результат (добавлены краски): +[iframe src="two-columns-2" border="1" height=440 link edit] + +В примере выше -- показана небольшая проблема. Колонки не растягиваются до одинаковой высоты. Конечно, это не имеет значения, если фон одинаковый, но что, если он разный? + +Технически, нет кросс-браузерного CSS-способа сделать, чтобы они растягивались. Есть различные обходы и трюки, которые позволяют обойти проблему в ряде ситуаций, но они выходят за рамки нашего обсуждения. Если интересно -- посмотрите, например, [Faux Columns](http://goodline.spb.ru/III-05-002.html). + +Техник построения структуры страницы и колонок ("CSS layout") очень много. Более подробно вы можете с ними познакомиться в книгах и продвинутых статьях, либо просто придумать самому на основе того, как работает `float`. + + + diff --git a/3-more/11-css-for-js/4-float/float-small-margin.png b/3-more/11-css-for-js/4-float/float-small-margin.png new file mode 100755 index 00000000..bf2c7b92 Binary files /dev/null and b/3-more/11-css-for-js/4-float/float-small-margin.png differ diff --git a/3-more/11-css-for-js/4-float/gallery-float.view/index.html b/3-more/11-css-for-js/4-float/gallery-float.view/index.html new file mode 100755 index 00000000..aadb464c --- /dev/null +++ b/3-more/11-css-for-js/4-float/gallery-float.view/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + diff --git a/3-more/11-css-for-js/4-float/text.png b/3-more/11-css-for-js/4-float/text.png new file mode 100755 index 00000000..30e0f650 Binary files /dev/null and b/3-more/11-css-for-js/4-float/text.png differ diff --git a/3-more/11-css-for-js/4-float/two-columns-2.view/index.html b/3-more/11-css-for-js/4-float/two-columns-2.view/index.html new file mode 100755 index 00000000..765499a1 --- /dev/null +++ b/3-more/11-css-for-js/4-float/two-columns-2.view/index.html @@ -0,0 +1,67 @@ + + + + + + + + +
                        Шапка
                        + +
                        +
                        +

                        Персонажи:

                        +
                          +
                        • Винни-Пух
                        • +
                        • Ослик Иа
                        • +
                        • Сова
                        • +
                        • Кролик
                        • +
                        +
                        +
                        +
                        +
                        + +

                        Винни-Пух

                        + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + +
                        +
                        + + + + + + + + diff --git a/3-more/11-css-for-js/4-float/two-columns.view/index.html b/3-more/11-css-for-js/4-float/two-columns.view/index.html new file mode 100755 index 00000000..52f935b4 --- /dev/null +++ b/3-more/11-css-for-js/4-float/two-columns.view/index.html @@ -0,0 +1,66 @@ + + + + + + + + +
                        Шапка
                        + +
                        +
                        +

                        Персонажи:

                        +
                          +
                        • Винни-Пух
                        • +
                        • Ослик Иа
                        • +
                        • Сова
                        • +
                        • Кролик
                        • +
                        +
                        +
                        + +
                        +
                        +

                        Винни-Пух

                        + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + +
                        +
                        + + + + + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-block-bg.view/index.html b/3-more/11-css-for-js/4-float/winnie-block-bg.view/index.html new file mode 100755 index 00000000..ad28d95c --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-block-bg.view/index.html @@ -0,0 +1,36 @@ + + + + + + + + +

                        Винни-Пух

                        + + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-block.view/index.html b/3-more/11-css-for-js/4-float/winnie-block.view/index.html new file mode 100755 index 00000000..f3d2fc59 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-block.view/index.html @@ -0,0 +1,34 @@ + + + + + + + + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clear-1.view/index.html b/3-more/11-css-for-js/4-float/winnie-clear-1.view/index.html new file mode 100755 index 00000000..6db2d8d9 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clear-1.view/index.html @@ -0,0 +1,50 @@ + + + + + + + + + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        + +

                        Сова

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Персонаж мультфильма про Винни-Пуха. Очень умная.

                        + +

                        Говорит редко, но чрезвычайно мудро.

                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clear-2.view/index.html b/3-more/11-css-for-js/4-float/winnie-clear-2.view/index.html new file mode 100755 index 00000000..b756e5fe --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clear-2.view/index.html @@ -0,0 +1,51 @@ + + + + + + + + + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        + +

                        Сова

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Персонаж мультфильма про Винни-Пуха. Очень умная.

                        + +

                        Говорит редко, но чрезвычайно мудро.

                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clear-3.view/index.html b/3-more/11-css-for-js/4-float/winnie-clear-3.view/index.html new file mode 100755 index 00000000..e2623e8e --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clear-3.view/index.html @@ -0,0 +1,51 @@ + + + + + + + + + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        + +
                        +

                        Сова

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Персонаж мультфильма про Винни-Пуха. Очень умная.

                        + +

                        Говорит редко, но чрезвычайно мудро.

                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clear-4.view/index.html b/3-more/11-css-for-js/4-float/winnie-clear-4.view/index.html new file mode 100755 index 00000000..60c6d5bf --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clear-4.view/index.html @@ -0,0 +1,41 @@ + + + + + + + + +
                        + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        +
                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clearfill-clearfix.view/index.html b/3-more/11-css-for-js/4-float/winnie-clearfill-clearfix.view/index.html new file mode 100755 index 00000000..a2705e04 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clearfill-clearfix.view/index.html @@ -0,0 +1,55 @@ + + + + + + + + +
                        + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        +
                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clearfill-div.view/index.html b/3-more/11-css-for-js/4-float/winnie-clearfill-div.view/index.html new file mode 100755 index 00000000..bde4e0e6 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clearfill-div.view/index.html @@ -0,0 +1,43 @@ + + + + + + + + +
                        + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        +
                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clearfill-float.view/index.html b/3-more/11-css-for-js/4-float/winnie-clearfill-float.view/index.html new file mode 100755 index 00000000..bde4e0e6 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clearfill-float.view/index.html @@ -0,0 +1,43 @@ + + + + + + + + +
                        + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        +
                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-clearfill-overflow.view/index.html b/3-more/11-css-for-js/4-float/winnie-clearfill-overflow.view/index.html new file mode 100755 index 00000000..2f1b5fdb --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-clearfill-overflow.view/index.html @@ -0,0 +1,45 @@ + + + + + + + + +
                        + +

                        Винни-Пух

                        + +
                        + +
                        Кадр из советского мультфильма
                        +
                        + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге).

                        + +

                        Один из самых известных героев детской литературы XX века.

                        +
                        + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-nofloat-1.view/index.html b/3-more/11-css-for-js/4-float/winnie-nofloat-1.view/index.html new file mode 100755 index 00000000..a1fbf5bb --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-nofloat-1.view/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-nofloat-2.view/index.html b/3-more/11-css-for-js/4-float/winnie-nofloat-2.view/index.html new file mode 100755 index 00000000..9623c884 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-nofloat-2.view/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-nofloat-3.view/index.html b/3-more/11-css-for-js/4-float/winnie-nofloat-3.view/index.html new file mode 100755 index 00000000..fcb3f50e --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-nofloat-3.view/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + + + + diff --git a/3-more/11-css-for-js/4-float/winnie-nofloat.view/index.html b/3-more/11-css-for-js/4-float/winnie-nofloat.view/index.html new file mode 100755 index 00000000..4e3861cb --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie-nofloat.view/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + + + + diff --git a/3-more/11-css-for-js/4-float/winnie.view/index.html b/3-more/11-css-for-js/4-float/winnie.view/index.html new file mode 100755 index 00000000..67123ea4 --- /dev/null +++ b/3-more/11-css-for-js/4-float/winnie.view/index.html @@ -0,0 +1,47 @@ + + + + + + + + +

                        Винни-Пух

                        + + + +

                        Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

                        + +

                        В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

                        + + + +

                        Первый перевод «Винни-Пуха» в СССР вышел в 1958 году в Литве (лит. Mikė Pūkuotukas), его выполнил 20-летний литовский писатель Виргилюс Чепайтис, пользовавшийся польским переводом Ирены Тувим. Впоследствии Чепайтис, познакомившись с английским оригиналом, существенно переработал свой перевод, переиздававшийся в Литве неоднократно.

                        + +

                        История Винни-Пуха в России начинается с того же 1958 года, когда с книгой познакомился Борис Владимирович Заходер.

                        + + + + +

                        В студии «Союзмультфильм» под руководством Фёдора Хитрука было создано три мультфильма.

                        + +

                        Сценарий написал Хитрук в соавторстве с Заходером; работа соавторов не всегда шла гладко, что стало в конечном счёте причиной прекращения выпуска мультфильмов (первоначально планировалось выпустить сериал по всей книге). Текст и картинки взяты с Wikipedia.

                        + + + + diff --git a/3-more/11-css-for-js/5-position/1-modal-window/solution.md b/3-more/11-css-for-js/5-position/1-modal-window/solution.md new file mode 100644 index 00000000..2f7da85d --- /dev/null +++ b/3-more/11-css-for-js/5-position/1-modal-window/solution.md @@ -0,0 +1,20 @@ +Если использовать `position: absolute`, то `DIV` не растянется на всю высоту документа, т.к. координаты вычисляются *относительно окна*. + +Можно, конечно, узнать эту высоту при помощи JavaScript, но CSS даёт более удобный способ. Будем использовать `position:fixed`: + +Стиль: + +```css +#box { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 999; +} +``` + +Свойство `z-index` должно превосходить все другие элементы управления, чтобы они перекрывались. + +[edit src="solution"]Полный текст решения[/edit] \ No newline at end of file diff --git a/3-more/11-css-for-js/5-position/1-modal-window/solution.view/index.html b/3-more/11-css-for-js/5-position/1-modal-window/solution.view/index.html new file mode 100755 index 00000000..b01580fe --- /dev/null +++ b/3-more/11-css-for-js/5-position/1-modal-window/solution.view/index.html @@ -0,0 +1,45 @@ + + + + + + + + + +
                        + + + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + + + + + + diff --git a/3-more/11-css-for-js/5-position/1-modal-window/source.view/index.html b/3-more/11-css-for-js/5-position/1-modal-window/source.view/index.html new file mode 100755 index 00000000..043e8083 --- /dev/null +++ b/3-more/11-css-for-js/5-position/1-modal-window/source.view/index.html @@ -0,0 +1,39 @@ + + + + + + + + + +
                        + + + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + +

                        Текст Текст Текст

                        + + + + + + diff --git a/3-more/11-css-for-js/5-position/1-modal-window/task.md b/3-more/11-css-for-js/5-position/1-modal-window/task.md new file mode 100644 index 00000000..73816ebb --- /dev/null +++ b/3-more/11-css-for-js/5-position/1-modal-window/task.md @@ -0,0 +1,17 @@ +# Модальное окно + +[importance 5] + +Создайте при помощи HTML/CSS "модальное окно", то есть `DIV`, который полностью перекрывает документ и находится над ним. + +При этом все элементы управления на документе перестают работать, т.к. клики попадают в `DIV`. + +В примере ниже `DIV'у` дополнительно поставлен цвет фона и прозрачность, чтобы было видно перекрытие: + +[iframe height=150 src="solution" border=1 link] + +Браузеры: все основные, IE8+. Должно работать при прокрутке окна (проверьте). + +[edit src="source" task/] + + diff --git a/3-more/11-css-for-js/5-position/article.md b/3-more/11-css-for-js/5-position/article.md new file mode 100644 index 00000000..6ae87934 --- /dev/null +++ b/3-more/11-css-for-js/5-position/article.md @@ -0,0 +1,374 @@ +# Свойство "position" + +Свойство `position` позволяет сдвигать элемент со своего обычного места. Цель этой главы -- не только напомнить, как оно работает, но и разобрать ряд частых заблуждений и граблей. +[cut] +## position: static + +*Статическое позиционирование* производится по умолчанию, в том случае, если свойство `position` не указано. + +Его можно также явно указать через CSS-свойство: + +```css +position: static; +``` + +Такая запись встречается редко и используется для переопределения других значений `position`. + +Здесь и далее, для примеров мы будем использовать следующий документ: + +```html + +
                        + Без позиционирования ("position: static"). + +

                        Заголовок

                        + +
                        А тут - всякий разный текст...
                        + ... В две строки!
                        +
                        +``` + +В этом документе сейчас все элементы отпозиционированы статически, то есть никак. + +[summary] +Элемент с `position: static` еще называют *не позиционированым*. +[/summary] + + +## position: relative + +*Относительное позиционирование* сдвигает элемент относительно его обычного положения. + +Для того, чтобы применить относительное позиционирование, необходимо указать элементу CSS-свойство `position: relative` и координаты `left/right/top/bottom`. + +Этот стиль сдвинет элемент на 10 пикселей относительно обычной позиции по вертикали: + +```css +position: relative; +top: 10px; +``` + + + +```html + +*!* + +*/!* + +
                        + Заголовок сдвинут на 10px вниз. + +

                        Заголовок

                        + +
                        А тут - всякий разный текст...
                        + ... В две строки!
                        +
                        +``` + +### Координаты + +Для сдвига можно использовать координаты: + +
                          +
                        • `top` - сдвиг от "обычной" верхней границы
                        • +
                        • `bottom` - сдвиг от нижней границы
                        • +
                        • `left` - сдвиг слева
                        • +
                        • `right` - сдвиг справа
                        • +
                        + +Не будут работать одновременно указанные `top` и `bottom`, `left` и `right`. Нужно использовать только одну границу из каждой пары. + +**Возможны отрицательные координаты** и координаты, использующие другие единицы измерения. Например, `left: 10%` сдвинет элемент на 10% его ширины вправо, а `left: -10%` -- влево. При этом часть элемента может оказаться за границей окна: + +```html + +*!* + +*/!* + +
                        + Заголовок сдвинут на 10% влево. + +

                        Заголовок

                        + +
                        А тут - всякий разный текст...
                        + ... В две строки!
                        +
                        +``` + +**Свойства `left/top` не будут работать для `position:static`**. Если их все же поставить, браузер их проигнорирует. Эти свойства предназначены для работы только с позиционированными элементами. + +## position: absolute + +Синтаксис: + +```css +position: absolute; +``` + +Абсолютное позиционирование делает две вещи: +
                          +
                        1. **Элемент исчезает с того места, где он должен быть и позиционируется заново.** Остальные элементы, располагаются так, как будто этого элемента никогда не было.
                        2. +
                        3. **Координаты `top/bottom/left/right` для нового местоположения отсчитываются от ближайшего позиционированного родителя**, т.е. родителя с позиционированием, отличным от `static`. Если такого родителя нет -- то относительно документа.
                        4. +
                        + +Кроме того: +
                          +
                        • **Ширина элемента с `position: absolute` устанавливается по содержимому.** Детали алгоритма вычисления ширины [описаны в стандарте](http://www.w3.org/TR/CSS2/visudet.html#abs-non-replaced-width).
                        • +
                        • **Элемент получает `display:block`**, который перекрывает почти все возможные `display` (см. [Relationships between 'display', 'position', and 'float'](http://www.w3.org/TR/CSS2/visuren.html#dis-pos-flo)).
                        • +
                        + +Например, отпозиционируем заголовок в правом-верхнем углу документа: + +```html + +*!* + +*/!* + +
                        + Заголовок в правом-верхнем углу документа. + +

                        Заголовок

                        + +
                        А тут - всякий разный текст...
                        + ... В две строки!
                        +
                        +``` + +Важное отличие от `relative`: **так как элемент удаляется со своего обычного места, то элементы под ним сдвигаются, занимая освободившееся пространство**. Это видно в примере выше: строки идут одна за другой. + +Так как при `position:absolute` размер блока устанавливается по содержимому, то +широкий `Заголовок` "съёжился" до прямоугольника в углу. + +Иногда бывает нужно поменять элементу `position` на `absolute`, но так, чтобы элементы вокруг не сдвигались. Как правило это делают, меняя соседей -- добавляют `margin/padding` или вставляют в документ пустой элемент с такими же размерами. + + +[smart header="Одновременное указание `left/right`, `top/bottom`"] + +**В абсолютно позиционированном элементе можно одновременно задавать противоположные границы.** + +Браузер растянет такой элемент до границ. Работает везде, кроме IE6: + +```html + + +
                        10px от границ
                        +``` + +[/smart] + + +[smart header="Внешним блоком является окно"] +Как растянуть абсолютно позиционированный блок на всю ширину документа? + +Первое, что может прийти в голову: + +```css +div { + position: absolute; + left: 0; top: 0; /* в левый-верхний угол */ + width: 100%; height: 100%; /* .. и растянуть */ +} +``` + +Но это будет работать лишь до тех пор, пока у страницы не появится скроллинг! + +Прокрутите вниз ифрейм: +[iframe src="position-100-wrong" height=200 link] + +**Вы увидите, что голубой фон оканчивается задолго до конца документа.** + +Дело в том, что в CSS `100%` относится к ширине внешнего блока ("containing block"). А какой внешний блок имеется в виду здесь, ведь элемент изъят со своего обычного места? + +В данном случае им является так называемый (["\"initial containing block\""](http://www.w3.org/TR/CSS21/visudet.html#containing-block-details)), которым является окно, *а не документ*. + +**То есть, координаты и ширины вычисляются относительно окна, а не документа.** + +Может быть, получится так? + +```css +div { + position: absolute; + left: 0; top: 0; /* в левый-верхний угол, и растянуть.. */ + right: 0; bottom: 0; /* ..указанием противоположных границ */ +} +``` + +С виду логично, но нет, не получится! + +Координаты `top/right/left/bottom` вычисляются относительно *окна*. Значение `bottom: 0` -- нижняя граница окна, а не документа, блок растянется до неё. То есть, будет то же самое, что и в предыдущем примере. + +[/smart] + + +## position: absolute в позиционированном родителе + +Если у элемента есть позиционированный предок, то `position: absolute` работает относительно него, а не относительно документа. + +То есть, достаточно поставить родительскому `div` позицию `relative`, даже без координат -- и заголовок будет в его правом-верхнем углу, вот так: + +```html + +*!* + +*/!* + +
                        + Заголовок в правом-верхнем углу DIV'а. + +

                        Заголовок

                        + +
                        А тут - всякий разный текст...
                        + ... В две строки!
                        +
                        +``` + +Нужно пользоваться таким позиционированием с осторожностью, т.к оно может перекрыть текст. Этим оно отличается от `float`. + +Сравните: +
                          +
                        • Используем `position` для размещения элемента управления: + +```html + + +1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 +``` + +**Часть текста перекрывается.** Кнопка более не участвует в потоке. +
                        • +
                        • Используем `float` для размещения элемента управления: + +```html + + +1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 +1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 +``` + +**Браузер освобождает место справа, текст перенесён.** Кнопка продолжает находиться в потоке, просто сдвинута. +
                        • +
                        + + + +## position: fixed + +Это подвид абсолютного позиционирования. + +Синтаксис: + +```css +position: fixed; +``` + +Позиционирует объект точно так же, как `absolute`, но относительно `window`. + +Разница в нескольких словах: + +**Когда страницу прокручивают, фиксированный элемент остается на своем месте и не прокручивается вместе со страницей.** + +В следующем примере, при прокрутке документа, ссылка `#top` всегда остается на своем месте. + +```html + + + +*!*Наверх (остается при прокрутке)*/!* + +Фиксированное позиционирование. + +

                        Текст страницы.. Прокрути меня...

                        +

                        Много строк..

                        Много строк..

                        +

                        Много строк..

                        Много строк..

                        +

                        Много строк..

                        Много строк..

                        +

                        Много строк..

                        Много строк..

                        +``` + +Поддерживается во всех современных браузерах, в IE начиная с версии 7. Для реализации аналогичного функционала в IE6 используют CSS-выражения. + + +## Итого + +Виды позиционирования и их особенности. + +
                        +
                        `static`
                        +
                        Иначе называется "без позиционирования". В явном виде задаётся только если надо переопределить другое правило CSS.
                        +
                        `relative`
                        +
                        Сдвигает элемент относительно текущего места. + +
                          +
                        • Противоположные границы `left/right` (`top/bottom`) одновременно указать нельзя.
                        • +
                        • Окружающие элементы ведут себя так, как будто элемент не сдвигался.
                        • +
                        +
                        +
                        `absolute`
                        +
                        Визуально переносит элемент на новое место. + +Новое место вычисляется по координатам `left/top/right/bottom` относительно ближайшего позиционированного родителя. Если такого родителя нет, то им считается окно. + +
                          +
                        • Ширина элемента по умолчанию устанавливается по содержимому.
                        • +
                        • Можно указать противположные границы `left/right` (`top/bottom`). Элемент растянется. Возможность не поддерживается в IE<8.
                        • +
                        • Окружающие элементы заполняют освободившееся место.
                        • +
                        + +
                        +
                        `fixed`
                        +
                        Подвид абсолютного позиционирования, при котором элемент привязывается к координатам окна, а не документа. + +При прокрутке он остаётся на том же месте.
                        +
                        + +## Почитать + +CSS-позиционирование по-настоящему глубоко в спецификации Visual Formatting Model, 9.3 и ниже. + +Еще есть хорошее руководство CSS Positioning in 10 steps, которое охватывает основные типы позиционирования. + diff --git a/3-more/11-css-for-js/5-position/position-100-wrong.view/index.html b/3-more/11-css-for-js/5-position/position-100-wrong.view/index.html new file mode 100755 index 00000000..4f346b24 --- /dev/null +++ b/3-more/11-css-for-js/5-position/position-100-wrong.view/index.html @@ -0,0 +1,17 @@ + + + + + + + +
                        Прокрутите меня...
                        + + + diff --git a/3-more/11-css-for-js/6-css-center/1-center-ball-css/solution.md b/3-more/11-css-for-js/6-css-center/1-center-ball-css/solution.md new file mode 100644 index 00000000..aaf9b801 --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/1-center-ball-css/solution.md @@ -0,0 +1,13 @@ +Сместим мяч в центр при помощи `left/top=50%`, а затем приподымем его указанием `margin`: + +```css +#ball { + position: absolute; + left: 50%; + top: 50%; + margin-left: -20px; + margin-top: -20px; +} +``` + +[edit src="solution"]Полный код решения[/edit] \ No newline at end of file diff --git a/3-more/11-css-for-js/6-css-center/1-center-ball-css/solution.view/index.html b/3-more/11-css-for-js/6-css-center/1-center-ball-css/solution.view/index.html new file mode 100755 index 00000000..5e6eb2d4 --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/1-center-ball-css/solution.view/index.html @@ -0,0 +1,31 @@ + + + + + + + + +
                        + +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
                        + + + + diff --git a/3-more/11-css-for-js/6-css-center/1-center-ball-css/source.view/index.html b/3-more/11-css-for-js/6-css-center/1-center-ball-css/source.view/index.html new file mode 100755 index 00000000..99a6d4fc --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/1-center-ball-css/source.view/index.html @@ -0,0 +1,27 @@ + + + + + + + + +
                        + +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +
                        + + + + diff --git a/3-more/11-css-for-js/6-css-center/1-center-ball-css/task.md b/3-more/11-css-for-js/6-css-center/1-center-ball-css/task.md new file mode 100644 index 00000000..cf748928 --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/1-center-ball-css/task.md @@ -0,0 +1,20 @@ +# Поместите мяч в центр поля (CSS) + +[importance 5] + +Поместите мяч в центр поля при помощи CSS. + +Исходный код: +[iframe height=200 src="source"] + +Используйте CSS, чтобы поместить мяч в центр: +[iframe height=200 src="solution"] + +
                          +
                        • CSS для центрирования может использовать размеры мяча.
                        • +
                        • CSS для центрирования не должен опираться на конкретный размер поля.
                        • +
                        + +[edit src="source"]Исходный документ[/edit]. + + diff --git a/3-more/11-css-for-js/6-css-center/2-form-modal/solution.md b/3-more/11-css-for-js/6-css-center/2-form-modal/solution.md new file mode 100644 index 00000000..b47bedb5 --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/2-form-modal/solution.md @@ -0,0 +1,12 @@ +# Структура решения + +Шаги решения: +
                        1. +Для того, чтобы элементы окна не работали, их нужно перекрыть `DIV'ом` с большим `z-index`.
                        2. +
                        3. Внутри него будет лежать "экран" с полупрозрачностью. Чтобы он растягивался, можно дать ему `position: absolute` и указать все координаты `top/left/right/bottom`. Это работает в IE8+.
                        4. +
                        5. Форму можно отцентрировать при помощи `margin` или `display: table-cell` + `vertical-align` на внешнем `DIV`.
                        6. +
                        + +# Решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/3-more/11-css-for-js/6-css-center/2-form-modal/solution.view/index.html b/3-more/11-css-for-js/6-css-center/2-form-modal/solution.view/index.html new file mode 100755 index 00000000..efe533ac --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/2-form-modal/solution.view/index.html @@ -0,0 +1,69 @@ + + + + + + + + +

                        Текст Текст Текст

                        + + + +

                        Текст Текст Текст

                        + + +
                        +
                        +
                        + +
                        + Добро пожаловать! + + + + +
                        Логин
                        Пароль
                        + +
                        + +
                        + + + + + diff --git a/3-more/11-css-for-js/6-css-center/2-form-modal/source.view/index.html b/3-more/11-css-for-js/6-css-center/2-form-modal/source.view/index.html new file mode 100755 index 00000000..28ca5e6f --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/2-form-modal/source.view/index.html @@ -0,0 +1,18 @@ + + + + + + + +

                        Текст Текст Текст

                        + + + +

                        Текст Текст Текст

                        + + + + + + diff --git a/3-more/11-css-for-js/6-css-center/2-form-modal/task.md b/3-more/11-css-for-js/6-css-center/2-form-modal/task.md new file mode 100644 index 00000000..a60e9b1f --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/2-form-modal/task.md @@ -0,0 +1,23 @@ +# Форма + модальное окно + +[importance 5] + +Создайте при помощи HTML/CSS форму для логина в модальном окне. + +[iframe height=280 src="solution" border=1 link] + +Требования: + +
                          +
                        • Кнопки окна вне формы не работают (даже на левый край нажать нельзя).
                        • +
                        • Полупрозрачный голубой "экран" отстоит от границ на `20px`.
                        • +
                        • Форма центрирована вертикально и горизонтально, её размеры фиксированы.
                        • +
                        • Посетитель может менять размер окна браузера, геометрия должна сохраняться.
                        • +
                        • Не ломается при прокрутке.
                        • +
                        + +Браузеры: все основные, IE8+. + +[edit src="source" task/] + + diff --git a/3-more/11-css-for-js/6-css-center/3-vertical-align-table-cell-position/solution.md b/3-more/11-css-for-js/6-css-center/3-vertical-align-table-cell-position/solution.md new file mode 100644 index 00000000..1e255207 --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/3-vertical-align-table-cell-position/solution.md @@ -0,0 +1,29 @@ +# Подсказка + +Центрирование не работает из-за `position: absolute`. + +# Решение + +Центрирование не работает потому, что `position: absolute` автоматически меняет элементу `display` на `block`. + +В однострочном случае можно сделать центрирование при помощи `line-height`: + +```html + + + +
                        «
                        +``` + +Если же центрировать нужно несколько строк или блок, то есть и другие [техники центрирования](/css-center), которые сработают без `display:table-cell`. \ No newline at end of file diff --git a/3-more/11-css-for-js/6-css-center/3-vertical-align-table-cell-position/task.md b/3-more/11-css-for-js/6-css-center/3-vertical-align-table-cell-position/task.md new file mode 100644 index 00000000..6f532d1f --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/3-vertical-align-table-cell-position/task.md @@ -0,0 +1,28 @@ +# vertical-align + table-cell + position = ? + +[importance 5] + +В коде ниже используется вертикальное центрирование при помощи `table-cell + vertical-align`. + +Почему оно не работает? Нажмите на просмотр, чтобы увидеть (стрелка должна быть в центре по вертикали). + +```html + + + +
                        «
                        +``` + +Как починить центрирование при помощи CSS? Свойства `position/height` менять нельзя. \ No newline at end of file diff --git a/3-more/11-css-for-js/6-css-center/article.md b/3-more/11-css-for-js/6-css-center/article.md new file mode 100644 index 00000000..48cbd1df --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/article.md @@ -0,0 +1,415 @@ +# Центрирование горизонтальное и вертикальное + +В CSS есть всего несколько техник центрирования элементов. Если их знать, то большинство задач решаются просто. +[cut] +## Горизонтальное + +### text-align + +Для центрирования инлайновых элементов -- достаточно поставить родителю `text-align: center`.: + +```html + + + +
                        Текст
                        +``` + +Для центрирования блока это уже не подойдёт, свойство просто не подействует. Например: + +```html + + + +
                        +
                        Текст
                        +
                        +``` + +Впрочем, в примере выше блок будет центрирован в IE6,7 (это отклонение от стандарта). + +### margin: auto + +Для всех браузеров, кроме IE6,7, блок по горизонтали центрируется `margin: auto`.: + +```html + + + +
                        +
                        Текст
                        +
                        +``` + +В отличие от `width/height`, значение `auto` для `margin` само не появляется. Обычно `margin` равно конкретной величине для элемента, например `0` для `DIV`. Нужно поставить его явно. + +Значение `margin-left:auto/margin-right:auto` заставляет браузер выделять под `margin` всё доступное сбоку пространство. А если и то и другое `auto`, то слева и справа будет одинаковый отступ, таким образом элемент окажется в середине. Детали вычислений описаны в разделе спецификации [Calculating widths and margins](http://www.w3.org/TR/CSS21/visudet.html#Computing_widths_and_margins). + +## Вертикальное + +Для горизонтального центрирования всё просто. Вертикальное же изначальное не было предусмотрено в спецификации CSS и по сей день вызывает ряд проблем. + +Есть три основных решения. + +### position:absolute + margin + +Центрируемый элемент позиционируем абсолютно и опускаем до середины по вертикали при помощи `top:50%`: + +```html + + + +
                        +
                        Текст
                        +
                        +``` + +Это, конечно, не совсем центр. По центру находится верхняя граница. Нужно ещё приподнять элемент на половину своей высоты. + +**Высота центрируемого элемента должна быть известна.** Родитель может иметь любую высоту. + +Если мы знаем, что это ровно одна строка, то её высота равна `line-height`. + +**Приподнимем элемент на пол-высоты при помощи `margin-top`:** + +```html + + + +
                        +
                        Текст
                        +
                        +``` + +[hide text="Почему -0.625em?"] +При стандартных настройках браузера высота строки `line-height: 1.25`, если поделить на два `1.25em / 2 = 0.625em`. + +Конечно, высота может быть и другой, главное чтобы мы её знали заранее. +[/hide] + +Можно аналогично центрировать и по горизонтали, если известен горизонтальный размер, при помощи `left:50%` и отрицательного `margin-left`. + +### Одна строка: line-height + +Вертикально отцентрировать одну строку в элементе с известной высотой `height` можно, указав эту высоту в свойстве `line-height`: + +```html + + + +
                        + Текст +
                        +``` + +Это работает, но лишь до тех пор, пока строка одна, а если содержимое вдруг переносится на другую строку, то начинает выглядеть довольно уродливо. + +### Таблица с vertical-align + +У свойства [vertical-align](http://www.w3.org/TR/CSS2/visudet.html#propdef-vertical-align), которое управляет вертикальным расположением элемента, есть два режима работы. + +**В таблицах свойство `vertical-align` указывает расположение *содержимого* ячейки.** + +Его возможные значения: +
                        +
                        `baseline`
                        +
                        Значение по умолчанию.
                        +
                        `middle`, `top`, `bottom`
                        +
                        Располагать содержимое посередине, вверху, внизу ячейки.
                        +
                        + +Например, ниже есть таблица со всеми 3-мя значениями: + +```html + + + + + +*!* + + + +*/!* + +
                        topmiddlebottom
                        +``` + +Обратим внимание, что в ячейке с `vertical-align: middle` содержимое находится по центру. Таким образом, можно обернуть нужный элемент в таблицу размера `width:100%;height:100%` с одной ячейкой, у которой указать `vertical-align:middle`, и он будет отцентрирован. + +Но мы рассмотрим более красивый способ, который поддерживается во всех современных браузерах, и в IE8+. В них не обязательно делать таблицу, так как доступно значение `display:table-cell`. Для элемента с таким `display` используются те же алгоритмы вычисления ширины и центрирования, что и в `TD`. И, в том числе, работает `vertical-align`: + +Пример центрирования: + +```html + +
                        + +
                        +``` + +**Этот способ замечателен тем, что он не требует знания высоты элементов.** + +Однако у него есть особенность. Вместе с `vertical-align` родительский блок получает табличный алгоритм вычисления ширины и начинает подстраиваться под содержимое. Это не всегда желательно. + +Чтобы его растянуть, нужно указать `width` явно, например: `300px`: + +```html + +
                        + +
                        +``` + +Можно и в процентах, но в примере выше они не сработают, потому что структура таблицы "сломана" -- ячейка есть, а собственно таблицы-то нет. + +Это можно починить, завернув "псевдоячейку" в элемент с `display:table`, которому и поставим ширину: + +```html + +
                        +
                        + +
                        +
                        +``` + +Если дополнительно нужно горизонтальное центрирование -- оно обеспечивается другими средствами, например `margin: 0 auto` для блочных элементов или `text-align:center` на родителе -- для других. + +### Центрирование в строке с vertical-align + +Для инлайновых элементов (`display:inline/inline-block`), включая картинки, свойство `vertical-align` центрирует *сам инлайн-элемент в окружающем его тексте*. + +В этом случае набор значений несколько другой: +[iframe src="vertical-align" height="300" link edit border="1"] + +Это можно использовать и для центрирования, если высота родителя известна, а центрируемого элемента -- нет. + +Допустим, высота внешнего элемента `120px`. Укажем её в свойстве `line-height`: + +```html + + +
                        + Центрирован
                        вертикально
                        +
                        +``` + +Работает во всех браузерах и IE8+. + +Свойство `line-height` наследуется, поэтому надо знать "правильную" высоту строки и переопределять её для `inner`. + +### Центрирование с vertical-align без таблиц + +Если центрирование должно работать для любой высоты родителя и центрируемого элемента, то обычно используют таблицы или `display:table-cell` с `vertical-align`. + +Если центрируются не-блочные элементы, например `inline` или `inline-block`, то `vertical-align` может решить задачу без всяких таблиц. Правда, понадобится вспомогательный элемент (можно через `:before`). + +Пример: + +```html + + + +
                        + + + Центрированный
                        Элемент +
                        +
                        +``` + +
                          +
                        • Перед центрируемым элементом помещается вспомогательный инлайн-блок `before`, занимающий всю возможную высоту.
                        • +
                        • Центрируемый блок выровнен по его середине.
                        • +
                        + +Для всех современных браузеров и IE8 можно добавить вспомогательный элемент через `:before`: + +```html + + + +
                        + + Центрированный
                        Элемент +
                        +
                        +``` + +В пример выше добавлено также горизонтальное центрирование `text-align: center`. Но вы можете видеть, что на самом деле внутренний элемент не центрирован горизонтально, он немного сдвинут вправо. + +Это происходит потому, что центрируется *весь текст*, а перед `inner` находится пробел, который занимает место. + +Варианта два: +
                          +
                        1. Убрать лишний пробел между `div` и началом `inner`, будет `
                          ...`.
                        2. +
                        3. Оставить пробел, но сделать отрицательный `margin-left` у `inner`, равный размеру пробела, чтобы `inner` сместился левее.
                        4. +
                        + +Второе решение: + +```html + + + +
                        + + Центрированный
                        Элемент +
                        +
                        +``` + +## Итого + +Обобщим решения, которые обсуждались в этой статье. + +Для горизонтального центрирования: +
                          +
                        • `text-align: center` -- центрирует инлайн-элементы в блоке. В IE<8 центрирует всё, но это нестандартное поведение.
                        • +
                        • `margin: 0 auto` -- центрирует блок внутри родителя. У блока должна быть указана ширина.
                        • +
                        + +Для вертикального центрирования одного блока внутри другого: + +
                        +
                        Если размер центрируемого элемента известен, а родителя - нет
                        +
                        Родителю `position:relative`, потомку `position:absolute; top:50%` и `margin-top:-<половина-высоты-потомка>`. Аналогично можно отцентрировать и по горизонтали.
                        +
                        Если нужно отцентрировать одну строку в блоке, высота которого известна
                        +
                        Поставить блоку `line-height: <высота>`. Нужны конкретные единицы высоты (`px`,`em`...). Значение `line-height:100%` не будет работать, т.к. проценты берутся не от высоты блока, а от текущей `line-height`.
                        +
                        Высота родителя известна, а центрируемого элемента - нет.
                        +
                        Поставить `line-height` родителю во всю его высоту, а потомку поставить `display:inline-block`.
                        +
                        Высота обоих элементов неизвестна.
                        +
                        Два варианта: +
                          +
                        1. Сделать элемент-родитель ячейкой таблицы при помощи `display:table-cell`(IE8) или реальной таблицы, и поставить ему `vertical-align:middle`. Отлично работает, но мы имеем дело с таблицей вместо обычного блока.
                        2. +
                        3. Решение с вспомогательным элементом `outer:before` и инлайн-блоками. Вполне универсально и не создаёт таблицу.
                        4. +
                        +
                        +
                        + + diff --git a/3-more/11-css-for-js/6-css-center/vertical-align.view/index.html b/3-more/11-css-for-js/6-css-center/vertical-align.view/index.html new file mode 100755 index 00000000..30c47286 --- /dev/null +++ b/3-more/11-css-for-js/6-css-center/vertical-align.view/index.html @@ -0,0 +1,20 @@ + + + + + + + +Картинка размером в 30px, значения vertical-align: + +
                        +baseline(по умолчанию) 
                        +middle(по середине) 
                        +subвровень с <sub> 
                        +superвровень с <sup> 
                        +text-top(верхняя граница вровень с текстом) 
                        +text-bottom(нижняя граница вровень с текстом) 
                        +
                        + + + diff --git a/3-more/11-css-for-js/7-font-size-line-height/article.md b/3-more/11-css-for-js/7-font-size-line-height/article.md new file mode 100644 index 00000000..8c11ebed --- /dev/null +++ b/3-more/11-css-for-js/7-font-size-line-height/article.md @@ -0,0 +1,119 @@ +# Свойства "font-size" и "line-height" + +Здесь мы рассмотрим, как соотносятся размеры шрифта и строки, и как их правильно задавать. + +## font-size и line-height +
                          +
                        • `font-size` -- *размер шрифта*, в частности, определяющий высоту букв.
                        • +
                        • `line-height` -- *высота строки*.
                        • +
                        + +Сделаем HTML, в котором шрифт и размер строки одинаковы: + +```html + + + +
                        Ёрш
                        +
                        Ёрш
                        +``` + +**Как видно из примера,`font-size` -- это абстрактное значение, которое привязано к шрифту, и даётся в типографских целях, это даже не "самая большая буква", а просто такая прикидка, чтобы было удобно планировать размер строки.** + +При размере строки, равном `font-size`, строка не будет размером точно "под букву". В зависимости от шрифта, "хвосты" букв при этом могут вылезать, поэтому обычно размер строки делают чуть больше, чем шрифт. + +**По умолчанию в браузерах используется специальное значение `line-height:normal`.** + +Оно означает, что браузер может принимать решение о размере строки самостоятельно. Как правило, оно будет в диапазоне `1.0 - 1.25`, но стандарт не гарантирует этого, он говорит лишь, что оно должно быть "разумным" (reasonable). + +## Множитель для line-height + +Значение `line-height` можно указать при помощи `px` или `em`, но гораздо лучше -- задать его числом. + +Значение-число интерпретируется как множитель относительно размера шрифта. Например, значение с множителем `line-height: 2` при `font-size: 16px` будет аналогично `line-height: 32px (=16px*2)`. + +Однако, между множителем и точным значением есть одна существенная разница. + +
                          +
                        • **Значение, заданное множителем, наследуется и применяется в каждом элементе относительно его размера шрифта.** +То есть, при `line-height: 2` означает, что высота строки будет равна удвоенному размеру шрифта, не важно какой шрифт. +
                        • +
                        • **Значение, заданное в единицах измерения, запоминается и наследуется "как есть".** +Это означает, что `line-height: 32px` будет всегда жёстко задавать высоту строки, даже если шрифт во вложенных элементах станет больше или меньше текущего.
                        • +
                        + +Давайте посмотрим, как это выглядит, на примерах: + +
                        +
                        Множитель, `line-height:1.25`
                        +
                        + +```html + +
                        + стандартная строка +
                        + шрифт в 2 раза больше
                        + шрифт в 2 раза больше +
                        +
                        +``` + +
                        +
                        Конкретное значение, `line-height:1.25em`
                        +
                        + +```html + +
                        + стандартная строка +
                        + шрифт в 2 раза больше
                        + шрифт в 2 раза больше +
                        +
                        +``` + +
                        +
                        + +Какой вариант выглядит лучше? Наверно, первый. В нём размер строки более-менее соответствует шрифту, поскольку задан через множитель. + +**В обычных ситуациях рекомендуется использовать именно множитель, за исключением особых случаев, когда вы действительно знаете что делаете.** + +## Синтаксис font: size/height family + +Установить `font-size` и `line-height` можно одновременно. + +Соответствующий синтаксис выглядит он так: + +```css +font: *!*20px/1.5*/!* Arial,sans-serif; +``` + +При этом нужно обязательно указать сам шрифт, например `Arial,sans-serif`. Укороченный вариант `font: 20px/1.5` работать не будет. + +Дополнительно можно задать и свойства `font-style`, `font-weight`: + +```css +font: *!*italic bold*/!* 20px/1.5 Arial,sans-serif; +``` + +## Итого + +
                        +
                        `line-height`
                        +
                        Размер строки, обычно он больше размера шрифта. При установке множителем расчитывается каждый раз относительно текущего шрифта, при установке в единицах измерения -- фиксируется.
                        +
                        `font-size`
                        +
                        Размер шрифта. Если сделать блок такой же высоты, как шрифт, то хвосты букв будут вылезать из-под него.
                        +
                        `font: 125%/1.5 FontFamily`
                        +
                        Даёт возможность одновременно задать размер, высоту строки и, собственно, сам шрифт.
                        +
                        diff --git a/3-more/11-css-for-js/8-white-space/article.md b/3-more/11-css-for-js/8-white-space/article.md new file mode 100644 index 00000000..9b2d438f --- /dev/null +++ b/3-more/11-css-for-js/8-white-space/article.md @@ -0,0 +1,122 @@ +# Свойство white-space + +Свойство `white-space` позволяет сохранять пробелы и переносы строк. + +У него есть два известных значения: +
                          +
                        • `white-space: normal` -- обычное поведение
                        • +
                        • `white-space: pre` -- текст ведёт себя, как будто оформлен в тег `pre`.
                        • +
                        + +Но браузеры поддерживают ещё 3, два из которых -- только в IE8+ и очень полезны. +[cut] + +## pre / nowrap + +Встречаем первую "сладкую парочку" -- `pre` и `nowrap`. + +Оба этих значения меняют стандартное поведение HTML при работе с текстом: + +**`pre`**: +
                          +
                        • **сохраняет пробелы**
                        • +
                        • **переносит текст при явном разрыве строки.**
                        • +
                        + +**`nowrap`** +
                          +
                        • **не сохраняет пробелы**
                        • +
                        • **игнорирует явные разрывы строки (не переносит текст).**
                        • +
                        + +Оба этих значения поддерживаются кросс-браузерно. + +**Их основной недостаток -- текст вылезает из контейнера.** + +Для примера, рассмотрим следующий фрагмент кода: + +```js +if (hours > 18) { + // Пойти поиграть в теннис +} +``` + +
                          +
                        • **`white-space: pre`:** + +[pre] +
                          if (hours > 18) { + // Пойти поиграть в теннис +} +
                          +[/pre] + +Здесь пробелы и переводы строк сохранены. В HTML этому значению `white-space` соответствует тег `PRE`. +
                        • +
                        • **`white-space: nowrap`:** + +[pre] +
                          if (hours > 18) { + // Пойти поиграть в теннис +} +
                          +[/pre] + +Здесь переводы строки проигнорированы, а подряд идущие пробелы, если присмотреться -- сжаты в один (например, перед комментарием `//`). +
                        • +
                        + +Допустим, мы хотим разрешить посетителям публиковать код на сайте, с сохранением разметки. Но тег `PRE` и описанные выше значения `white-space` для этого не подойдут! + +Злой Василий Пупкин может написать такой текст, который вылезет из контейнера и поломает вёрстку страницы. + +Можно скрыть вылезшее значение при помощи `overflow-x: hidden` или сделать так, чтобы была горизонтальная прокрутка, но, к счастью, есть другие значения `white-space`, специально для таких случаев. + +## pre-wrap/pre-line + +Эти значения не поддерживаются в IE7-. + +
                        +
                        `pre-wrap`
                        +
                        То же самое, что `pre`, но переводит строку, если текст вылезает из контейнера.
                        +
                        `pre-line`
                        +
                        То же самое, что `pre`, но переводит строку, если текст вылезает из контейнера и не сохраняет пробелы.
                        +
                        + +Оба поведения отлично прослеживаются на примерах: + +
                          +
                        • **`white-space: pre-wrap`:** + +[pre] +
                          if (hours > 18) { + // Пойти поиграть в теннис +} +
                          +[/pre] +Отличный выбор для безопасной вставки кода посетителями. + +
                        • +
                        • **`white-space: pre-line`:** + +[pre] +
                          if (hours > 18) { + // Пойти поиграть в теннис +} +
                          +[/pre] + +Сохранены переводы строк, ничего не вылезает, но пробелы интерпретированы в режиме обычного HTML. +
                        • +
                        + +Никто не мешает использовать эти значения на сайте, а для IE7- игнорировать проблему или задавать контейнеру `overflow-x: hidden`. + + \ No newline at end of file diff --git a/3-more/11-css-for-js/9-outline/article.md b/3-more/11-css-for-js/9-outline/article.md new file mode 100644 index 00000000..2830a468 --- /dev/null +++ b/3-more/11-css-for-js/9-outline/article.md @@ -0,0 +1,45 @@ +# Свойство "outline" + +Свойство `outline` задаёт дополнительную рамку вокруг элемента, *за пределами его CSS-блока*. Поддерживается во всех браузерах, IE8+. +[cut] +Для примера, рассмотрим его вместе с обычной рамкой `border`: + +```html + +
                        + Элемент +
                        +``` + +
                          +
                        • **В отличие от `border`, рамка `outline` не участвует в блочной модели CSS.** Она не занимает места и не меняет размер элемента. Поэтому его используют, когда хотят добавить рамку без изменения других CSS-параметров. +
                        • +
                        • Также, в отличие от `border`, рамку `outline` можно задать только со всех сторон: свойств `outline-top`, `outline-left` не существует.
                        • +
                        + +Так как `outline` находится за границами элемента -- **`outline`-рамки соседей могут перекрывать друг друга**: + +```html + +
                        + Элемент +
                        +
                        + Элемент +
                        +``` + +В примере выше верхняя рамка нижнего элемента находится на территории верхнего и наоборот. + +Все браузеры, кроме IE<10, также поддерживают свойство `outline-offset`, задающее отступ `outline` от внешней границы элемента: + +```html + +
                        + Везде, кроме IE<10, между рамками будет расстояние 5px +
                        +``` + +Ещё раз заметим, что основная особенность `outline` -- в том, что при наличии `outline-offset` или без него -- он не занимает места в блоке. + +Поэтому его часто используют для стилей `:hover` и других аналогичных, когда нужно выделить элемент, но чтобы ничего при этом не прыгало. \ No newline at end of file diff --git a/3-more/11-css-for-js/index.md b/3-more/11-css-for-js/index.md new file mode 100644 index 00000000..c35c778b --- /dev/null +++ b/3-more/11-css-for-js/index.md @@ -0,0 +1,2 @@ +# CSS для JavaScript-разработчика + diff --git a/3-more/2-animation/1-js-animation/1-carousel-animated/solution.md b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.md new file mode 100644 index 00000000..78e8da76 --- /dev/null +++ b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.md @@ -0,0 +1,3 @@ +Проще всего -- использовать функцию [](#animate), а еще удобнее -- `animateProp`. + +[edit src="solution"/] \ No newline at end of file diff --git a/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/animate.js b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/animate.js new file mode 100755 index 00000000..51bb38c6 --- /dev/null +++ b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/animate.js @@ -0,0 +1,92 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', start:0, end: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + var value = Math.round(start + (end - start)*delta); + opts.elem.style[prop] = value + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress); +} + +function linear(progress) { + return progress; +} + +function quad(progress) { + return Math.pow(progress, 2); +} + +function quint(progress) { + return Math.pow(progress, 5); +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)); +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5); +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2; + else + return (2 - delta(2*(1-progress))) / 2; + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress); + } +} diff --git a/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/index.html b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/index.html new file mode 100755 index 00000000..b7f62348 --- /dev/null +++ b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/index.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + diff --git a/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/style.css b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/style.css new file mode 100755 index 00000000..d430e776 --- /dev/null +++ b/3-more/2-animation/1-js-animation/1-carousel-animated/solution.view/style.css @@ -0,0 +1,52 @@ +body { + padding: 10px +} + +.carousel { + position: relative; + width: 398px; + padding: 10px 40px; + border: 1px solid #CCC; + border-radius: 15px; + background: #eee; +} +.carousel img { + width: 130px; + height: 130px; + display: block; /* если не поставить block, в ряде браузеров будет inline -> лишнее пространтсво вокруг элементов */ +} +.carousel .arrow { + position: absolute; + top: 57px; + padding: 3px 8px 8px 9px; + background: #ddd; + border-radius: 15px; + font-size: 24px; + color: #444; + text-decoration: none; +} + +.carousel .arrow:hover { + background: #ccc; +} +.carousel .left-arrow { + left: 7px; +} +.carousel .right-arrow { + right: 7px; +} +.gallery { + width: 390px; + overflow: hidden; +} +.gallery ul { + height: 130px; + width: 9999px; + margin: 0; + padding: 0; + list-style: none; +} + +.gallery ul li { + float: left; +} diff --git a/3-more/2-animation/1-js-animation/1-carousel-animated/task.md b/3-more/2-animation/1-js-animation/1-carousel-animated/task.md new file mode 100644 index 00000000..0442a370 --- /dev/null +++ b/3-more/2-animation/1-js-animation/1-carousel-animated/task.md @@ -0,0 +1,10 @@ +# Анимируйте карусель + +[importance 5] + +Возьмите решение задачи [](/task/carousel) и сделайте передвижение карусели плавным, анимированым. + +Подключите для этого файл animate.js. + +Результат: +[iframe height=220 src="solution"] \ No newline at end of file diff --git a/3-more/2-animation/1-js-animation/2-animate-logo/solution.md b/3-more/2-animation/1-js-animation/2-animate-logo/solution.md new file mode 100644 index 00000000..b4953291 --- /dev/null +++ b/3-more/2-animation/1-js-animation/2-animate-logo/solution.md @@ -0,0 +1,5 @@ +Анимируйте одновременно свойства `left/top` и `width/height`. + +Чтобы в процессе анимации таблица сохраняла геометрию -- создайте на месте `IMG` временный `DIV` фиксированного размера и переместите `IMG` внутрь него. После анимации можно вернуть как было. + +[edit src="solution"]Открыть решение в песочнице[/edit] \ No newline at end of file diff --git a/3-more/2-animation/1-js-animation/2-animate-logo/solution.view/animate.js b/3-more/2-animation/1-js-animation/2-animate-logo/solution.view/animate.js new file mode 100755 index 00000000..51bb38c6 --- /dev/null +++ b/3-more/2-animation/1-js-animation/2-animate-logo/solution.view/animate.js @@ -0,0 +1,92 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', start:0, end: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + var value = Math.round(start + (end - start)*delta); + opts.elem.style[prop] = value + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress); +} + +function linear(progress) { + return progress; +} + +function quad(progress) { + return Math.pow(progress, 2); +} + +function quint(progress) { + return Math.pow(progress, 5); +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)); +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5); +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2; + else + return (2 - delta(2*(1-progress))) / 2; + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress); + } +} diff --git a/3-more/2-animation/1-js-animation/2-animate-logo/solution.view/index.html b/3-more/2-animation/1-js-animation/2-animate-logo/solution.view/index.html new file mode 100755 index 00000000..e27697d8 --- /dev/null +++ b/3-more/2-animation/1-js-animation/2-animate-logo/solution.view/index.html @@ -0,0 +1,52 @@ + + + + + + + + + +Кликните картинку для анимации. Расположение элементов при анимации не должно меняться! + + + + + + + +
                        Догнать +..и перегнать!
                        + + + + + diff --git a/3-more/2-animation/1-js-animation/2-animate-logo/source.view/animate.js b/3-more/2-animation/1-js-animation/2-animate-logo/source.view/animate.js new file mode 100755 index 00000000..51bb38c6 --- /dev/null +++ b/3-more/2-animation/1-js-animation/2-animate-logo/source.view/animate.js @@ -0,0 +1,92 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', start:0, end: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + var value = Math.round(start + (end - start)*delta); + opts.elem.style[prop] = value + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress); +} + +function linear(progress) { + return progress; +} + +function quad(progress) { + return Math.pow(progress, 2); +} + +function quint(progress) { + return Math.pow(progress, 5); +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)); +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5); +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2; + else + return (2 - delta(2*(1-progress))) / 2; + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress); + } +} diff --git a/3-more/2-animation/1-js-animation/2-animate-logo/source.view/index.html b/3-more/2-animation/1-js-animation/2-animate-logo/source.view/index.html new file mode 100755 index 00000000..41a246e6 --- /dev/null +++ b/3-more/2-animation/1-js-animation/2-animate-logo/source.view/index.html @@ -0,0 +1,31 @@ + + + + + + + + + +Кликните картинку для анимации. Расположение элементов при анимации не должно меняться! + + + + + + +
                        Догнать +..и перегнать!
                        + +В процессе анимации повторные нажатия на изображение игнорируются. + + + + + + + diff --git a/3-more/2-animation/1-js-animation/2-animate-logo/task.md b/3-more/2-animation/1-js-animation/2-animate-logo/task.md new file mode 100644 index 00000000..0b0c434f --- /dev/null +++ b/3-more/2-animation/1-js-animation/2-animate-logo/task.md @@ -0,0 +1,12 @@ +# Анимировать лого + +[importance 5] + +Реализуйте анимацию, как в демке ниже (клик на картинку): +[iframe src="solution" border=1] + +Продолжительность анимации: 3 секунды. + +В процессе анимации пусть повторные клики на изображение игнорируются. + +[edit src="source" task/] diff --git a/3-more/2-animation/1-js-animation/3-animate-ball/solution.md b/3-more/2-animation/1-js-animation/3-animate-ball/solution.md new file mode 100644 index 00000000..05b9bbe1 --- /dev/null +++ b/3-more/2-animation/1-js-animation/3-animate-ball/solution.md @@ -0,0 +1,28 @@ +В HTML/CSS, падение мяча можно отобразить изменением свойства `ball.style.top` от 0 и до значения, соответствующего нижнему положению. + +Нижняя граница элемента `field`, в котором находится мяч, имеет значение `field.clientHeight`. Но свойство `top` относится к верху мяча, поэтому оно меняется до `field.clientHeight - ball.clientHeight`. + +Для создания анимационного эффекта лучше всего подойдет функция `bounce` в режиме `easeOut`. + +Следующий код даст нам нужный результат: + +```js +var img = document.getElementById('ball'); +var field = document.getElementById('field'); +img.onclick = function() { + var from = 0; + var to = field.clientHeight - img.clientHeight; + animate({ + delay: 20, + duration: 1000, +*!* + delta: makeEaseOut(bounce), + step: function(delta) { + img.style.top = to*delta + 'px' + } +*/!* + }); +} +``` + +[edit src="solution"]Полное решение[/edit] \ No newline at end of file diff --git a/3-more/2-animation/1-js-animation/3-animate-ball/solution.view/animate.js b/3-more/2-animation/1-js-animation/3-animate-ball/solution.view/animate.js new file mode 100755 index 00000000..12ae14e3 --- /dev/null +++ b/3-more/2-animation/1-js-animation/3-animate-ball/solution.view/animate.js @@ -0,0 +1,91 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', from:0, to: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + opts.elem.style[prop] = Math.round(start + (start - end)*delta) + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress) +} + +function linear(progress) { + return progress +} + +function quad(progress) { + return Math.pow(progress, 2) +} + +function quint(progress) { + return Math.pow(progress, 5) +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)) +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5) +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2 + else + return (2 - delta(2*(1-progress))) / 2 + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress) + } +} diff --git a/3-more/2-animation/1-js-animation/3-animate-ball/solution.view/index.html b/3-more/2-animation/1-js-animation/3-animate-ball/solution.view/index.html new file mode 100755 index 00000000..92f12472 --- /dev/null +++ b/3-more/2-animation/1-js-animation/3-animate-ball/solution.view/index.html @@ -0,0 +1,50 @@ + + + + + + + + + +
                        + +
                        + + + + + + + diff --git a/3-more/2-animation/1-js-animation/3-animate-ball/source.view/animate.js b/3-more/2-animation/1-js-animation/3-animate-ball/source.view/animate.js new file mode 100755 index 00000000..12ae14e3 --- /dev/null +++ b/3-more/2-animation/1-js-animation/3-animate-ball/source.view/animate.js @@ -0,0 +1,91 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', from:0, to: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + opts.elem.style[prop] = Math.round(start + (start - end)*delta) + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress) +} + +function linear(progress) { + return progress +} + +function quad(progress) { + return Math.pow(progress, 2) +} + +function quint(progress) { + return Math.pow(progress, 5) +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)) +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5) +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2 + else + return (2 - delta(2*(1-progress))) / 2 + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress) + } +} diff --git a/3-more/2-animation/1-js-animation/3-animate-ball/source.view/index.html b/3-more/2-animation/1-js-animation/3-animate-ball/source.view/index.html new file mode 100755 index 00000000..cc74a8f9 --- /dev/null +++ b/3-more/2-animation/1-js-animation/3-animate-ball/source.view/index.html @@ -0,0 +1,22 @@ + + + + + + + + + +
                        + +
                        + + + + + diff --git a/3-more/2-animation/1-js-animation/3-animate-ball/task.md b/3-more/2-animation/1-js-animation/3-animate-ball/task.md new file mode 100644 index 00000000..2c736213 --- /dev/null +++ b/3-more/2-animation/1-js-animation/3-animate-ball/task.md @@ -0,0 +1,10 @@ +# Анимируйте мяч + +[importance 5] + +Сделайте так, чтобы мяч подпрыгивал. Кликните по мячу, чтобы увидеть, как это должно выглядеть. +[iframe height=250 src="solution"] + +В исходный документ включена функция [](#animate) и набор `delta`-функций. + +[edit src="source" task/] diff --git a/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.md b/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.md new file mode 100644 index 00000000..ffe2ef02 --- /dev/null +++ b/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.md @@ -0,0 +1,37 @@ +Посмотрите задачу [](/task/animate-ball). Там создаётся подпрыгивающий мяч. А для решения этой задачи нам нужно добавить еще одну анимацию для `elem.style.left`. + +Горизонтальная координата меняется по другому закону, нежели вертикальная. Она не "подпрыгивает", а постоянно увеличивается, постепенно сдвигая мяч вправо. + +Мы могли бы применить для неё `linear`, но тогда горизонтальное движение будет отставать от скачков мяча. Более красиво будет что-то типа `makeEaseOut(quad)`. + +Код: + +```js +img.onclick = function() { + + var height = document.getElementById('field').clientHeight - img.clientHeight + var width = 100 + + animate({ + delay: 20, + duration: 1000, + delta: makeEaseOut(bounce), + step: function(delta) { + img.style.top = height*delta + 'px' + } + }); + +*!* + animate({ + delay: 20, + duration: 1000, + delta: makeEaseOut(quad), + step: function(delta) { + img.style.left = width*delta + "px" + } + }); +*/!* +} +``` + +[edit src="solution"]Полное решение[/edit] \ No newline at end of file diff --git a/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.view/animate.js b/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.view/animate.js new file mode 100755 index 00000000..12ae14e3 --- /dev/null +++ b/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.view/animate.js @@ -0,0 +1,91 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', from:0, to: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + opts.elem.style[prop] = Math.round(start + (start - end)*delta) + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress) +} + +function linear(progress) { + return progress +} + +function quad(progress) { + return Math.pow(progress, 2) +} + +function quint(progress) { + return Math.pow(progress, 5) +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)) +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5) +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2 + else + return (2 - delta(2*(1-progress))) / 2 + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress) + } +} diff --git a/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.view/index.html b/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.view/index.html new file mode 100755 index 00000000..252e1fdb --- /dev/null +++ b/3-more/2-animation/1-js-animation/4-animate-ball-hops/solution.view/index.html @@ -0,0 +1,58 @@ + + + + + + + + + +
                        + +
                        + + + + + + + diff --git a/3-more/2-animation/1-js-animation/4-animate-ball-hops/source.view/animate.js b/3-more/2-animation/1-js-animation/4-animate-ball-hops/source.view/animate.js new file mode 100755 index 00000000..12ae14e3 --- /dev/null +++ b/3-more/2-animation/1-js-animation/4-animate-ball-hops/source.view/animate.js @@ -0,0 +1,91 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', from:0, to: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + opts.elem.style[prop] = Math.round(start + (start - end)*delta) + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress) +} + +function linear(progress) { + return progress +} + +function quad(progress) { + return Math.pow(progress, 2) +} + +function quint(progress) { + return Math.pow(progress, 5) +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)) +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5) +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2 + else + return (2 - delta(2*(1-progress))) / 2 + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress) + } +} diff --git a/3-more/2-animation/1-js-animation/4-animate-ball-hops/source.view/index.html b/3-more/2-animation/1-js-animation/4-animate-ball-hops/source.view/index.html new file mode 100755 index 00000000..cc74a8f9 --- /dev/null +++ b/3-more/2-animation/1-js-animation/4-animate-ball-hops/source.view/index.html @@ -0,0 +1,22 @@ + + + + + + + + + +
                        + +
                        + + + + + diff --git a/3-more/2-animation/1-js-animation/4-animate-ball-hops/task.md b/3-more/2-animation/1-js-animation/4-animate-ball-hops/task.md new file mode 100644 index 00000000..7663713d --- /dev/null +++ b/3-more/2-animation/1-js-animation/4-animate-ball-hops/task.md @@ -0,0 +1,11 @@ +# Анимируйте падение мяча с отскоками вправо + +[importance 5] + +Заставьте мяч падать вправо. Кликните, чтобы увидеть в действии. +[iframe height=250 src="solution"] + +Напишите код, который будет анимировать мяч. Дистанция вправо составляет `100px`. + +В исходный документ включена функция [](#animate) и набор `delta`-функций. +[edit src="source" task/] diff --git a/3-more/2-animation/1-js-animation/article.md b/3-more/2-animation/1-js-animation/article.md new file mode 100644 index 00000000..2e3fd1ab --- /dev/null +++ b/3-more/2-animation/1-js-animation/article.md @@ -0,0 +1,696 @@ +# JS-Анимация + +В этой главе мы рассмотрим устройство браузерной анимации. Она примерно одинаково реализована во всех фреймворках. + +Понимание этого позволит разобраться в происходящем, если что-то вдруг не работает, а также написать сложную анимацию самому. + +Анимация при помощи JavaScript и современная CSS-анимация дополняют друг друга. +[cut] + +## Основы анимации + +С точки зрения HTML/CSS, анимация -- это постепенное изменение стиля DOM-элемента. Например, увеличение координаты `style.left` от `0px` до `100px` сдвигает элемент. + +Код, который производит изменение, вызывается таймером. Интервал таймера очень мал и поэтому анимация выглядит плавной. Это тот же принцип, что и в кино: для непрерывной анимации достаточно 24 или больше вызовов таймера в секунду. + +Псевдо-код для анимации выглядит так: + +```js +var timer = setInterval(function() { + показать новый кадр + if (время вышло) clearInterval(timer); +}, 10) +``` + +Задержка между кадрами в данном случае составляет `10 ms`, что означает `100` кадров в секунду. + +В большинстве фреймворков, задержка по умолчанию составляет `10`-`15` мс. Меньшая задержка делает анимацию более плавной, но только в том случае, если браузер достаточно быстр, чтобы анимировать каждый шаг вовремя. + +Если анимация требует большого количества вычислений, то нагрузка процессора может доходить до 100% и вызывать ощутимые "тормоза" в работе браузера. В +таком случае, задержку можно увеличить. Например, 40мс дадут нам 25 кадров в секунду, что очень близко к кинематографическому стандарту в 24 кадра. + +[smart header="`setInterval` вместо `setTimeout`"] +Мы используем `setInterval`, а не рекурсивный `setTimeout`, потому что нам нужен *один кадр за промежуток времени*, а не *фиксированная задержка между кадрами*. +В статье [](/setTimeout-setInterval) описана разница между `setInterval` и рекурсивным `setTimeout`. +[/smart] + + +### Пример + +Например, передвинем элемент путём изменения `element.style.left` от 0 до 100px. Изменение происходит на 1px каждые 10мс. + +```html + + +
                        +
                        +
                        +``` + +Кликните для демонстрации: +[iframe height=60 src="move100" link] + + +## Структура анимации + +У анимации есть три основных параметра: +
                        +
                        `delay`
                        +
                        Время между кадрами (в миллисекундах, т.е. 1/1000 секунды). Например, 10мс.
                        +
                        `duration`
                        +
                        Общее время, которое должна длиться анимация, в мс. Например, 1000мс.
                        +
                        `step(progress)`
                        +
                        Функция **`step(progress)`** занимается отрисовкой состояния анимации, соответствующего времени `progress`.
                        +
                        + +Каждый кадр выполняется, сколько времени прошло: `progress = (now-start)/duration`. Значение `progress` меняется от `0` в начале анимации до `1` в конце. Так как вычисления с дробными числами не всегда точны, то в конце оно может быть даже немного больше 1. В этом случае мы уменьшаем его до 1 и завершаем анимацию. + +Создадим функцию `animate`, которая получает объект со свойствами `delay, duration, step` и выполняет анимацию. + +```js +function animate(opts) { + + var start = new Date; // сохранить время начала + + var timer = setInterval(function() { + + // вычислить сколько времени прошло + var progress = (new Date - start) / opts.duration; + if (progress > 1) progress = 1; + + // отрисовать анимацию + opts.step(progress); + + if (progress == 1) clearInterval(timer); // конец :) + + }, opts.delay || 10); // по умолчанию кадр каждые 10мс + +} +``` + +### Пример + + +Анимируем ширину элемента `width` от `0` до `100%`, используя нашу функцию: + +```js +function stretch(elem) { + animate({ + duration: 1000, // время на анимацию 1000 мс + step: function(progress) { + elem.style.width = progress*100 + '%'; + } + }); +} +``` + +Кликните для демонстрации: +[iframe height=60 src="width" link] + + +Функция `step` может получать дополнительные параметры анимации из `opts` (через `this`) или через замыкание. + +Следующий пример использует параметр `to` из замыкания для анимации бегунка: + +```js +function move(elem) { + var to = 500; + + animate({ + duration: 1000, + step: function(progress) { + // progress меняется от 0 до 1, left от 0px до 500px + elem.style.left = to*progress + "px"; + } + }); + +} +``` + +Кликните для демонстрации: +[iframe height=60 src="move" link] + +## Временная функция delta + +В сложных анимациях свойства изменяются по определённому закону. Зачастую, он гораздо сложнее, чем простое равномерное возрастание/убывание. + +Для того, чтобы можно было задать более хитрые виды анимации, в алгоритм добавляется дополнительная функция `delta(progress)`, которая вычисляет текущее состояние анимации от 0 до 1, а `step` использует её значение вместо `progress`. + +В `animate` изменится всего одна строчка. Было: + +```js +... +opts.step(progress); +... +``` + +Станет: + +```js +... +opts.step( opts.delta(progress) ); +... +``` + + + +```js +//+ hide="Раскрыть код animate с delta" +function animate(opts) { + + var start = new Date; + + var timer = setInterval(function() { + + var progress = (new Date - start) / opts.duration; + if (progress > 1) progress = 1; + + opts.step( opts.delta(progress) ); + + if (progress == 1) clearInterval(timer); + + }, opts.delay || 10); + +} +``` + +Такое небольшое изменение добавляет много гибкости. Функция `step` занимается всего лишь отрисовкой текущего состояния анимации, а само состояние по времени определяется в `delta`. + +Разные значения `delta` заставляют скорость анимации, ускорение и другие параметры вести себя абсолютно по-разному. + +Рассмотрим примеры анимации движения с использованием различных `delta`. + +### Линейная delta + +Самая простая функция `delta` -- это та, которая просто возвращает `progress`. + +```js +function linear(progress) { + return progress; +} +``` + +То есть, как будто никакой `delta` нет. Состояние анимации (которое при передвижении отображается как координата `left`) зависит от времени линейно. + + +**График:** + + +**По горизонтали - `progress`, а по вертикали - `delta(progress)`.** + +Пример: + +```html +
                        +
                        +
                        +``` + +Здесь и далее функция `move` будет такой: + +```js +function move(elem, delta, duration) { + var to = 500; + + animate({ + delay: 10, + duration: duration || 1000, + delta: delta, + step: function(delta) { + elem.style.left = to*delta + "px" + } + }); + +} +``` + +То есть, она будет перемещать бегунок, изменяя `left` по закону `delta`, за `duration` мс (по умолчанию 1000мс). + +Кликните для демонстрации линейной `delta`: +
                        +
                        +
                        + + +### В степени n + +Вот еще один простой случай. `delta` - это `progress` в `n-й` степени . Частные случаи - квадратичная, кубическая функции и т.д. + +Для квадратичной функции: + +```js +function quad(progress) { + return Math.pow(progress, 2) +} +``` + +**График квадратичной функции:** + + + +Пример для квадратичной функции (клик для просмотра): +
                        +
                        +
                        + +Увеличение степени влияет на ускорение. Например, график для 5-й степени: + + + +И пример: +
                        +
                        +
                        + +**Функция `delta` описывает развитие анимации в зависимости от времени.** + +В примере выше -- сначала медленно: время идёт (ось X), а состояние анимации почти не меняется (ось Y), а потом всё быстрее и быстрее. Другие графики зададут иное поведение. + + +### Дуга + +Функция: + +```js +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)) +} +``` + +**График:** + + +Пример: +
                        +
                        +
                        + + + +### Back: стреляем из лука + +Эта функция работает по принципу лука: сначала мы "натягиваем тетиву", а затем "стреляем". + +В отличие от предыдущих функций, эта зависит от дополнительного параметра `x`, который является "коэффициентом упругости". Он определяет расстояние, на которое "оттягивается тетива". + +Её код: + +```js +function back(progress, x) { + return Math.pow(progress, 2) * ((x + 1) * progress - x) +} +``` + +**График для `x = 1.5`:** + + +Пример для `x = 1.5`: +
                        +
                        +
                        + + +### Отскок + +Представьте, что мы отпускаем мяч, он падает на пол, несколько раз отскакивает и останавливается. + +Функция `bounce` делает то же самое, только наоборот: "подпрыгивание" начинается сразу. + +Эта функция немного сложнее предыдущих и использует специальные коэффициенты: + +```js +function bounce(progress) { + for (var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2) + } + } +} +``` + +Код взят из MooTools.FX.Transitions. Конечно же, есть и другие реализации `bounce`. + +Пример: +
                        +
                        +
                        + + +### Упругая анимация + +Эта функция зависит от дополнительного параметра `x`, который определяет начальный диапазон. + +```js +function elastic(progress, x) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*x/3*progress) +} +``` + +**График для `x=1.5`:** + + +Пример для `x=1.5`: +
                        +
                        +
                        + + +## Реверсивные функции (easeIn, easeOut, easeInOut) + +Обычно, JavaScript-фреймворк предоставляет несколько `delta`-функций. +Их прямое использование называется "easeIn". + +**Иногда нужно показать анимацию в обратном режиме. Преобразование функции, которое даёт такой эффект, называется "easeOut"**. + + +### easeOut + +В режиме "easeOut", значение delta вычисляется так: +`deltaEaseOut(progress) = 1 - delta(1 - progress)` + +Например, функция `bounce` в режиме "easeOut": + +```js +function bounce(progress) { + for (var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +*!* +function makeEaseOut(delta) { // преобразовать delta + return function(progress) { + return 1 - delta(1 - progress); + } +} +*/!* + +*!*var bounceEaseOut = makeEaseOut(bounce);*/!* +``` + +Кликните для демонстрации: +
                        +
                        +
                        + +Давайте посмотрим, как преобразование `easeOut` изменяет поведение функции: + + + +Красным цветом обозначен easeIn, а зеленым - easeOut. +
                          +
                        • Обычно анимируемый объект сначала медленно скачет внизу, а затем, в конце, резко достигает верха..
                        • +
                        • А после easeOut он сначала прыгает наверх, а затем медленно скачет внизу.
                        • +
                        + +**При `easeOut` анимация развивается в обратном временном порядке.** + +Если есть анимационный эффект, такой как подпрыгивание -- он будет показан в конце, а не в начале (или наоборот, в начале, а не в конце). + +### easeInOut + +А еще можно сделать так, чтобы показать эффект *и* в начале *и* в конце анимации. Соответствующее преобразование называется "easeInOut". + +Его код выглядит так: + +```js +if (progress <= 0.5) { // первая половина анимации) + return delta(2 * progress) / 2; +} else { // вторая половина + return (2 - delta(2 * (1 - progress))) / 2; +} +``` + +Например, `easeInOut` для `bounce`: +
                        +
                        +
                        + +*У этого примера длительность составляет 3 секунды для того, что бы хватило времени для обоих эффектов(начального и конечного).* + +Код, который трансформирует `delta`: + +```js +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2; + else + return (2 - delta(2*(1-progress))) / 2; + } +} + +bounceEaseInOut = makeEaseInOut(bounce); +``` + +Трансформация "easeInOut" объединяет в себе два графика в один: `easeIn` для первой половины анимации и `easeOut` -- для второй. + +Например, давайте посмотрим эффект `easeOut`/`easeInOut` на примере функции `circ`: + + +Как видно, график первой половины анимации представляет собой уменьшенный "easeIn", а второй -- уменьшенный "easeOut". В результате, анимация начинается и заканчивается одинаковым эффектом. + + +### Графопостроитель + +Для наглядной демонстрации в действии различных delta, как нормальных(easeIn), так и измененных(easeOut,easeInOut), я подготовил графопостроитель. + + +Открыть в новом окне. + +Выберите функцию и нажмите `Рисовать!` + + +
                          +
                        • easeIn - базовое поведение: медленная анимация в начале, с постепенным ускорением.
                        • +
                        • easeOut - поведение, обратное easeIn: быстрая анимация на старте, а затем все медленней и медленней.
                        • +
                        • easeInOut - слияние обоих поведений. Анимация разделяется на две части. Первая часть - это `easeIn`, а вторая - `easeOut`.
                        • +
                        + +Для примера, попробуйте "bounce". + +[summary] +Процесс анимации полностью в ваших руках благодаря `delta`. Вы можете сделать ее настолько реалистичной, насколько захотите. + +И кстати. Если вы когда-нибудь изучали математику... Некоторые вещи все же бывают полезны в жизни :) Можно продумать и сделать красиво. + +Впрочем, исходя из практики, можно сказать, что варианты `delta`, описанные выше, покрывают 95% потребностей в анимации. +[/summary] + +## Сложные варианты step + +Анимировать можно все, что угодно. Вместо движения, как во всех предыдущих примерах, вы можете изменять прозрачность, ширину, высоту, цвет... Все, о чем вы можете подумать! + +Достаточно лишь написать соответствующий `step`. + +### Подсветка цветом + +Функция `highlight`, представленная ниже, анимирует изменение цвета. + +```js +function highlight(elem) { + var from = [255,0,0], to = [255,255,255] + animate({ + delay: 10, + duration: 1000, + delta: linear, + step: function(delta) { + elem.style.backgroundColor = 'rgb(' + + Math.max(Math.min(parseInt((delta * (to[0]-from[0])) + from[0], 10), 255), 0) + ',' + + Math.max(Math.min(parseInt((delta * (to[1]-from[1])) + from[1], 10), 255), 0) + ',' + + Math.max(Math.min(parseInt((delta * (to[2]-from[2])) + from[2], 10), 255), 0) + ')' + } + }) +} +``` + +
                        Кликните по этой надписи, чтобы увидеть функцию в действии
                        + +А теперь тоже самое, но `delta = makeEaseOut(bounce)`: + +
                        Кликните по этой надписи, чтобы увидеть функцию в действии
                        + + +### Набор текста + +Вы можете создавать интересные анимации, как, например, набор текста в "скачущем" режиме: + +[pre] + + + +[/pre] +Исходный код: + +```js +function animateText(textArea) { + var text = textArea.value + var to = text.length, from = 0 + + animate({ + delay: 20, + duration: 5000, + delta: bounce, + step: function(delta) { + var result = (to-from) * delta + from + textArea.value = text.substr(0, Math.ceil(result)) + } + }) +} +``` + +## Итого [#animate] + + +Анимация выполняется путём использования `setInterval` с маленькой задержкой, порядка 10-50мс. При каждом запуске происходит отрисовка очередного кадра. + +Анимационная функция, немного расширенная: + +```js +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + + var progress = (new Date - start) / opts.duration; + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} +``` + +Основные параметры: +
                          +
                        • `delay` - задержка между кадрами, по умолчанию 13мс.
                        • +
                        • `duration` - длительность анимации в мс.
                        • +
                        • `delta` - функция, которая определяет состояние анимации каждый кадр. Получает часть времени от 0 до 1, возвращает завершенность анимации от 0 до 1. По умолчанию `linear`.
                        • +
                        • `step` - функция, которая отрисовывает состояние анимации от 0 до 1.
                        • +
                        • `complete` - функция для вызова после завершенности анимации.
                        • +
                        • Вызов `animate` возвращает таймер, чтобы анимацию можно было отменить.
                        • +
                        + +Функцию `delta` можно модифицировать, используя трансформации `easeOut/easeInOut`: + +```js +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) return delta(2*progress) / 2; + else return (2 - delta(2*(1-progress))); / 2; + } +} + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress); + } +} +``` + +На основе этой общей анимационной функции можно делать и более специализированные, например `animateProp`, которая анимирует свойство `opts.elem[opts.prop]` от `opts.start px` до `opts.end px` : + +```js +//+ run +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + opts.elem.style[prop] = Math.round(start + (end - start)*delta) + 'px'; + } + return animate(opts); +} + +// Использование: +animateProp({ + elem: document.body, + prop: "width", + start: 0, + duration: 2000, + end: document.body.clientWidth +}); +``` + +Можно добавить еще варианты `delta`, `step`, создать общий фреймворк для анимации с единым таймером и т.п. Собственно, это и делают библиотеки типа jQuery. + + +### Советы по оптимизации + +
                        +
                        Большое количество таймеров сильно нагружают процессор.
                        +
                        Если вы хотите запустить несколько анимаций одновременно, например, показать много падающих снежинок, то управляйте ими с помощью одного таймера. + +Дело в том, что каждый таймер вызывает перерисовку. Поэтому браузер работает гораздо эффективней, если для всех анимаций приходится делать одну объединенную перерисовку вместо нескольких. + +Фреймворки обычно используют один `setInterval` и запускают все кадры в заданном интервале. +
                        +
                        Помогайте браузеру в отрисовке
                        +
                        Браузер управляет отрисовкой дерева и элементы зависят друг от друга. + +Если анимируемый элемент лежит глубоко в DOM, то другие элементы зависят от его размеров и позиции. Даже если анимация не касается их, браузер все равно делает лишние расчёты. + +Для того, чтобы анимация меньше расходовала ресурсы процессора(и была плавнее), не анимируйте элемент, находящийся глубоко в DOM. + +Вместо этого: +
                          +
                        1. Для начала, удалите анимируемый элемент из DOM и прикрепите его непосредственно к `BODY`. Вам, возможно придется использовать `position: absolute` и выставить координаты.
                        2. +
                        3. Анимируйте элемент.
                        4. +
                        5. Верните его обратно в DOM. +
                        + +Эта хитрость поможет выполнять сложные анимации и при этом экономить ресурсы процессора. +
                        +
                        + +Там, где это возможно, стоит использовать CSS-анимацию, особенно на смартфонах и планшетах, где процессор слабоват и JavaScript работает не быстро. + +[head] + + + + +[/head] \ No newline at end of file diff --git a/3-more/2-animation/1-js-animation/back.png b/3-more/2-animation/1-js-animation/back.png new file mode 100755 index 00000000..72b681b3 Binary files /dev/null and b/3-more/2-animation/1-js-animation/back.png differ diff --git a/3-more/2-animation/1-js-animation/bounce-inout.png b/3-more/2-animation/1-js-animation/bounce-inout.png new file mode 100755 index 00000000..e8915745 Binary files /dev/null and b/3-more/2-animation/1-js-animation/bounce-inout.png differ diff --git a/3-more/2-animation/1-js-animation/circ.png b/3-more/2-animation/1-js-animation/circ.png new file mode 100755 index 00000000..912af849 Binary files /dev/null and b/3-more/2-animation/1-js-animation/circ.png differ diff --git a/3-more/2-animation/1-js-animation/circ_ease.png b/3-more/2-animation/1-js-animation/circ_ease.png new file mode 100755 index 00000000..051e5d40 Binary files /dev/null and b/3-more/2-animation/1-js-animation/circ_ease.png differ diff --git a/3-more/2-animation/1-js-animation/elastic.png b/3-more/2-animation/1-js-animation/elastic.png new file mode 100755 index 00000000..243b1bbc Binary files /dev/null and b/3-more/2-animation/1-js-animation/elastic.png differ diff --git a/3-more/2-animation/1-js-animation/linear.png b/3-more/2-animation/1-js-animation/linear.png new file mode 100755 index 00000000..2310ffef Binary files /dev/null and b/3-more/2-animation/1-js-animation/linear.png differ diff --git a/3-more/2-animation/1-js-animation/move.view/index.html b/3-more/2-animation/1-js-animation/move.view/index.html new file mode 100755 index 00000000..ec6f3e03 --- /dev/null +++ b/3-more/2-animation/1-js-animation/move.view/index.html @@ -0,0 +1,13 @@ + + + + + + + + +
                        +
                        +
                        + + diff --git a/3-more/2-animation/1-js-animation/move100.view/index.html b/3-more/2-animation/1-js-animation/move100.view/index.html new file mode 100755 index 00000000..fb33ca3e --- /dev/null +++ b/3-more/2-animation/1-js-animation/move100.view/index.html @@ -0,0 +1,30 @@ + + + + + + + + +
                        +
                        +
                        + + + diff --git a/3-more/2-animation/1-js-animation/quad.png b/3-more/2-animation/1-js-animation/quad.png new file mode 100755 index 00000000..3db3b278 Binary files /dev/null and b/3-more/2-animation/1-js-animation/quad.png differ diff --git a/3-more/2-animation/1-js-animation/quint.png b/3-more/2-animation/1-js-animation/quint.png new file mode 100755 index 00000000..ab34dd49 Binary files /dev/null and b/3-more/2-animation/1-js-animation/quint.png differ diff --git a/3-more/2-animation/1-js-animation/width.view/index.html b/3-more/2-animation/1-js-animation/width.view/index.html new file mode 100755 index 00000000..68385955 --- /dev/null +++ b/3-more/2-animation/1-js-animation/width.view/index.html @@ -0,0 +1,13 @@ + + + + + + + + +
                        +
                        +
                        + + diff --git a/3-more/2-animation/2-bezier/article.md b/3-more/2-animation/2-bezier/article.md new file mode 100644 index 00000000..6e7497fb --- /dev/null +++ b/3-more/2-animation/2-bezier/article.md @@ -0,0 +1,191 @@ +# Кривые Безье + +Кривые Безье используются в компьютерной графике для рисования плавных изгибов, в [CSS-анимации](#css-animation) для описания процесса анимации и много где ещё. + +Тему эту стоит изучить, чтобы в дальнейшем с комфортом пользоваться этим замечательным инструментом. +[cut] + + +## Виды кривых Безье + +[Кривая Безье](http://ru.wikipedia.org/wiki/%D0%9A%D1%80%D0%B8%D0%B2%D0%B0%D1%8F_%D0%91%D0%B5%D0%B7%D1%8C%D0%B5) задаётся опорными точками. + +Их может быть две, три, четыре или больше. Например: + + + + + + + + + + + + +
                        По двум точкамПо трём точкамПо четырём точкам
                        + + + + + +
                        + +Если вы посмотрите внимательно на эти кривые, то "на глазок" заметите: +
                          +
                        1. **Степень кривой равна числу точек минус один**. +На рисунках выше, соответственно, получаются для двух точек -- линейная кривая (прямая), для трёх точек -- квадратическая кривая (парабола), для четырёх -- кубическая.
                        2. +
                        3. **Кривая всегда находится внутри [выпуклой оболочки](http://ru.wikipedia.org/wiki/%D0%92%D1%8B%D0%BF%D1%83%D0%BA%D0%BB%D0%B0%D1%8F_%D0%BE%D0%B1%D0%BE%D0%BB%D0%BE%D1%87%D0%BA%D0%B0), образованной опорными точками:** + + + +Благодаря последнему свойству в компьютерной графике можно оптимизировать проверку пересечений двух кривых. Если их выпуклые оболочки не пересекаются, то и кривые тоже не пересекутся. +
                        4. +
                        + +Основная ценность кривых Безье -- в том, что **кривую можно менять, двигая точки**. При этом **кривая меняется интуитивно понятным образом**. + +Попробуйте двигать точки мышью в примере ниже: + +[iframe src="demo.svg?nocpath=1&p=0,0,0.5,0,0.5,1,1,1" height=370] + +Как можно заметить, **кривая натянута по касательным 1 -> 2 и 3 -> 4.** + +После небольшой практики становится понятно, как расположить точки, чтобы получить нужную форму. А, соединяя несколько кривых, можно получить практически что угодно. + +Вот некоторые примеры: + + + +## Математика + +У кривых Безье есть математическая формула. Как мы увидим далее, в ней нет особенной необходимости, но для полноты картины -- вот она. + +**Координаты кривой описываются в зависимости от параметра `t⋲[0,1]`** + +
                          +
                        • Для двух точек: + +[pre] +P = (1-t)P1 + tP2 +[/pre] +
                        • +
                        • Для трёх точек: + +[pre] +P = (1−t)2P1 + 2(1−t)tP2 + t2P3 +[/pre] +
                        • +
                        • Для четырёх точек: + +[pre] +P = (1−t)3P1 + 3(1−t)2tP2 +3(1−t)t2P3 + t3P4 +[/pre] +
                        • +
                        + +Эти уравнения -- векторные, т.е. вместо Pi нужно подставить координаты i-й опорной точки (xi, yi). + +Формула даёт возможность строить кривые, но не очень понятно, почему они именно такие, и как зависят от опорных точек. С этим нам поможет разобраться другой алгоритм. + +## Рисование "де Кастельжо" + +[Метод де Кастельжо](http://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%B4%D0%B5_%D0%9A%D0%B0%D1%81%D1%82%D0%B5%D0%BB%D1%8C%D0%B6%D0%BE) идентичен математическому определению кривой и наглядно показывает, как она строится. + +Посмотрим его на примере трех точек (точки можно двигать). Нажатие на кнопку "edit" запустит демонстрацию. + +[iframe src="demo.svg?p=0,0,0.5,1,1,0&animate=1" height=370] + +**Алгоритм построения "де Кастельжо":** + +
                          + +
                        1. Строятся отрезки между опорными точками 1-2-3. На рисунке выше они **чёрные**.
                        2. +
                        3. Параметр `t` пробегает значения от `0` до `1`. В примере выше использован шаг `0.05`, т.е. в цикле `0, 0.05, 0.1, 0.15, ... 0.95, 1`. + +Для каждого значения `t`: +
                            +
                          1. На каждом из этих отрезков берётся точка, находящаяся от начала на расстоянии от 0 до `t` пропорционально длине. То есть, при `t=0` -- точка будет в начале, при `t=0.25` -- на расстоянии в 25% от начала отрезка, при `t=0.5` -- 50%(на середине), при `t=1` -- в конце. Так как **чёрных** отрезков -- два, то и точек выходит две штуки.
                          2. +
                          3. Эти точки соединяются. На рисунке ниже соединяющий их отрезок изображён синим. + + + + + + + +
                            При `t=0.25`При `t=0.5`
                            + +
                          4. +
                          5. На получившемся отрезке берётся точка на расстоянии, соответствующем `t`. То есть, для `t=0.25` получаем точку в конце первой четверти отрезка, для `t=0.5` -- в середине отрезка. На рисунке выше эта точка отмечена красным. +
                          6. +
                          +
                        4. +
                        5. +По мере того как `t` пробегает последовательность от `0` до `1`, каждое значение `t` добавляет к кривой точку. **Совокупность таких точек для всех значений `t` образуют кривую Безье.** +
                        6. +
                        +**Это был процесс для построения по трём точкам. Но то же самое происходит и с четырьмя точками.** + + +Демо для четырёх точек (точки можно двигать): + +[iframe src="demo.svg?p=0,0,0.5,0,0.5,1,1,1&animate=1" height=370] + +Алгоритм: +
                          +
                        • Точки по порядку соединяются отрезками: `1-2`, `2-3`, `3-4`. Получается три чёрных отрезка.
                        • +
                        • На отрезках берутся точки, соответствующие текущему `t`, соединяются. Получается два зелёных отрезка.
                        • +
                        • На этих отрезках берутся точки, соответствующие текущему `t`, соединяются. Получается один синий отрезок.
                        • +
                        • На синем отрезке берётся точка, соответствующая текущему `t`. При запуске примера выше она красная.
                        • +
                        • Эти точки описывают кривую.
                        • +
                        + +Нажмите на кнопку "edit" в примере выше, чтобы увидеть это в действии. + +Ещё примеры кривых: + +[iframe src="demo.svg?p=0,0,0,0.75,0.25,1,1,1&animate=1" height=370] + +С другими точками: + +[iframe src="demo.svg?p=0,0,1,0.5,0,0.5,1,1&animate=1" height=370] + +Петелька: + +[iframe src="demo.svg?p=0,0,1,0.5,0,1,0.5,0&animate=1" height=370] + +Пример негладкой кривой Безье: + +[iframe src="demo.svg?p=0,0,1,1,0,1,1,0&animate=1" height=370] + +Аналогичным образом могут быть построены кривые Безье и более высокого порядка: по пяти точкам, шести и так далее. Но обычно используются 2-3 точки, а для сложных линий несколько кривых соединяются. Это гораздо проще с точки зрения поддержки и расчётов. + +[smart header="Как провести кривую через нужные точки?"] +Этот вопрос не связан с кривыми Безье, но он иногда возникает в смежных задачах. + +Такая задача называется [интерполяцией](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%BF%D0%BE%D0%BB%D1%8F%D1%86%D0%B8%D1%8F). Существуют математические формулы, которые подбирают коэффициенты кривой по точкам, исходя из требований, например [многочлен Лагранжа](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%BF%D0%BE%D0%BB%D1%8F%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BC%D0%BD%D0%BE%D0%B3%D0%BE%D1%87%D0%BB%D0%B5%D0%BD_%D0%9B%D0%B0%D0%B3%D1%80%D0%B0%D0%BD%D0%B6%D0%B0). + +Как правило, в компьютерной графике для интерполяции используют кубические кривые, соединённых гладким образом. Вместе они выглядят как одна кривая. Это называется [интерполяция сплайнами](http://ru.wikipedia.org/wiki/%D0%9A%D1%83%D0%B1%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D1%81%D0%BF%D0%BB%D0%B0%D0%B9%D0%BD). +[/smart] +## Итого + +Кривые Безье задаются опорными точками. Мы рассмотрели два определения кривых: +
                          +
                        1. Через математическую формулу.
                        2. +
                        3. Через процесс построения де Кастельжо.
                        4. +
                        + +С их помощью можно описать почти любую линию, особенно если соединить несколько. + +Применение: + +
                          +
                        • В компьютерной графике, моделировании, в графических редакторах. Шрифты описываются с помощью кривых Безье.
                        • +
                        • В веб-разработке -- для графики на Canvas или в формате SVG. Кстати, все живые примеры выше написаны на SVG. Фактически, это один SVG-документ, к которому точки передаются параметрами. Вы можете открыть его в отдельном окне и посмотреть исходник: demo.svg.
                        • +
                        • В CSS-анимации, для задания временной функции.
                        • +
                        \ No newline at end of file diff --git a/3-more/2-animation/2-bezier/bezier2.png b/3-more/2-animation/2-bezier/bezier2.png new file mode 100755 index 00000000..820113dd Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier2.png differ diff --git a/3-more/2-animation/2-bezier/bezier3-draw1.png b/3-more/2-animation/2-bezier/bezier3-draw1.png new file mode 100755 index 00000000..4fbe5db1 Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier3-draw1.png differ diff --git a/3-more/2-animation/2-bezier/bezier3-draw2.png b/3-more/2-animation/2-bezier/bezier3-draw2.png new file mode 100755 index 00000000..bd06a0e4 Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier3-draw2.png differ diff --git a/3-more/2-animation/2-bezier/bezier3-e.png b/3-more/2-animation/2-bezier/bezier3-e.png new file mode 100755 index 00000000..b8639bc6 Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier3-e.png differ diff --git a/3-more/2-animation/2-bezier/bezier3.png b/3-more/2-animation/2-bezier/bezier3.png new file mode 100755 index 00000000..314f6432 Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier3.png differ diff --git a/3-more/2-animation/2-bezier/bezier4-e.png b/3-more/2-animation/2-bezier/bezier4-e.png new file mode 100755 index 00000000..fb9fe70b Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier4-e.png differ diff --git a/3-more/2-animation/2-bezier/bezier4.png b/3-more/2-animation/2-bezier/bezier4.png new file mode 100755 index 00000000..8c3456b5 Binary files /dev/null and b/3-more/2-animation/2-bezier/bezier4.png differ diff --git a/3-more/2-animation/2-bezier/car.jpg b/3-more/2-animation/2-bezier/car.jpg new file mode 100755 index 00000000..fc31600e Binary files /dev/null and b/3-more/2-animation/2-bezier/car.jpg differ diff --git a/3-more/2-animation/2-bezier/demo.svg b/3-more/2-animation/2-bezier/demo.svg new file mode 100755 index 00000000..7dd51ad6 --- /dev/null +++ b/3-more/2-animation/2-bezier/demo.svg @@ -0,0 +1 @@ + 1 t:1 \ No newline at end of file diff --git a/3-more/2-animation/2-bezier/letter_m.png b/3-more/2-animation/2-bezier/letter_m.png new file mode 100755 index 00000000..e95dc0b9 Binary files /dev/null and b/3-more/2-animation/2-bezier/letter_m.png differ diff --git a/3-more/2-animation/2-bezier/vase.png b/3-more/2-animation/2-bezier/vase.png new file mode 100755 index 00000000..97055d18 Binary files /dev/null and b/3-more/2-animation/2-bezier/vase.png differ diff --git a/3-more/2-animation/3-css-animation/1-animate-logo-css/solution.md b/3-more/2-animation/3-css-animation/1-animate-logo-css/solution.md new file mode 100644 index 00000000..9f5ed693 --- /dev/null +++ b/3-more/2-animation/3-css-animation/1-animate-logo-css/solution.md @@ -0,0 +1,28 @@ +# Алгоритм + +Анимируйте одновременно свойства `left/top` и `width/height`. + +Чтобы в процессе анимации таблица сохраняла геометрию -- создайте на месте `IMG` временный `DIV` фиксированного размера и переместите `IMG` внутрь него. После анимации можно вернуть как было. + +Для начала анимации - добавьте класс изображению: + +```css +.growing { + /* все свойства анимируются 3 секунды */ + -webkit-transition: all 3s; + -moz-transition: all 3s; + -o-transition: all 3s; + -ms-transition: all 3s; +} +``` + +При этом, чтобы анимация началась, может понадобиться отложить установку класса и новых свойств через `setTimeout(.., 0)`. + +Для отлова конца анимации используйте событие `onTransitionEnd`. Оно сработает несколько раз, для каждого свойства, поэтому чтобы обработчик не вывел "OK" много раз -- можно обрабатывать окончание только при одном `event.propertyName`. + +# Похожая задача +Аналогичная задача, решённая средствами JS: [](/task/animate-logo). + +# Решение + +[edit src="solution" task/] \ No newline at end of file diff --git a/3-more/2-animation/3-css-animation/1-animate-logo-css/solution.view/index.html b/3-more/2-animation/3-css-animation/1-animate-logo-css/solution.view/index.html new file mode 100755 index 00000000..87165c7e --- /dev/null +++ b/3-more/2-animation/3-css-animation/1-animate-logo-css/solution.view/index.html @@ -0,0 +1,106 @@ + + + + + + + + + +Кликните картинку для анимации. Расположение элементов при анимации не должно меняться! + + + + + + +
                        Догнать +..и перегнать!
                        + +В процессе анимации повторные клики на изображение игнорировать. + + + + + + + diff --git a/3-more/2-animation/3-css-animation/1-animate-logo-css/source.view/animate.js b/3-more/2-animation/3-css-animation/1-animate-logo-css/source.view/animate.js new file mode 100755 index 00000000..51bb38c6 --- /dev/null +++ b/3-more/2-animation/3-css-animation/1-animate-logo-css/source.view/animate.js @@ -0,0 +1,92 @@ +/** + Docs: http://learn.javascript.ru/tutorial/lib +*/ + +function animate(opts) { + + var start = new Date; + var delta = opts.delta || linear; + + var timer = setInterval(function() { + var progress = (new Date - start) / opts.duration; + + if (progress > 1) progress = 1; + + opts.step( delta(progress) ); + + if (progress == 1) { + clearInterval(timer); + opts.complete && opts.complete(); + } + }, opts.delay || 13); + + return timer; +} + +/** + Анимация стиля opts.prop у элемента opts.elem + от opts.from до opts.to продолжительностью opts.duration + в конце opts.complete + + Например: animateProp({ elem: ..., prop: 'height', start:0, end: 100, duration: 1000 }) +*/ +function animateProp(opts) { + var start = opts.start, end = opts.end, prop = opts.prop; + + opts.step = function(delta) { + var value = Math.round(start + (end - start)*delta); + opts.elem.style[prop] = value + 'px'; + } + return animate(opts); +} + +// ------------------ Delta ------------------ + +function elastic(progress) { + return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*1.5/3*progress); +} + +function linear(progress) { + return progress; +} + +function quad(progress) { + return Math.pow(progress, 2); +} + +function quint(progress) { + return Math.pow(progress, 5); +} + +function circ(progress) { + return 1 - Math.sin(Math.acos(progress)); +} + +function back(progress) { + return Math.pow(progress, 2) * ((1.5 + 1) * progress - 1.5); +} + + +function bounce(progress) { + for(var a = 0, b = 1, result; 1; a += b, b /= 2) { + if (progress >= (7 - 4 * a) / 11) { + return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2); + } + } +} + +function makeEaseInOut(delta) { + return function(progress) { + if (progress < .5) + return delta(2*progress) / 2; + else + return (2 - delta(2*(1-progress))) / 2; + } +} + + +function makeEaseOut(delta) { + return function(progress) { + return 1 - delta(1 - progress); + } +} diff --git a/3-more/2-animation/3-css-animation/1-animate-logo-css/source.view/index.html b/3-more/2-animation/3-css-animation/1-animate-logo-css/source.view/index.html new file mode 100755 index 00000000..41a246e6 --- /dev/null +++ b/3-more/2-animation/3-css-animation/1-animate-logo-css/source.view/index.html @@ -0,0 +1,31 @@ + + + + + + + + + +Кликните картинку для анимации. Расположение элементов при анимации не должно меняться! + + + + + + +
                        Догнать +..и перегнать!
                        + +В процессе анимации повторные нажатия на изображение игнорируются. + + + + + + + diff --git a/3-more/2-animation/3-css-animation/1-animate-logo-css/task.md b/3-more/2-animation/3-css-animation/1-animate-logo-css/task.md new file mode 100644 index 00000000..175ee0a8 --- /dev/null +++ b/3-more/2-animation/3-css-animation/1-animate-logo-css/task.md @@ -0,0 +1,12 @@ +# Анимировать лого (CSS) + +[importance 5] + +Реализуйте анимацию, как в демке ниже (клик на картинку): +[iframe src="solution" height=350] + +Продолжительность анимации: 3 секунды. + +Для анимации использовать CSS, по окончании вывести "ок". + + [edit src="source" task/] diff --git a/3-more/2-animation/3-css-animation/article.md b/3-more/2-animation/3-css-animation/article.md new file mode 100644 index 00000000..2dd69883 --- /dev/null +++ b/3-more/2-animation/3-css-animation/article.md @@ -0,0 +1,270 @@ +# CSS-анимация + +Все современные браузеры, кроме IE<10 поддерживают CSS transitions, которые позволяют реализовать анимацию средствами CSS, без привлечения JavaScript. + +Большинство примеров из этой статьи не будут работать в IE<10. +[cut] + +## Анимация свойства [#css-animation] + +Идея проста. Вы указываете, что некоторое свойство будет анимироваться при помощи специальных CSS-правил. Далее, при изменении этого свойства, браузер сам обработает анимацию. + +Например, CSS, представленный ниже, 2 секунды анимирует свойство `background-color`. + +```css +.animated { + transition-property: background-color; + transition-duration: 2s; +} +``` + +Любое изменение фонового цвета будет анимироваться в течение 2-х секунд. + +У свойства `"transition"` есть и короткая запись: + +```css +.animated { + transition: background-color 2s; +} +``` + +Так как [стандарт CSS Transitions](http://www.w3.org/TR/css3-transitions/) находится в стадии разработки, то `transition` нужно снабжать браузерными префиксами. + +### Пример +Этот пример работает во всех современных браузерах, не работает в IE<10. + +```html + +
                        + Кликни меня +
                        +``` + + +
                        + Кликни меня +
                        + +CSS-анимации особенно рекомендуются на мобильных устройствах. Они отрисовываются плавнее, чем JavaScript, и меньше нагружают процессор, так как используют графическую акселерацию. + + +## Полный синтаксис CSS + +Свойства для CSS-анимаций: +
                        +
                        `transition-property`
                        +
                        Список свойств, которые будут анимироваться. Анимировать можно не все свойства, но [многие](http://www.w3.org/TR/css3-transitions/#animatable-properties-). Значение `all` означает "анимировать все свойства".
                        +
                        `transition-duration`
                        +
                        Продолжительность анимации. Если указано одно значение -- оно применится ко всем свойствам, можно указать несколько значений для разных `transition-property`.
                        +
                        `transition-timing-function`
                        +
                        [Кривая Безье](/bezier) по 4-м точкам, используемая в качестве временной функциии.
                        +
                        `transition-delay`
                        +
                        Указывает задержку от изменения свойства до начала CSS-анимации.
                        +
                        + +Свойство **`transition`** может содержать их все, в порядке: `property duration timing-function delay, ...`. + +### Пример + +Анимируем одновременно цвет и размер шрифта: + +```html + + +``` + + + + +## Временнáя функция + +В качестве временной функции можно выбрать любую [кривую Безье](/bezier), удовлетворяющую условиям: +
                          +
                        1. Начальная точка `(0,0)`.
                        2. +
                        3. Конечная точка `(1,1)`.
                        4. +
                        5. Для промежуточных точек значения `x` должны быть в интервале `0..1`.
                        6. +
                        + +Синтаксис для задания кривой Безье в CSS: `cubic-bezier(x2, y2, x3, y3)`. В нём указываются координаты второй и третьей точек, так как первая и последняя фиксированы. + +Например, торможение можно описать кривой Безье: `cubic-bezier(0.0, 0.5, 0.5 ,1.0)`. + +График этой кривой: + + + +Вы можете увидеть эту временную функцию в действии, кликнув на поезд: + +```html + + +``` + + + + + +Существуют и несколько стандартных кривых: `linear`, `ease`, `ease-in`, `ease-out` и `ease-in-out`. + +Значение `linear` -- это прямая, равномерное изменение. Оно используется по умолчанию. + +Остальные кривые являются короткой записью следующих `cubic-bezier`. В таблице ниже показано соответствие: + + + + + + + + + + + + + + + + + + + + +
                        `ease``ease-in``ease-out``ease-in-out`
                        `(0.25, 0.1, 0.25, 1.0)``(0.42, 0, 1.0, 1.0)``(0, 0, 0.58, 1.0)``(0.42, 0, 0.58, 1.0)`
                        + +Наиболее близкий стандартный вариант для примера с поездом -- `ease-out`: + +```css +.train { + -moz-transition: left 5s ease-out; + ... +} +``` + +## CSS-преобразования + +Браузеры, которые поддерживают CSS-анимацию, поддерживают и [CSS-преобразования](https://developer.mozilla.org/en/CSS/Using_CSS_transforms). + +С их помощью можно сделать много красивых эффектов. Например, вращение: + +```js +//+ run + +document.body.style.MozTransition = "all 5s"; +document.body.style.MozTransform = "rotate(360deg)"; +document.body.style.WebkitTransition = "all 5s"; +document.body.style.WebkitTransform = "rotate(360deg)"; +document.body.style.OTransition = "all 5s"; +document.body.style.OTransform = "rotate(360deg)"; +document.body.style.MsTransition = "all 5s"; +document.body.style.MsTransform = "rotate(360deg)"; + +document.body.style.Transition = "all 5s"; +document.body.style.Transform = "rotate(360deg)"; +``` + +Самое замечательное -- все эти эффекты используют графический ускоритель и почти не нагружают процессор. + +Все браузеры, кроме IE<10 поддерживают это, ну а в IE может быть что-то через JavaScript или вообще без анимации. + +## Событие transitionend + +На конец CSS-анимации можно повесить обработчик. Его стандартное имя: `transitionend`, но браузерные префиксы требуются и тут. + +Кликните на лодочку: + +[iframe src="boat" height=300 edit link] + +Её анимация осуществляется функцией `go`, которая перезапускается по окончании (с переворотом через CSS). + +```js +... + go(); + + elem.addEventListener('transitionend', go, false); /* на будущее */ + elem.addEventListener('webkitTransitionEnd', go, false); + elem.addEventListener('mozTransitionEnd', go, false); + elem.addEventListener('oTransitionEnd', go, false); + elem.addEventListener('msTransitionEnd', go, false); +... +``` + +Объект события `transitionend` также содержит свойства: +
                        +
                        `propertyName`
                        +
                        Свойство, анимация которого завершилась.
                        +
                        `elapsedTime`
                        +
                        Время (в секундах), которое заняла анимация, без учета `transition-delay`.
                        +
                        + +Свойство `propertyName` может быть полезно при одновременной анимации нескольких свойств. Каждое свойство даст своё событие, и можно решить, что с ним делать дальше. + +## Ограничения и достоинства CSS-анимаций + +[compare] +-Основное ограничение -- это то, что временная функция может быть задана только кривой Безье. Более сложные анимации, состоящие из нескольких кривых, реализуются при помощи [CSS animations](http://dev.w3.org/csswg/css3-animations/#animation-name-property) (стандарт пока не готов). +-CSS-анимации касаются только свойств, а в JavaScript можно делать всё, что угодно. +-Отсутствует поддержка в IE9- ++Простые вещи делаются просто. Особенно удобно, если от отсутствия эффекта в IE проблем не возникнет. ++Гораздо "легче" для процессора, чем анимации JavaScript. Лучше используется графический ускоритель. Это очень важно для мобильных устройств. +[/compare] + +[head] + + +[/head] \ No newline at end of file diff --git a/3-more/2-animation/3-css-animation/boat.view/index.html b/3-more/2-animation/3-css-animation/boat.view/index.html new file mode 100755 index 00000000..c9dc7a36 --- /dev/null +++ b/3-more/2-animation/3-css-animation/boat.view/index.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + diff --git a/3-more/2-animation/3-css-animation/ease-in-out.png b/3-more/2-animation/3-css-animation/ease-in-out.png new file mode 100755 index 00000000..5b875894 Binary files /dev/null and b/3-more/2-animation/3-css-animation/ease-in-out.png differ diff --git a/3-more/2-animation/3-css-animation/ease-in.png b/3-more/2-animation/3-css-animation/ease-in.png new file mode 100755 index 00000000..2610c5a8 Binary files /dev/null and b/3-more/2-animation/3-css-animation/ease-in.png differ diff --git a/3-more/2-animation/3-css-animation/ease-out.png b/3-more/2-animation/3-css-animation/ease-out.png new file mode 100755 index 00000000..9e631a38 Binary files /dev/null and b/3-more/2-animation/3-css-animation/ease-out.png differ diff --git a/3-more/2-animation/3-css-animation/ease.png b/3-more/2-animation/3-css-animation/ease.png new file mode 100755 index 00000000..2f57ee34 Binary files /dev/null and b/3-more/2-animation/3-css-animation/ease.png differ diff --git a/3-more/2-animation/3-css-animation/train-curve.png b/3-more/2-animation/3-css-animation/train-curve.png new file mode 100755 index 00000000..bb10d5f4 Binary files /dev/null and b/3-more/2-animation/3-css-animation/train-curve.png differ diff --git a/3-more/2-animation/index.md b/3-more/2-animation/index.md new file mode 100644 index 00000000..d940e7df --- /dev/null +++ b/3-more/2-animation/index.md @@ -0,0 +1,2 @@ +# Анимация + diff --git a/3-more/3-jquery-stub/1-jquery-intro/article.md b/3-more/3-jquery-stub/1-jquery-intro/article.md new file mode 100644 index 00000000..d390a3dd --- /dev/null +++ b/3-more/3-jquery-stub/1-jquery-intro/article.md @@ -0,0 +1,47 @@ +# Введение + +Эта глава содержит основную информацию, которая нужна, чтобы при помощи jQuery работать с документом. + +Я также постараюсь изложить в ней структуру методов и основные особенности jQuery, которые не очевидны из документации. + +**Глава находится "в работе". Это означает, что раскрыты далеко не все темы, а то, что раскрыто, является скорее очерком, который постепенно обрастает "мясом" разъяснений и задач.** + + +[cut] +## О библиотеке jQuery + +[jQuery](http://jquery.com) -- библиотека, которая позволяет делать код короче, а также устраняет наиболее одиозные кросс-браузерные различия. + +Она не содержит встроенных архитектурных решений и, таким образом, не навязывает свой стиль. Просто делает код короче. За это её и любим. + +## Подключаем jQuery + +Библиотеку jQuery можно скачать себе на диск с сайта [](http://jquery.com), а можно -- вставить в документ, используя известные интернет адреса: + +
                          +
                        • По адресу [](http://code.jquery.com/jquery-latest.js) -- доступна всегда последняя версия
                        • +
                        • С сети Google: [](https://developers.google.com/speed/libraries/devguide?hl=ru#jquery) можно загрузить любую из не слишком старых версий. Синтаксис такой: `src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"`, где `1.8.3` -- версия, причём можно указать её приблизительно: `1.8` означает последнюю версию вида `1.8.*`, а `1` -- последнюю версию вида `1.*`. Файл `jquery.min.js` обозначает сжатый код, а `jquery.js` -- несжатый, для удобства отладки.
                        • +
                        + +Например, подключим на страницу последнюю версию jQuery в несжатом виде и проверим её работу: + +```html + + + + +``` + +Или так: + +```html + + + + +``` + diff --git a/3-more/3-jquery-stub/2-jquery-search/article.md b/3-more/3-jquery-stub/2-jquery-search/article.md new file mode 100644 index 00000000..d056509d --- /dev/null +++ b/3-more/3-jquery-stub/2-jquery-search/article.md @@ -0,0 +1,294 @@ +# jQuery: поиск элементов + +Когда-то давным давно, когда деревья были большими, один молодой парень по имени Джон Ресиг решил создать библиотеку для поиска элементов в DOM. +[cut] + +## Немного истории + +Браузеры тогда не давали возможности искать по CSS-селекторам, поэтому библиотеку назвали "jQuery" (**J**avaScript **Query**). + +Поиск получился хорошо, и Джон стал прикручивать к библиотеке новые возможности, и со временем jQuery стала такой, как мы видим её сейчас. + +[smart header="Sizzle"] +...А функционал поиска со временем был выделен в отдельный проект: [движок Sizzle](http://sizzlejs.com/). + +Это позволяет взять от jQuery только поиск и встроить в другую библиотеку. Например, фреймворк [Dojo Toolkit](http://dojotoolkit.org) можно собрать со своим поиском, а можно и с Sizzle. +[/smart] + +## Поиск в jQuery + +"Сердцем" jQuery является функция `$()`. Все вызовы делаются через `$`. Это самая обычная функция, которая объявлена библиотекой. Как мы помним, символ `"$"` является допустимым символом для имён в JavaScript. + +Простейший поиск элементов выглядит так: + +```js +var result = $('...CSS-селектор...'); +``` + +Например, `$("div a")` -- ссылки `a` внутри `div`. + +Возможны и более сложные запросы, например: + +```js +var list = $('li > a:odd:not([href^="http://"])'); +// расшифровка: нечётные (:odd) ссылки внутри li, +// кроме тех (:not), у которых атрибут href начинается c http:// +``` + +Вызов `$(селектор)` аналогичен `document.querySelectorAll(селектор)`, но: +
                          +
                        1. Во-первых, поддерживается всеми браузерами, включая старые IE. Список CSS3-селекторов можно найти в главе [](/css-selectors).
                        2. +
                        3. Во-вторых, jQuery предоставляет свои поисковые расширения, например псевдофильтр `:checked` -- выбранные элементы `input` и `option`, `:header` -- заголовок `h1..6`, и другие, более полный список которых доступен [в документации](http://api.jquery.com/category/selectors/).
                        4. +
                        + +[warn header="Поиск: `querySelectorAll` и Sizzle"] + +При выполнении выборки jQuery сначала определяет, имеет ли селектор простой вид, например `#menu` или `body`, то есть можно ли получить результат сразу вызовом `getElementById` или через `document.body`. Проверяется и ряд других простейших случаев. Если да -- отлично, если нет -- jQuery пытается получить результат вызовом `querySelectorAll`. + +Если и `querySelectorAll` не срабатывает, то jQuery задействует свой поисковой движок [Sizzle](http://sizzlejs.com). + +Обычно "не срабатывает", когда либо браузер старый, либо запрос использует jQuery-расширения, не входящие в CSS3. Стоит ли говорить, что поиск по Sizzle обычно медленнее, чем встроенный браузерный? Поэтому к использованию jQuery-расширений стоит подходить осторожно и не применять их в "узких" местах. +[/warn] + +## Перебор результатов + +Результатом поиска является jQuery-объект. Он похож на массив: в нём есть нумерованные элементы и `length`. + +jQuery-объект также называют "jQuery-коллекцией", "элементами, обёрнутыми в jQuery" и десятком других жаргонных терминов. + +Используем jQuery, чтобы выбрать все элементы по селектору `li > a` и перебрать их: + +```html + + + + + + + +``` + +## Поиск внутри элемента + +Для поиска внутри какого-либо элемента -- можно передать его вторым аргументом `$`. + +Например, найдём все `a` внутри `#menu`: + +```js +var menu = document.getElementById('menu'); +*!* +$('a', menu); // поиск аналогичен menu.querySelectorAll('a') +*/!* +``` + +Второй аргумент в этом случае называются "контекстом поиска". + +В качестве контекста можно передать не только DOM-элемент, но и селектор: + +```js + +$('a', '#menu' ); +``` + +Также можно передать результат другого поиска: + +```js +var menu = $('#menu'); +$('a', menu); +``` + +**Не важно, в каком виде мы хотим указать контекст: DOM-элемент, строка или результат поиска -- jQuery понимает всё.** + +А что, если контекст поиска содержит много элементов? Например, как будет работать запрос `$('a', 'li')`, если `
                      • ` в документе много? + +Здесь все немного сложнее, но, тем не менее, интуитивно понятно. + +**Если в контексте много элементов, то поиск будет произведён в каждом из них. А затем результаты будут объединены.** + +Повторы элементов при этом отфильтровываются, то есть два раза один и тот же элемент в результате не появится. + +Например, найдём `$('a', 'li')` в многоуровневом списке: + +```html + + + + + + +``` + +## Метод each + +Для более удобного перебора у jQuery-коллекции есть метод [each](http://api.jquery.com/each/). Его синтаксис похож на [forEach](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach) массива: + +```js +.each( function(index, item) ) +``` + +Он выполняет для каждого элемента коллекции перед точкой функцию-аргумент, и передаёт ей номер `index` и очередной элемент `item`. + +Используем его вместо `for`, чтобы перебрать коллекцию найденных ссылок: + +```js +$('li a').each(function(i, a) { + alert( i + ": " + a.href); +}); +``` + +**У `.each` есть важная возможность, которой нет в `forEach`: возврат `false` из функции прекращает перебор.** + +Например: + +```html + + + +Википедия + + + + +``` + +**При переборе `each`, текущий элемент передаётся как `this`.** + +Можно было бы использовать это, чтобы сделать код короче: + +```js +$('li a').each(function(i) { + alert(i + ': ' + *!*this*/!*.innerHTML); + + if (i == 1) return false; +}); +``` + +## Получение конкретного элемента + +Даже если элемент найден только один, всё равно результатом поиска будет jQuery-коллекция. + +Если нам нужен один элемент из неё -- есть много способов его получить. Рассмотрим основные: + +
                          +
                        1. **Прямой доступ по номеру:** + +```js +//+ run +alert( $('body')[0] ); // BODY +``` + +Это -- самый быстрый и прямой доступ, он использует внутреннюю структуру jQuery-коллекции: элементы там хранятся по индексам. Но этим он нарушает принцип инкапсуляции. + +Вообще-то, в jQuery есть специальные методы для получения элемента по номеру, и сейчас мы будем говорить о них. + +
                        2. +
                        3. **Метод [get(индекс)](http://api.jquery.com/get/), работает так же, как прямой доступ:** + +```js +//+ run +alert( $('body').get(0) ); // BODY +``` + +Если элемента с таким номером нет -- вызов `get` возвратит `undefined`. + +
                        4. +
                        5. **Метод [eq(индекс)](http://api.jquery.com/eq/) возвращает коллекцию из одного элемента -- с данным номером.** + +Он отличается от метода [get(индекс)](http://api.jquery.com/get/) и прямого обращения по индексу тем, что возвращает именно jQuery-коллекцию с одним элементом, а не сам элемент. + +Например: + +```js +// DOM-элемент для первой ссылки +$('a').get(0); + +// jQuery-объект из одного элемента: первой ссылки +$('a').eq(0); +``` + +Во втором случае вызов `eq` создаёт новую jQuery-коллекцию, добавляет в нее нулевой элемент и возвращает. Это удобно, если мы хотим дальше работать с этим элементом, используя методы jQuery. + +Если элемента с таким номером нет -- `eq` возвратит пустую коллекцию. +
                        6. +
                        + + +## "Сцепление" вызовов + +Почти все методы jQuery-объекта возвращают jQuery-объект -- либо новый, либо тот на котором вызваны. + +**Поэтому можно вызывать методы один за другим.** + +Это называют *"сцепление"* вызовов, или *"чейнинг"* (от англ. "chaining"): + +```js +$('li a[href$=".pdf"]') // ссылки, оканчивающиеся на pdf +*!* + .each(function() { +*/!* + this.className = "pdf"; // дать им класс + }) +*!* + .each(function() { +*/!* + alert(this.href); // вывести + }); +``` + +Для удобства чтения каждый новый вызов в чейнинге идёт с отступом и с новой строки. + +## Итого + +Для поиска элементов можно вызвать `$(CSS-селектор[, контекст])`. Результатом будет jQuery-объект, который можно: +
                          +
                        • Перебирать как массив.
                        • +
                        • Перебирать методом [each](http://api.jquery.com/each/).
                        • +
                        • Получить один элемент прямым доступом по индексу, либо вызовом [get](http://api.jquery.com/get/) или [eq](http://api.jquery.com/eq/).
                        • +
                        + +Для поиска используется, по возможности, `querySelectorAll`, либо если он не может обработать запрос -- встроенный движок Sizzle. + + diff --git a/3-more/3-jquery-stub/3-jquery-traversal/1-select-parents-with-self/solution.md b/3-more/3-jquery-stub/3-jquery-traversal/1-select-parents-with-self/solution.md new file mode 100644 index 00000000..290f6050 --- /dev/null +++ b/3-more/3-jquery-stub/3-jquery-traversal/1-select-parents-with-self/solution.md @@ -0,0 +1,6 @@ + + +```js +var result = $(elem).parents().addBack(); +``` + diff --git a/3-more/3-jquery-stub/3-jquery-traversal/1-select-parents-with-self/task.md b/3-more/3-jquery-stub/3-jquery-traversal/1-select-parents-with-self/task.md new file mode 100644 index 00000000..7009d5fc --- /dev/null +++ b/3-more/3-jquery-stub/3-jquery-traversal/1-select-parents-with-self/task.md @@ -0,0 +1,19 @@ +# Выберите всех родителей вместе с элементом + +[importance 5] + +Есть элемент `elem`. Выберите в jQuery всех его родителей и сам элемент. + +Например, для `LI` из примера выше должна получиться коллекция `HTML, BODY, UL, LI`: + +```html + + +
                          +
                        • Текст
                        • +
                        + + +``` + +Код должен быть как можно короче. \ No newline at end of file diff --git a/3-more/3-jquery-stub/3-jquery-traversal/article.md b/3-more/3-jquery-stub/3-jquery-traversal/article.md new file mode 100644 index 00000000..d1ad40cb --- /dev/null +++ b/3-more/3-jquery-stub/3-jquery-traversal/article.md @@ -0,0 +1,458 @@ +# Навигация по jQuery-коллекции + +Мы умеем перебирать коллекцию, получать нужный элемент по номеру. Достаточно ли этого? + +Конечно нет! По коллекции нужно путешествовать. Получать детей, искать родителей, выбирать элементы по условию, и чтобы это было удобно. +[cut] + +## Шпаргалка по методам + +Методов для навигации по коллекции много. Их гораздо проще воспринять, если разбить на группы. + +Итак, на картинке ниже: +
                          +
                        • По центру -- действия с текущей коллекцией: фильтрация, добавление, изменение и т.п.
                        • +
                        • Сверху -- методы для доступа к родителям.
                        • +
                        • Снизу -- для поиска среди потомков.
                        • +
                        • Слева/справа -- для поиска соседей.
                        • +
                        + +[pre] + + + + + + + + + + + + + + + + + + + +
                          +parent +parents +parentsUntil
                        +closest +
                         
                        +prev +prevAll +prevUntil
                        +siblings +
                        +filter +not +has
                        +eq +first +last +slice
                        +is +add +map +
                        +next +nextAll +nextUntil
                        +siblings +
                          +find
                        +children +contents +
                         
                        +[/pre] + +Так как методов много, то запомнить их все сложно, да и не нужно. Достаточно знать, какие есть, а при необходимости -- обратиться к этой странице или к [официальной документации](http://api.jquery.com/category/traversing/). + +У этих методов очень хорошие названия, они станут очевидны, как только мы их рассмотрим чуть подробнее. + +## По текущей коллекции + +
                        +
                        filter(фильтр), not(фильтр), has(фильтр)
                        +
                        Возвращают отфильтрованную коллекцию, содержащую только элементы... +
                          +
                        • для `filter` -- подходящие под фильтр,
                        • +
                        • для `not` -- не подходящие под фильтр,
                        • +
                        • для `has` -- содержащие потомка, подходящего под фильтр.
                        • +
                        + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +В качестве фильтра может быть использована также функция, принимающая элемент как `this` и возвращающая `true/false`: + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +
                        +
                        eq(n), first, last, slice(start, end)
                        +
                        Возвращает новую коллекцию... +
                          +
                        • `eq(n)` -- с одним элементом по его номеру,
                        • +
                        • `first/last` -- из первого/последнего элемента,
                        • +
                        • `slice(start[, end])` -- из элементов с номера `start` до `end`.
                        • +
                        +
                        +
                        is(фильтр)
                        +
                        Возвращает `true`, если *какой-нибудь* элемент из коллекции подходит под фильтр. + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +
                        +
                        add(элементы) +
                        Возвращает новую коллекцию из элементов текущей и новых, по селектору. + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +Важно: эта функция не меняет текущую коллекцию, она создаёт новую из существующих и добавленных элементов. + +
                        +
                        map(функция)
                        +
                        Этот метод стоит особняком. Он не меняет коллекцию, не ищет в ней, а пропускает её через функцию и возвращает результаты. + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +
                        +
                        + +Заметим две приятные особенности: +
                          +
                        • Большинство методов, которые осуществляют фильтрацию, могут принимать как селектор так и фильтрующую функцию. jQuery любит функции.
                        • +
                        • Большинство методов, которые принимают элементы, могут получать их в виде jQuery-коллекции или селектора. Главное, чтобы найти по этому можно было.
                        • +
                        + +## По родителям + +
                        +
                        [parent()](http://api.jquery.com/parent/), [parents(фильтр)](http://api.jquery.com/parents/), [parentsUntil(стоп, фильтр)](http://api.jquery.com/parentsUntil/)
                        +
                        Родители -- один `parent`, все `parents(фильтр)` (с фильтром по селектору) и до определённого `parentsUntil(где остановиться, селектор для элементов)`.
                        +
                        [closest(фильтр, элемент-контейнер)](http://api.jquery.com/closest/)
                        +
                        Ищет одного, ближайшего, родителя, подходящего под селектор. + +Второй, необязательный, аргумент `элемент-контейнер`, если он передан, ограничивает поиск. jQuery будет идти вверх до тех пор, пока не встретит этот DOM-элемент. + +```html + + + + + + +``` + +
                        +
                        + +## По потомкам + +
                        +
                        [find(селектор)](http://api.jquery.com/find/)
                        +
                        Ищет в потомках по селектору. + +```js +// для каждого LI искать CODE среди потомков, вернуть их коллекцию +$('li').find('code'); +``` + +
                        +
                        [children(селектор)](http://api.jquery.com/children/), [contents()](http://api.jquery.com/contents/)
                        +
                        Выбирает детей по селектору, без аргументов -- всех детей: + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +Метод [contents()](http://api.jquery.com/contents/) -- также возвращает детей, но в отличие от `children` -- узлы всех типов, включая текстовые и комментарии, а не только узлы-элементы. + +```html + +
                          +
                        • filter: только подходящие.
                        • +
                        • not: не подходящие.
                        • +
                        • has: по потомкам.
                        • +
                        • ...
                        • +
                        + + +``` + +
                        +
                        + +## По соседям + +
                        +
                        prev(), prevAll(фильтр), prevUntil(элемент, фильтр)
                        +
                        Получить левого соседа `prev`, всех левых соседей `prevAll` или всех левых соседей `prevUntil` до указанного (`элемент`), подходящих под фильтр.
                        +
                        next(), nextAll(фильтр), nextUntil(элемент, фильтр)
                        +
                        То же самое, но правые соседи.
                        +
                        siblings()
                        +
                        Получить коллекцию всех соседей.
                        +
                        + + +## Стек селекторов: методы end и addBack + +Все методы не влияют на текущую коллекцию, они создают и возвращают новую. + +При каждом новом поиске возвращается jQuery-объект с результатом. + +**Предыдущий jQuery-объект при этом не теряется, к нему всегда можно вернуться вызовом [end()](http://api.jquery.com/end/).** + +Посмотрим это на примере задачи. Допустим, мы нашли форму `$('form')` и хотим выбрать все чекбоксы в ней. + +Можно сделать это так: + +```js +$('form') + .find(':checkbox') + .each(function() { ... }); // сделать что-то с элементами коллекции +``` + +...И теперь хотим поискать в этой же форме что-то ещё. + +Самый очевидный способ это сделать -- сохранить `$('form')` в переменной: + +```js +var form = $('form'); + +form + .find(':checkbox') + .each(function() { ... }); + +form + .find(':radio') + .each(function() { ... }); +``` + +...Но на самом деле в этом нет необходимости. + +jQuery, при вызове `find` сохраняет предыдущую найденную коллекцию, к которой можно вернуться вызовом `end()`: + +```js +$('form') + .find(':checkbox') // нашли чекбоксы внутри + .each(function() { ... }) // обработали +*!* + .end() // вернулись обратно на форму +*/!* + .find(':radio') // поискали другие элементы внутри.. + .each(function() { ... }); // сделали с ними что-то ещё +``` + +**Метод [addBack(selector)](http://api.jquery.com/addBack/) добавляет элементы из предыдущей коллекции к текущей.** + +Если указать селектор, то он отфильтрует их. + +Этот метод бывает очень удобен, когда какую-то операцию нужно сделать как с детьми, так и с самим элементом, например: + +```js +$('ul') // коллекция: UL + .children() // получить коллекцию LI + .addBack() // добавить к ней сам UL + .each(...) // теперь у нас коллекция LI и UL-родитель +``` + +Полный список методов вы найдёте в [разделе Traversing](http://api.jquery.com/category/traversing/) документации. При использовании jQuery, вы часто будете иметь с ними дело и отлично запомните. + +## Неэффективность jQuery + +Для ряда задач jQuery-методы поиска по коллекции неэффективны. + +Например, нужно найти первого потомка. Некоторые способы: + +```js +elem.children(':first'); +elem.children().first(); +$(':first-child', elem); +``` + +Все эти способы неэффективны. Особенно первые два. + +
                          +
                        1. Первый проходит всех детей, по псевдо-фильтру выбирая нужного.
                        2. +
                        3. Второй копирует детей в коллекцию, а потом получает из неё первый элемент.
                        4. +
                        5. Третий запускает `querySelectorAll(':first-child)` (на самом деле там чуть сложнее, но не суть важно) в контексте `elem`. Здесь, с первого взгляда, нет копирования всех детей, но внутренний алгоритм браузера для выполнения `querySelectorAll` всё равно работает полным перебором. Впрочем, это будет намного быстрее, чем предыдущие решения.
                        6. +
                        + +Обычный же вызов `elem[0].children[0]` на порядки обгонит все вышеприведённые способы, особенно если детей много. + +Какой вывод из этого? + +**Там, где потеря в производительности некритична, используем jQuery -- для удобства. Это большинство случаев. Там, где она важна -- обращаемся к обычному DOM.** + + + + + + + + + diff --git a/3-more/3-jquery-stub/4-jquery-dom/article.md b/3-more/3-jquery-stub/4-jquery-dom/article.md new file mode 100644 index 00000000..a6599891 --- /dev/null +++ b/3-more/3-jquery-stub/4-jquery-dom/article.md @@ -0,0 +1,45 @@ +# Методы для работы с DOM [в работе] + +jQuery-коллекции предоставляют полный набор методов для DOM-манипуляций. И кое-что сверху... + +[cut] + +## Классы и стили + +
                          +
                        • addClass, hasClass, removeClass
                        • +
                        + +attr +prop +css + +Таблица методов: + + + +
                        +
                        .append(parent)
                        +
                        Вставить последним потомком `parent`
                        +
                        .prepend(parent)
                        +
                        Вставить первым потомком `parent`
                        +
                        .insertAfter(sibling)
                        +
                        Вставить сразу после элемента `sibling`
                        +
                        .insertBefore(sibling)
                        +
                        Вставить сразу перед элементом `sibling`
                        +
                        .remove()
                        +
                        Удалить элемент из DOM, удалить связанные с ним данные и обработчики событий.
                        +
                        .detach()
                        +
                        Удалить элемент из DOM, не удаляя связанные с ним данные и обработчики.
                        +
                        .empty()
                        +
                        Удалить всех потомков вызовом remove
                        + +
                        + +Зеркальные методы: +
                          +
                        • append -> appendTo
                        • +
                        • prepend -> prependTo
                        • +
                        • insertAfter -> after
                        • +
                        • insertBefore -> before
                        • + diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.md b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.md new file mode 100644 index 00000000..099f5e81 --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.view/bagua.css b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.view/bagua.css new file mode 100755 index 00000000..b390d188 --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.view/bagua.css @@ -0,0 +1,52 @@ +#bagua-table th { + text-align:center; + font-weight:bold; +} + +#bagua-table td { + width: 150px; + white-space: nowrap; + text-align:center; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 12px; +} + +#bagua-table .nw { + background-color: #999; +} + +#bagua-table .n { + background-color: #03f; + color: #fff; +} + +#bagua-table .ne { + background-color: #ff6; +} + +#bagua-table .w { + background-color: #ff0; +} +#bagua-table .c { + background-color: #60c; + color: #fff; +} +#bagua-table .e { + background-color: #09f; + color: #fff; +} + +#bagua-table .sw { + background-color: #963; + color: #fff; +} + +#bagua-table .s { + background-color: #f60; + color: #fff; +} +#bagua-table .se { + background-color: #0c3; + color: #fff; +} diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.view/index.html b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.view/index.html new file mode 100755 index 00000000..d11ea4f7 --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/solution.view/index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          Bagua Chart: Direction, Element, Color, Meaning
                          Northwest
                          Metal
                          Silver
                          Elders +
                          North
                          Water
                          Blue
                          Change +
                          Northeast
                          Earth
                          Yellow
                          Direction +
                          West
                          Metal
                          Gold
                          Youth +
                          Center
                          All
                          Purple
                          Harmony +
                          East
                          Wood
                          Blue
                          Future +
                          Southwest
                          Earth
                          Brown
                          Tranquility +
                          South
                          Fire
                          Orange
                          Fame +
                          Southeast
                          Wood
                          Green
                          Romance +
                          + + + + + diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/source.view/bagua.css b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/source.view/bagua.css new file mode 100755 index 00000000..b390d188 --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/source.view/bagua.css @@ -0,0 +1,52 @@ +#bagua-table th { + text-align:center; + font-weight:bold; +} + +#bagua-table td { + width: 150px; + white-space: nowrap; + text-align:center; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 12px; +} + +#bagua-table .nw { + background-color: #999; +} + +#bagua-table .n { + background-color: #03f; + color: #fff; +} + +#bagua-table .ne { + background-color: #ff6; +} + +#bagua-table .w { + background-color: #ff0; +} +#bagua-table .c { + background-color: #60c; + color: #fff; +} +#bagua-table .e { + background-color: #09f; + color: #fff; +} + +#bagua-table .sw { + background-color: #963; + color: #fff; +} + +#bagua-table .s { + background-color: #f60; + color: #fff; +} +#bagua-table .se { + background-color: #0c3; + color: #fff; +} diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/source.view/index.html b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/source.view/index.html new file mode 100755 index 00000000..569a4132 --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/source.view/index.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          Bagua Chart: Direction, Element, Color, Meaning
                          Northwest
                          Metal
                          Silver
                          Elders +
                          North
                          Water
                          Blue
                          Change +
                          Northeast
                          Earth
                          Yellow
                          Direction +
                          West
                          Metal
                          Gold
                          Youth +
                          Center
                          All
                          Purple
                          Harmony +
                          East
                          Wood
                          Blue
                          Future +
                          Southwest
                          Earth
                          Brown
                          Tranquility +
                          South
                          Fire
                          Orange
                          Fame +
                          Southeast
                          Wood
                          Green
                          Romance +
                          + + + + + diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/task.md b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/task.md new file mode 100644 index 00000000..858511db --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/1-rewrite-with-jquery/task.md @@ -0,0 +1,7 @@ +# Перепишите на jQuery + +[importance 5] + +Перепишите делегирование на jQuery. + +[edit src="source" task/] \ No newline at end of file diff --git a/3-more/3-jquery-stub/5-jquery-stub-article/article.md b/3-more/3-jquery-stub/5-jquery-stub-article/article.md new file mode 100644 index 00000000..e9b923b6 --- /dev/null +++ b/3-more/3-jquery-stub/5-jquery-stub-article/article.md @@ -0,0 +1,3 @@ +# ... [в работе] + +Раздел не будет доступен еще некоторое время. diff --git a/3-more/3-jquery-stub/index.md b/3-more/3-jquery-stub/index.md new file mode 100644 index 00000000..c3b1a546 --- /dev/null +++ b/3-more/3-jquery-stub/index.md @@ -0,0 +1,3 @@ +# jQuery: курс немолодого бойца [в работе] + +Пока материал этого раздела существует только в голове автора. \ No newline at end of file diff --git a/3-more/4-optimize/1-memory-leaks/article.md b/3-more/4-optimize/1-memory-leaks/article.md new file mode 100644 index 00000000..4029b76b --- /dev/null +++ b/3-more/4-optimize/1-memory-leaks/article.md @@ -0,0 +1,480 @@ +# Утечки памяти + +*Утечки памяти* происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов. + +Обычно это происходит автоматически ([](/memory-management)). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях: + +[cut] +
                            +
                          1. Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.
                          2. +
                          3. Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включенным, чтобы не закрывать браузер с кучей вкладок. Приходит утром -- а браузер съел всю память и рухнул и сильно тормозит.
                          4. +
                          +Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре JavaScript-кода. Мы разберём несколько наиболее частых и важных примеров. + +## Коллекция утечек в IE + +### Утечка DOM ↔ JS в IE7- + +IE до версии 8 не умел очищать циклические ссылки, появляющиеся между DOM-объектами и объектами JavaScript. В результате и DOM и JS оставались в памяти навсегда. + +[warn header="Пропустите эту секцию, если IE8- не нужен"] +Проблема была особенно серьезна в IE6 до SP3 (или до обновления июня 2007 года), там память не освобождалась даже при переходе на другую страницу. + +Сейчас она существует в IE6, 7, а также, в облегчённом варианте, в IE8 (см. далее). Если вы не поддерживаете IE8-, то можете пропустить эту секцию. +[/warn] + +Функция `setHandler` в примере ниже ведёт к утечке памяти в IE6,7: + +```js +function setHandler() { + var elem = document.getElementById('id'); + elem.onclick = function() { + /* может быть пустая функция, не важно */ + }; +} +``` + +Элемент `elem` здесь ссылается на JavaScript-функцию через ссылку `onclick` напрямую, через свойство, а функция ссылается на `elem` через замыкание. + + + +Здесь вместо DOM-элемента в IE может быть `XMLHttpRequest`, `ActiveX`, любой другой COM-объект. Круговая ссылка гарантирует утечку. + + +**Обойти утечки памяти в IE можно, разорвав циклические ссылки.** + +Например, можно удалить ссылку на `elem` из замыкания, присвоив `elem = null`. Таким образом обработчик больше не ссылается на DOM-элемент. Циклическая ссылка разорвана: + + + + +Больше информации об этой утечке вы можете почерпнуть из статей: Understanding and Solving Internet Explorer Leak Patterns и Circular Memory Leak Mitigation. + + +### Утечка DOM ↔ JS, вариант для IE8- + +В браузере IE8 была проведена серьёзная работа над ошибками, и пример, описанный выше, больше не приводит к утечке. + +**Но ситуация исправлена не до конца. Утечка в IE8- появляется, если круговая ссылка возникает "через объект".** + +Чтобы было понятнее, о чём речь, посмотрите на следующий код. Он вызывает утечку памяти: + +```js +function leak() { + // Создаём новый DIV, добавляем к BODY + var elem = document.createElement('div'); + document.body.appendChild(elem); + + // Записываем в свойство жирный объект + elem.__expando = { + bigAss: new Array(1000000).join('lalala') + }; + +*!* + // Создаём круговую ссылку. Без этой строки утечки не будет. + elem.__expando.__elem = elem; +*/!* + + // Удалить элемент из DOM. Браузер должен очистить память. + elem.parentElement.removeChild(elem); +} +``` + +Открыть в новом окне (в IE) + +Этот пример, течёт в IE6,7,8, а также в IE9 в режиме совместимости с IE8. Проблема -- в круговой ссылке `elem.__expando__elem = elem`. + +**Утечка может возникать и неявным образом, через замыкание:** + +```js +function leak() { + var elem = document.createElement('div'); + document.body.appendChild(elem); + + elem.__expando = { + bigAss: new Array(1000000).join('lalala'), +*!* + method: function() {} // создаётся круговая ссылка через замыкание +*/!* + }; + + // Удалить элемент из DOM. Браузер должен очистить память. + elem.parentElement.removeChild(elem); +} +``` + +Открыть в новом окне (в IE) + +**Без метода `method` здесь утечки не возникнет.** + +Бывает ли такая ситуация в реальной жизни? Или это -- целиком синтетический пример, для заумных программистов? + +Да, конечно бывает. Например, при разработке графических компонент. + +Во-первых, сам объект компонента хранит ссылку на его DOM-элемент, чтобы работать с ним. Например: + +```js +function Menu(elem) { + elem.onclick = function() { + // ссылка на elem осталась в замыкании + }; +} + +var menu = new Menu(elem); +``` + +То есть, компонент всегда знает свой элемент. + +Но бывают ситуации, когда нужно пойти в обратном направлении, а именно -- по элементу определить, какой на нём компонент. Например, при делегировании, чтобы передать обработку события на элементе соответствующему компоненту. Или при Drag'n'Drop, чтобы получить компонент, соответствующий элементу, на который произведён перенос. + +Сама задача не является чем-то из ряда вон выходящим. Вполне естественно, что JS-компонент привязан к элементу, а элемент знает о компоненте на нём. Но в IE8- прямая привязка ведёт к утечке памяти! + +```js +function Menu(elem) { + elem.onclick = function() {}; +} + +var menu = new Menu(elem); // Menu содержит ссылку на elem +*!* +elem.menu = menu; // вот такая привязка или что-то подобное ведёт к утечке +``` + +Открыть в новом окне (в IE) + +Такая привязка удобна, т.к. мы по DOM-элементу можем получить JS-компонент, который к нему привязан. Но, как видим, ведёт к утечке в IE8-. + +### Утечка IE8 при обращении к коллекциям таблицы + +Эта утечка происходит только в IE8 в стандартном режиме. В нём при обращении к табличным псевдо-массивам (напр. `rows`) создаются и не очищаются внутренние ссылки, что приводит к утечкам. + +Также воспроизводится в новых IE в режиме совместимости с IE8. + +Код: + +```js +var elem = document.createElement('div'); // любой элемент + +function leak() { + + elem.innerHTML = '
                          1
                          '; + +*!* + elem.firstChild.rows[0]; // просто доступ через rows[] приводит к утечке + // при том, что мы даже не сохраняем значение в переменную +*/!* + + elem.removeChild(elem.firstChild); // удалить таблицу (*) + // alert(elem.childNodes.length) // выдал бы 0, elem очищен, всё честно +} +``` + +Открыть в новом окне (в IE) + +Особенности: +
                            +
                          • Если убрать отмеченную строку, то утечки не будет.
                          • +
                          • Если заменить строку `(*)` на `elem.innerHTML = ''`, то память будет очищена, т.к. этот способ работает по-другому, нежели просто `removeChild` (см. главу [](/memory-management)).
                          • +
                          • Утечка произойдёт не только при доступе к `rows`, но и к другим свойствам, например `elem.firstChild.tBodies[0]`.
                          • +
                          + +Эта утечка проявляется, в частности, при удалении детей элемента следующей функцией: + +```js +function empty(elem) { + while(elem.firstChild) elem.removeChild(elem.firstChild); +} +``` + +Если идёт доступ к табличным коллекциям и регулярное обновление таблиц при помощи DOM-методов -- утечка в IE8 будет расти. + +Более подробно вы можете почитать об этой утечке в статье [Утечки памяти в IE8, или страшная сказка со счастливым концом](http://habrahabr.ru/post/141451/). + +### Утечка через XmlHttpRequest в IE8- + +Следующий код вызывает утечки памяти в IE8-: + +```js +function leak() { + var xhr = new XMLHttpRequest(); // в IE6 создать через ActiveX + + xhr.open('GET', '/server.do', true); + + xhr.onreadystatechange = function() { + if(xhr.readyState == 4 && xhr.status == 200) { + // ... + } + } + + xhr.send(null); +} +``` + +Как вы думаете, почему? Если вы внимательно читали то, что написано выше, то имеете информацию для ответа на этот вопрос.. + +Посмотрим, какая структура памяти создается при каждом запуске: + + + +Когда запускается асинхронный запрос `xhr`, браузер создаёт специальную внутреннюю ссылку (internal reference) на этот объект. находится в процессе коммуникации. Именно поэтому объект `xhr` будет жив после окончания работы функции. + +**Когда запрос завершен, браузер удаляет внутреннюю ссылку, `xhr` становится недостижимым и память очищается... Везде, кроме IE<9.** + + +Открыть в новом окне (в IE8-) (откройте страницу и пусть поработает минут 20 - съест всю память, включая виртуальную). + +Чтобы это исправить, нам нужно разорвать круговую ссылку `XMLHttpRequest ↔ JS`. Например, можно удалить `xhr` из замыкания: + +```js +function leak() { + var xhr = new XMLHttpRequest(); + + xhr.open('GET', 'something.js?'+Math.random(), true); + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + if(xhr.status == 200) { + document.getElementById('test').innerHTML++; + } + +*!* + xhr = null; // по завершении запроса удаляем ссылку из замыкания +*/!* + } + + xhr.send(null); +} +``` + + + +Теперь циклической ссылки нет -- мы устранили утечку. + +Посмотреть исправленный пример для IE в отдельном окне. + + +## Объемы утечек памяти + +Объем "утекающей" памяти может быть небольшим. Тогда это почти не ощущается. Но так как замыкания ведут к сохранению переменных внешних функций, то одна функция может тянуть за собой много чего ещё. + +Представьте, вы создали функцию, и одна из ее переменных содержит очень большую по объему строку (например, получает с сервера). + +```js +function f() { + var data = "Большой объем данных, например, переданных сервером" + + /* делаем что-то хорошее (ну или плохое) с полученными данными */ + + function inner() { + // ... + } + + return inner; +} +``` + +**Пока функция `inner` остается в памяти, `LexicalEnvironment` с переменной большого объема внутри висит в памяти.** + +Висит до тех пор, пока функция `inner` жива. + +**Как правило, JavaScript не знает, какие из переменных функции `inner` будут использованы, поэтому оставляет их все.** + +Исключение -- виртуальная машина V8 (Chrome, Opera, Node.JS), она обычно видит, что переменная не используется во внутренних функциях, и очистит память. + +В других же интерпретаторах, даже если код спроектирован так, что никакой утечки нет, по вполне разумной причине может создаваться множество функций, а память будет расти потому, что функция тянет за собой своё замыкание. + +Сэкономить память здесь вполне можно. Мы же знаем, что переменная `data` не используется в `inner`. Поэтому просто обнулим её: + +```js +function f() { + var data = "Большое количество данных, например, переданных сервером" + + /* действия с data */ + + function inner() { + // ... + } + +*!* + data = null; // когда data станет не нужна - +*/!* + + return inner; +} +``` + +## jQuery: утечки и борьба с утечками + +[warn header="Изменения в jQuery 2"] +В jQuery 2 объект с данными элемента, о котором идёт речь далее, более недоступен извне. Он стал локальной переменной внутри jQuery с именем `data_priv`, явный доступ к внутренним данным более невозможен (или deprecated). + +Но в остальном всё работает точно так, как описано, и с теми же последствиями. +[/warn] + +**В jQuery для борьбы с утечками памяти в IE6-7 используется $.data API. Однако, это может стать причиной новых(!) утечек, характерных для jQuery.** + +Основной принцип `$.data` — это для любого JavaScript объекта сохранить/получить значение для элемента с помощью jQuery вызова: + +```js +//+ run +$(document.body).data('prop', { anything: "любой объект" }) // set +alert( $(document.body).data('prop') ) // get +``` + +jQuery `elem.data(prop, val)` делает следующее: +
                            +
                          1. Элемент получает уникальный идентификатор, если у него такого еще нет: + +```js +elem[ jQuery.expando ] = id = ++jQuery.uuid; // средствами jQuery +``` + +`jQuery.expando` -- это случайная строка, сгенерированная jQuery один раз при входе на страницу. Уникальное свойство, чтобы ничего важного не перезаписать.
                          2. +
                          3. ...А сами данные сохраняются в специальном объекте `jQuery.cache`: + +```js +jQuery.cache[id]['prop'] = { anything: "любой объект" }; +``` + +
                          4. +
                          + +Когда данные считываются из элемента: + +
                            +
                          1. Уникальный идентификатор элемента извлекается из `id = elem[ jQuery.expando]`. +
                          2. Данные считываются из `jQuery.cache[id]`.
                          3. +
                          + +Смысл этого API в том, что DOM-элемент никогда не ссылается на JavaScript объект напрямую. Задействуется идентификатор, а сами данные хранятся в `jQuery.cache`. Утечек в IE не будет. + +К тому же все данные известны библиотеке, так что можно клонировать с ними и т.п. + +**Как побочный эффект -- возникает утечка памяти, если элемент удален из DOM без дополнительной очистки.** + +### Примеры утечек в jQuery + +Следующий код создает jQuery-утечку во всех браузерах: + +```js +$('
                          ') + .html(new Array(1000).join('text')) // div с текстом, возможна AJAX-загрузка + .click(function() { }) + .appendTo('#data') + +document.getElementById('data').innerHTML = ''; // (*) +``` + +Показать в отдельном окне + +Утечка происходит потому, что обработчик события в jQuery хранится в данных элемента. В строке `(*)` элемент удален очисткой родительского `innerHTML`, но в `jQuery.cache` данные остались. + +Более того, система обработки событий в jQuery устроена так, что вместе с обработчиком в данных хранится и ссылка на элемент, так что в итоге оба -- и обработчик и элемент -- остаются в памяти вместе со всем замыканием! + +**Ещё более простой пример утечки:** + +Этот код создает утечку: + +```js +function go() { + $('
                          ') + .html(new Array(1000).join('text')) + .click(function() { }) +} +``` + +Показать в отдельном окне + +Причина здесь в том, что элемент `
                          ` создан, но нигде не размещен :). После выполнения функции ссылка на него теряется. Но обработчик события `click` уже сохранил данные в `jQuery.cache`, которые застревают там навсегда. + +### Используем jQuery без утечек + +Чтобы избежать утечек, описанных выше, для удаления элементов используйте функции jQuery API, а не чистый JavaScript. + +Методы remove(), empty() и html() проверяют дочерние элементы на наличие данных и очищают их. Это несколько замедляет процедуру удаления, но зато освобождается память. + + +**К счастью обнаружить такие утечки легко. Проверьте размер `$.cache.`** Если он большой и растет, то изучите кэш, посмотрите, какие записи остаются и почему. + +### Улучшение производительности jQuery + +У способа борьбы с утечками IE, применённого в jQuery, есть побочный эффект. + +**Функции, удаляющие элементы, бегают по всему дереву DOM и очищают подэлементы.** + +Представьте себе, что вы получили с сервера большую таблицу (в виде текста), вставили её в документ и хотите обновить. Вызов `$('table').remove()` будет бегать по всем ячейкам и искать в них данные. Но мы-то знаем, что обработчики назначены через делегирование, и тратить на это время ни к чему! + +Чтобы "грязно" удалить элемент, без чистки, можно воспользоваться методом detach(). Его официальное назначение -- в том, чтобы убрать элемент из DOM, но сохранить возможность для вставки (и, соответственно, оставить на нём все данные). А неофициальное -- быстро убрать элемент из DOM. Если на нём нет данных и обработчиков, то всё хорошо. + +В принципе, если хочется всё сделать чисто, но быстро -- никто не мешает сделать `elem.detach()` и поместить вызов `elem.remove()` в `setTimeout`. В результате очистка будет происходить ассинхронно и незаметно. + +Итак, будем надеяться, что эта тема для вас теперь прозрачна и ясна и в следующих разделах мы не будем больше говорить об утечках в jQuery. + +## Поиск и устранение утечек памяти + +### Проверка на утечки + +Существует множество шаблонов утечек и ошибок в браузерах, которые могут приводить к утечкам. Для их устранения сперва надо постараться изолировать и воспроизвести утечку. + +
                            +
                          • **Необходимо помнить, что браузер может очистить память не сразу когда объект стал недостижим, а чуть позже.** Например, сборщик мусора может ждать, пока не будет достигнут определенный лимит использования памяти, или запускаться время от времени.
                          • + +Поэтому если вы думаете, что нашли проблему и тестовый код, запущенный в цикле, течёт -- подождите примерно минуту, добейтесь, чтобы памяти ело стабильно и много. Тогда будет понятно, что это не особенность сборщика мусора.
                          • **Если речь об IE, то надо смотреть "Виртуальную память" в списке процессов, а не только обычную "Память".** Обычная может очищаться за счет того, что перемещается в виртуальную (на диск).
                          • +
                          • Для простоты отладки, если есть подозрение на утечку конкретных объектов, в них добавляют большие свойства-маркеры. Например, подойдет фрагмент текста: `new Array(999999).join('leak')`.
                          • +
                          + +### Настройка браузера + +Утечки могут возникать из-за расширений браузера, взимодействющих со страницей. Еще более важно, что **утечки могут быть следствием конфликта двух браузерных расширений** Например, было такое: память текла когда включены расширения Skype и плагин антивируса одновременно. + +Чтобы понять, в расширениях дело или нет, нужно отключить их: + +
                            +
                          1. Отключить Flash.
                          2. +
                          3. Отключить анивирусную защиту, проверку ссылок и другие модули и дополнения.
                          4. +
                          5. Отключить плагины. Отключить ВСЕ плагины. +
                              +
                            • + Для IE есть параметр коммандной строки: + +``` +"C:\Program Files\Internet Explorer\iexplore.exe" -extoff +``` + +Кроме того необходимо отключить сторонние расширения в свойствах IE. + + + + +
                            • +
                            • Firefox необходимо запускать с чистым профилем. Используйте следующую команду для запуска менеджера профилей и создания чистого пустого профиля: + +``` +firefox --profilemanager +``` + +
                            • +
                            +
                          6. +
                          + +## Инструменты + +Пожалуй, единственный браузер с поддержкой отладки памяти -- это Chrome. В инструментах разработчика вкладка Timeline -- Memory показывает график использования памяти. + + + +Можем посмотреть, сколько памяти и куда он использует. + +Также в Profiles есть кнопка Take Heap Snapshot, здесь можно сделать и исследовать снимок текущего состояния страницы. Снимки можно сравнивать друг с другом, выяснять количество новых объектов. Можно смотреть, почему объект не очищен и кто на него ссылается. + +Замечательная статья на эту тему есть в документации: [Chrome Developer Tools: Heap Profiling](http://code.google.com/chrome/devtools/docs/heap-profiling.html). + +Утечки памяти штука довольно сложная. В борьбе с ними вам определенно понадобится одна вещь: *Удача!* + +
                          + +
                          + +

                          Перевести с английского варианта
                          учебника помог Марат Шагиев

                          + diff --git a/3-more/4-optimize/1-memory-leaks/chrome.png b/3-more/4-optimize/1-memory-leaks/chrome.png new file mode 100755 index 00000000..5f233448 Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/chrome.png differ diff --git a/3-more/4-optimize/1-memory-leaks/goodluck.png b/3-more/4-optimize/1-memory-leaks/goodluck.png new file mode 100755 index 00000000..50086e2f Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/goodluck.png differ diff --git a/3-more/4-optimize/1-memory-leaks/ie1.png b/3-more/4-optimize/1-memory-leaks/ie1.png new file mode 100755 index 00000000..31c0875e Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/ie1.png differ diff --git a/3-more/4-optimize/1-memory-leaks/ie2.png b/3-more/4-optimize/1-memory-leaks/ie2.png new file mode 100755 index 00000000..ded23dfc Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/ie2.png differ diff --git a/3-more/4-optimize/1-memory-leaks/ie9_disable1.png b/3-more/4-optimize/1-memory-leaks/ie9_disable1.png new file mode 100755 index 00000000..0b44ee20 Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/ie9_disable1.png differ diff --git a/3-more/4-optimize/1-memory-leaks/ie9_disable2.png b/3-more/4-optimize/1-memory-leaks/ie9_disable2.png new file mode 100755 index 00000000..e8077bac Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/ie9_disable2.png differ diff --git a/3-more/4-optimize/1-memory-leaks/xhr.png b/3-more/4-optimize/1-memory-leaks/xhr.png new file mode 100755 index 00000000..29c3bcd7 Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/xhr.png differ diff --git a/3-more/4-optimize/1-memory-leaks/xhr2.png b/3-more/4-optimize/1-memory-leaks/xhr2.png new file mode 100755 index 00000000..9806976a Binary files /dev/null and b/3-more/4-optimize/1-memory-leaks/xhr2.png differ diff --git a/3-more/4-optimize/2-script-place-optimize/article.md b/3-more/4-optimize/2-script-place-optimize/article.md new file mode 100644 index 00000000..c0f473dc --- /dev/null +++ b/3-more/4-optimize/2-script-place-optimize/article.md @@ -0,0 +1,63 @@ +# Оптимизация скриптов: dom, async, defer [в работе] + +Скрипт может быть размещен в любом месте HTML. + +Здесь мы разберём то, как его местоположение влияет на скорость загрузки страницы. + +[cut] + + +## Перемещение скрипта в HEAD + +Если нужно, чтобы скрипт обязательно выполнился до того, как страничка будет отображена, то самое подходящее место -- это `HEAD`: + +```html + + + + *!* + + */!* + + + + +

                          Нажмите на кнопку, чтобы начать

                          + + + + + + +``` + +## Скрипты в конце BODY + +Скрипт может быть и в конце документа, например перед закрывающим ``. В таком случае он выполняется *после* отрисовки страницы. + +[compare] ++ Это хорошо, т.к. пользователь не должен ждать, пока загрузятся скрипты. Он увидит страницу быстро. +- Страница будет лишь частично работать, т.к. для инициализации интерфейсов нужен JS. Нужно это учесть, например, закрасить серым нерабочие интерфейсы, показать на них индикатор загрузки. +[/compare] + +[smart header="Рендеринг и CSS"] +По стандарту, CSS-стили могут располагаться только внутри тега `HEAD`, и они тоже блокируют отображение. + +В принципе, их можно для оптимизации перенести вниз документа -- всё будет работать. Но пока они не загрузятся, страница будет показываться без стилей, это обычно непрезентабельно. Поэтому так не делают. +[/smart] + +## Итоги + +Скрипты могут быть внутренними и подключаться напрямую в тег `SCRIPT` и внешними, в виде отдельных файлов, подключаемых с помощью атрибута `src="path/to/file.js"` тега `SCRIPT` + +В любом случае, отрисовка HTML-страницы блокируется, пока скрипт не будет скачан и исполнен. Большинство браузеров умеют качать несколько скриптов параллельно, что несколько убыстряет процесс, но представьте -- у посетителя медленный канал (смотрит с телефона). Он открывает сайт. Если в `HEAD` стоит большой внешний скрипт, то пока он не загрузится -- страница не отобразится. + +Поэтому, чтобы убыстрить процесс открытия, скрипты добавляют в конец `BODY` или ставят им атрибуты `async` или `defer`. В таком случае страница будет показана до загрузки скрипта. + +Но при этом посетитель может тут же куда-нибудь нажать, а скрипты для этой кнопки ещё не загрузились. Эту особенность взаимодействия нужно предусмотреть в интерфейсе (это вполне возможно), и все будет здорово. \ No newline at end of file diff --git a/3-more/4-optimize/3-reflow/article.md b/3-more/4-optimize/3-reflow/article.md new file mode 100644 index 00000000..7004cdf6 --- /dev/null +++ b/3-more/4-optimize/3-reflow/article.md @@ -0,0 +1,4 @@ +# Reflow + + +[iframe src="reflow"] \ No newline at end of file diff --git a/3-more/4-optimize/3-reflow/reflow.view/index.html b/3-more/4-optimize/3-reflow/reflow.view/index.html new file mode 100755 index 00000000..7b0b99fa --- /dev/null +++ b/3-more/4-optimize/3-reflow/reflow.view/index.html @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          0:00:10:20:30:40:50:60:70:80:90:100:110:120:130:140:150:160:170:180:19
                          1:01:11:21:31:41:51:61:71:81:91:101:111:121:131:141:151:161:171:181:19
                          2:02:12:22:32:42:52:62:72:82:92:102:112:122:132:142:152:162:172:182:19
                          3:03:13:23:33:43:53:63:73:83:93:103:113:123:133:143:153:163:173:183:19
                          4:04:14:24:34:44:54:64:74:84:94:104:114:124:134:144:154:164:174:184:19
                          5:05:15:25:35:45:55:65:75:85:95:105:115:125:135:145:155:165:175:185:19
                          6:06:16:26:36:46:56:66:76:86:96:106:116:126:136:146:156:166:176:186:19
                          7:07:17:27:37:47:57:67:77:87:97:107:117:127:137:147:157:167:177:187:19
                          8:08:18:28:38:48:58:68:78:88:98:108:118:128:138:148:158:168:178:188:19
                          9:09:19:29:39:49:59:69:79:89:99:109:119:129:139:149:159:169:179:189:19
                          10:010:110:210:310:410:510:610:710:810:910:1010:1110:1210:1310:1410:1510:1610:1710:1810:19
                          11:011:111:211:311:411:511:611:711:811:911:1011:1111:1211:1311:1411:1511:1611:1711:1811:19
                          12:012:112:212:312:412:512:612:712:812:912:1012:1112:1212:1312:1412:1512:1612:1712:1812:19
                          13:013:113:213:313:413:513:613:713:813:913:1013:1113:1213:1313:1413:1513:1613:1713:1813:19
                          14:014:114:214:314:414:514:614:714:814:914:1014:1114:1214:1314:1414:1514:1614:1714:1814:19
                          15:015:115:215:315:415:515:615:715:815:915:1015:1115:1215:1315:1415:1515:1615:1715:1815:19
                          16:016:116:216:316:416:516:616:716:816:916:1016:1116:1216:1316:1416:1516:1616:1716:1816:19
                          17:017:117:217:317:417:517:617:717:817:917:1017:1117:1217:1317:1417:1517:1617:1717:1817:19
                          18:018:118:218:318:418:518:618:718:818:918:1018:1118:1218:1318:1418:1518:1618:1718:1818:19
                          19:019:119:219:319:419:519:619:719:819:919:1019:1119:1219:1319:1419:1519:1619:1719:1819:19
                          + + + + + + + + + diff --git a/3-more/4-optimize/index.md b/3-more/4-optimize/index.md new file mode 100644 index 00000000..77df4229 --- /dev/null +++ b/3-more/4-optimize/index.md @@ -0,0 +1,2 @@ +# Оптимизация + diff --git a/3-more/5-compress/1-minification/article.md b/3-more/5-compress/1-minification/article.md new file mode 100644 index 00000000..0ab4f306 --- /dev/null +++ b/3-more/5-compress/1-minification/article.md @@ -0,0 +1,512 @@ +# Как работают сжиматели JavaScript + +Перед выкладыванием JavaScript на "боевую" машину -- пропускаем его через минификатор (также говорят "сжиматель"), который удаляет пробелы и по-всякому оптимизирует код, уменьшая его размер. + +В этой статье мы посмотрим, как работают современные минификаторы, за счёт чего они укорачивают код и какие с ними возможны проблемы. +[cut] + +## Современные сжиматели + +Рассматриваемые в этой статье алгоритмы и подходы относятся к минификаторам последнего поколения. + +Вот их список: + +
                            +
                          • [Google Closure Compiler](https://developers.google.com/closure/compiler/)
                          • +
                          • [UglifyJS](https://github.com/mishoo/UglifyJS)
                          • +
                          • [Microsoft AJAX Minifier](http://ajaxmin.codeplex.com/)
                          • +
                          +Самые широко используемые -- первые два, поэтому будем рассматривать в первую очередь их. + +Наша цель -- понять, как они работают, и что интересного с их помощью можно сотворить. + +## С чего начать? + +Для GCC: + +
                            +
                          1. Убедиться, что стоит [Java](http://java.oracle.com)
                          2. +
                          3. Скачать и распаковать [](http://closure-compiler.googlecode.com/files/compiler-latest.zip), нам нужен файл `compiler.jar`.
                          4. +
                          5. Сжать файл `my.js`: `java -jar compiler.jar --charset UTF-8 --js my.js --js_output_file my.min.js`
                          6. +
                          + +Обратите внимание на флаг `--charset` для GCC. Без него русские буквы будут закодированы во что-то типа `\u1234`. + +Google Closure Compiler также содержит [песочницу](http://closure-compiler.appspot.com/home) для тестирования сжатия и [веб-сервис](https://developers.google.com/closure/compiler/docs/gettingstarted_api?hl=ru), на который код можно отправлять для сжатия. Но скачать файл обычно гораздо проще, поэтому его редко где используют. + +Для UglifyJS: + +
                            +
                          1. Убедиться, что стоит [Node.js](http://nodejs.org)
                          2. +
                          3. Поставить `npm install -g uglify-js`.
                          4. +
                          5. Сжать файл `my.js`: `uglifyjs my.js -o my.min.js`
                          6. +
                          + +## Что делает минификатор? + +Все современные минификаторы работают следующим образом: +
                            +
                          1. Разбирают JavaScript-код в синтаксическое дерево. + +Также поступает любой интерпретатор JavaScript перед тем, как его выполнять. Но затем, вместо исполнения кода...
                          2. +
                          3. Бегают по этому дереву, анализируют и оптимизируют его.
                          4. +
                          5. Записывают из синтаксического дерева получившийся код.
                          6. +
                          + +## Как выглядит дерево? + +Посмотреть синтаксическое дерево можно, запустив компилятор со специальным флагом. + +Для GCC есть даже способ вывести его: + +
                            +
                          1. Сначала сгенерируем дерево в формате [DOT](http://en.wikipedia.org/wiki/DOT_language): + +``` +java -jar compiler.jar --js my.js --use_only_custom_externs --print_tree >my.dot +``` + +Здесь флаг `--print_tree` выводит дерево, а `--use_only_custom_externs` убирает лишнюю служебную информацию. +
                          2. +
                          3. Файл в этом формате используется в различных программах для графопостроения. + +Чтобы превратить его в обычную картинку, подойдёт утилита `dot` из пакета [Graphviz](http://www.graphviz.org/): + +``` +// конвертировать в формат png +dot -Tpng my.dot -o my.png + +// конвертировать в формат svg +dot -Tsvg my.dot -o my.svg +``` + +
                          4. +
                          + +Пример кода `my.js`: + +```js +function User(name) { + + this.sayHi = function() { + alert(name); + }; + +} +``` + +Результат, получившееся из `my.js` дерево: + + + +В узлах-эллипсах на иллюстрации выше стоит тип, например `FUNCTION` (функция) или `NAME` (имя переменной). Комментарии к ним на русском языке добавлены мной вручную. + +Кроме него к каждому узлу привязаны конкретные данные. Сжиматель умеет ходить по этому дереву и менять его, как пожелает. + +[smart header="Комментарии JSDoc"] +Обычно когда код превращается в дерево -- из него естественным образом исчезают комментарии и пробелы. Они не имеют значения при выполнении, поэтому игнорируются. + +Но Google Closure Compiler добавляет в дерево информацию из *комментариев JSDoc*, т.е. комментариев вида `/** ... */`, например: + +```js +*!* +/** + * Номер минимальной поддерживаемой версии IE + * @const + * @type {number} + */ +*/!* +var minIEVersion = 8; +``` + +Такие комментарии не создают новых узлов дерева, а добавляются в качестве информации к существующем. В данном случае -- к переменной `minIEVersion`. + +В них может содержаться информация о типе переменной (`number`) и другая, которая поможет сжимателю лучше оптимизировать код (`const` -- константа). + +[/smart] + +## Оптимизации + +Сжиматель бегает по дереву, ищет "паттерны" -- известные ему структуры, которые он знает, как оптимизировать, и обновляет дерево. + +В разных минификаторах реализован разный набор оптимизаций, сами оптимизации применяются в разном порядке, поэтому результаты работы могут отличаться. В примерах ниже даётся результат работы GCC. + + +
                          +
                          Объединение и сжатие констант
                          +
                          +До оптимизации: + +```js +function test(a, b) { + run(a, 'my'+'string', 600*600*5, 1 && 0, b && 0) +} +``` + +После: + +```js +function test(a,b){run(a,"mystring",18E5,0,b&&0)}; +``` + +
                            +
                          • `'my' + 'string'` -> `"mystring"`.
                          • +
                          • `600 * 600 * 5` -> `18E5` (научная форма числа, для краткости).
                          • +
                          • `1 && 0` -> `0`.
                          • +
                          • `b && 0` -> без изменений, т.к. результат зависит от `b`.
                          • +
                          +
                          +
                          Укорачивание локальных переменных
                          +
                          +До оптимизации: + +```js +function sayHi(*!*name*/!*, *!*message*/!*) { + alert(name +" сказал: " + message); +} +``` + +После оптимизации: + +```js +function sayHi(a,b){alert(a+" сказал: "+b)}; +``` + +
                            +
                          • Локальная переменная заведомо доступна только внутри функции, поэтому обычно её переименование безопасно (необычные случаи рассмотрим далее).
                          • +
                          • Также переименовываются локальные функции.
                          • +
                          • Вложенные функции обрабатываются корректно.
                          • +
                          +
                          +
                          Объединение и удаление локальных переменных
                          +
                          +До оптимизации: + +```js +function test(nodeId) { + var elem = document.getElementsById(nodeId); + var parent = elem.parentNode; + alert(parent); +} +``` + +После оптимизации GCC: + +```js +function test(a){a=document.getElementsById(a).parentNode;alert(a)}; +``` + +
                            +
                          • Локальные переменные были переименованы.
                          • +
                          • Лишние переменные убраны. Для этого сжиматель создаёт вспомогательную внутреннюю структуру данных, в которой хранятся сведения о "пути использования" каждой переменной. Если одна переменная заканчивает свой путь и начинает другая, то вполне можно дать им одно имя.
                          • +
                          • Кроме того, операции `elem = getElementsById` и `elem.parentNode` объединены, но это уже другая оптимизация.
                          • +
                          +
                          +
                          Уничтожение недостижимого кода, разворачивание `if`-веток
                          +
                          + +До оптимизации: + +```js +function test(node) { + var parent = node.parentNode; + + if (0) { + alert("Привет с параллельной планеты"); + } else { + alert("Останется только один"); + } + + return; + + alert(1); +} +``` + +После оптимизации: + +```js +function test(){alert("Останется только один")} +``` + +
                            +
                          • Если переменная присваивается, но не используется, она может быть удалена. В примере выше эта оптимизация была применена к переменной `parent`, а затем и к параметру `node`.
                          • +
                          • Заведомо ложная ветка `if(0) { .. }` убрана, заведомо истинная -- оставлена. + +То же самое будет с условиями в других конструкциях, например `a = true ? c : d` превратится в `a = c`.
                          • +
                          • Код после `return` удалён как недостижимый.
                          • +
                          +
                          +
                          Переписывание синтаксических конструкций
                          +
                          +До оптимизации: + +```js +var i = 0; +while (i++ < 10) { + alert(i); +} + +if (i) { + alert(i); +} + +if (i=='1') { + alert(1); +} else if(i == '2') { + alert(2); +} else { + alert(i); +} +``` + +После оптимизации: + +```js +for(var i=0;10>i++;)alert(i);i&&alert(i);"1"==i?alert(1):"2"==i?alert(2):alert(i); +``` + +
                            +
                          • Конструкция `while` переписана в `for`.
                          • +
                          • Конструкция `if (i) ...` переписана в `i&&...`.
                          • +
                          • Конструкция `if (cond) ... else ...` была переписана в `cond ? ... : ...`.
                          • +
                          +
                          +
                          Инлайнинг функций
                          +
                          +*Инлайнинг функции* -- приём оптимизации, при котором функция заменяется на своё тело. + +До оптимизации: + +```js +function sayHi(message) { + + var elem = createMessage('div', message); + showElement(elem); + + function createMessage(tagName, message) { + var el = document.createElement(tagName); + el.innerHTML = message; + return el; + } + + function showElement(elem) { + document.body.appendChild(elem); + } +} +``` + +После оптимизации (переводы строк также будут убраны): + +```js +function sayHi(b){ + var a=document.createElement("div"); + a.innerHTML=b; + document.body.appendChild(a) +}; +``` + +
                            +
                          • Вызовы функций `createMessage` и `showElement` заменены на тело функций. В данном случае это возможно, так как функции используются всего по разу.
                          • +
                          • Эта оптимизация применяется не всегда. Если бы каждая функция использовалась много раз, то с точки зрения размера выгоднее оставить их "как есть".
                          • +
                          +
                          +
                          Инлайнинг переменных
                          +
                          Переменные заменяются на значение, если оно заведомо известно. + +До оптимизации: + +```js +(function() { + var isVisible = true; + var hi = "Привет вам из JavaScript"; + + window.sayHi = function() { + if (isVisible) { + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + alert(hi); + } + } + +})(); +``` + +После оптимизации: + +```js +(function(){ + window.sayHi=function(){ + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + alert("Привет вам из JavaScript"); + }; +}})(); +``` + +
                            +
                          • Переменная `isVisible` заменена на `true`, после чего `if` стало возможным убрать.
                          • +
                          • Переменная `hi` заменена на строку.
                          • +
                          + +Казалось бы -- зачем менять `hi` на строку? Ведь код стал ощутимо длиннее! + +...Но всё дело в том, что минификатор знает, что дальше код будет сжиматься при помощи gzip. Во всяком случае, все правильно настроенные сервера так делают. + +[Алгоритм работы gzip](http://www.gzip.org/algorithm.txt) заключается в том, что он ищет повторы в данных и выносит их в специальный "словарь", заменяя на более короткий идентификатор. Архив как раз и состоит из словаря и данных, в которых дубликаты заменены на идентификаторы. + +Если вынести строку обратно в переменную, то получится как раз частный случай такого сжатия -- взяли `"Привет вам из JavaScript"` и заменили на идентификатор `hi`. Но gzip справляется с этим лучше, поэтому эффективнее будет оставить именно строку. Gzip сам найдёт дубликаты и сожмёт их. + +Плюс такого подхода станет очевиден, если сжать gzip оба кода -- до и после минификации. Минифицированный gzip-сжатый код в итоге даст меньший размер. +
                          + +
                          Разные мелкие оптимизации
                          +
                          Кроме основных оптимизаций, описанных выше, есть ещё много мелких: + +
                            +
                          • Убираются лишние кавычки у ключей + +```js +{"prop" : "val" } => {prop:"val"} +``` + +
                          • +
                          • Упрощаются простые вызовы `Array/Object` + +```js +a = new Array() => a = [] +o = new Object() => o = {} +``` + +Эта оптимизация предполагает, что `Array` и `Object` не переопределены программистом. Для включения её в UglifyJS нужен флаг `--unsafe`. +
                          • +
                          • ...И еще некоторые другие мелкие изменения кода...
                          • +
                          + + +
                          +
                          + +## Подводные камни + +Описанные оптимизации, в целом, безопасны, но есть ряд подводных камней. + +### Конструкция with + +Рассмотрим код: + +```js +function changePosition(style) { + var position, test; + +*!* + with (style) { + position = 'absolute'; + } +*/!* +} +``` + +Куда будет присвоено значение `position = 'absolute'`? + +Это неизвестно до момента выполнения: если свойство `position` есть в `style` -- то туда, а если нет -- то в локальную переменную. + +Можно ли в такой ситуации заменить локальную переменную на более короткую? Очевидно, нет: + +```js +function changePosition(style) { + var a, b; + +*!* + with (style) { // а что, если в style нет такого свойства? + position = 'absolute';// куда будет осуществлена запись? в window.position? + } +*/!* +} +``` + +Такая же опасность для сжатия кроется в использованном `eval`. Ведь `eval` может обращаться к локальным переменным: + +```js +function f(code) { + var myVar; + + eval(code); // а что, если будет присвоение eval("myVar = ...") ? + + alert(myVar); +``` + +Получается, что при наличии `eval` мы не имеем права переименовывать локальные переменные. Причём (!), если функция является вложенноой, то и во внешних функциях тоже. + +А ведь сжатие переменных -- очень важная оптимизация. Как правило, она уменьшает размер сильнее всего. + +Что делать? Разные минификаторы поступают по-разному. + +
                            +
                          • UglifyJS -- не будет переименовывать переменные. Так что наличие `with/eval` сильно повлияет на степень сжатие кода.
                          • +
                          • GCC -- всё равно сожмёт локальные переменные. Это, конечно же, может привести к ошибкам, причём в сжатом коде, отлаживать который не очень-то удобно. Поэтому он выдаст предупреждение о наличии опасной конструкции.
                          • +
                          + +Ни тот ни другой вариант нас, по большому счёту, не устраивают. + +**Для того, чтобы код сжимался хорошо и работал правильно, не используем `with` и `eval`.** + +Либо, если уж очень надо использовать -- делаем это с оглядкой на поведение минификатора, чтобы не было проблем. + +### Условная компиляция IE10- + +В IE10- поддерживалось [условное выполнение JavaScript](http://msdn.microsoft.com/en-us/library/121hztk3.aspx). + +Синтаксис: `/*@cc_on код */`. + +Такой код выполнится в IE10-, например: + +```js +//+ run +var isIE /*@cc_on =true@*/; + +alert(isIE); // true в IE. +``` + +Там же доступны и дополнительные директивы: `@_jscript_version`, `@if` и т.п., но речь здесь не о том. + +Для минификаторов этот "условный" комментарий -- всего лишь обычный комментарий. Они его удалят. Получится, что код не поймёт, где же IE. + +Что делать? + +
                            +
                          1. Первое и наиболее корректное решение -- не использовать условную компиляцию.
                          2. +
                          3. Второе, если уж очень надо -- применить хак, завернуть его в `eval` или `new Function` (чтобы сжиматель не ругался): + +```js +//+ run +var isIE = new Function('', '/*@cc_on return true@*/')(); + +alert(isIE); // true в IE. +``` + +
                          4. +
                          + +Ещё раз заметим, что в современных IE11+ эта компиляция не работает в любом случае. + +В следующих главах мы посмотрим, какие продвинутые возможности есть в минификаторах, как сделать сжатие более эффективным. + diff --git a/3-more/5-compress/1-minification/my.svg b/3-more/5-compress/1-minification/my.svg new file mode 100755 index 00000000..36ca2420 --- /dev/null +++ b/3-more/5-compress/1-minification/my.svg @@ -0,0 +1,262 @@ + + + + + + +AST + + +node0 + +BLOCK + + +node1 + +SCRIPT + + +node0->node1 + + + + +node0->node1 + + +UNCOND + + +RETURN + +RETURN + + +node0->RETURN + + +SYN_BLOCK + + +node2 + +FUNCTION +функция + + +node1->node2 + + + + +node1->RETURN + + +UNCOND + + +node3 + +NAME +имя + + +node2->node3 + + + + +node4 + +PARAM_LIST +список параметров + + +node2->node4 + + + + +node6 + +BLOCK +тело функции + + +node2->node6 + + +UNCOND + + +node2->node6 + + + + +node5 + +NAME +имя + + +node4->node5 + + + + +node7 + +EXPR_RESULT +выражение + + +node6->node7 + + +UNCOND + + +node6->node7 + + + + +node8 + +ASSIGN +присвоить + + +node7->node8 + + + + +node7->RETURN + + +UNCOND + + +node9 + +GETPROP +получить свойство + + +node8->node9 + + + + +node12 + +FUNCTION +функция + + +node8->node12 + + + + +node10 + +THIS + + +node9->node10 + + + + +node11 + +STRING +строка-константа + + +node9->node11 + + + + +node13 + +NAME + + +node12->node13 + + + + +node14 + +PARAM_LIST + + +node12->node14 + + + + +node15 + +BLOCK + + +node12->node15 + + + + +node16 + +EXPR_RESULT + + +node15->node16 + + + + +node17 + +CALL +вызов функции + + +node16->node17 + + + + +node18 + +NAME +имя(функции) + + +node17->node18 + + + + +node19 + +NAME +имя(параметр) + + +node17->node19 + + + + + diff --git a/3-more/5-compress/2-better-minification/article.md b/3-more/5-compress/2-better-minification/article.md new file mode 100644 index 00000000..68b4debb --- /dev/null +++ b/3-more/5-compress/2-better-minification/article.md @@ -0,0 +1,182 @@ +# Улучшаем сжатие кода + +Здесь мы обсудим разные приёмы, которые используются, чтобы улучшить сжатие кода. +[cut] +## Больше локальных переменных + +Например, код jQuery обёрнут в функцию, запускаемую "на месте". + +```js +(function(window, undefined) { + // ... + var jQuery = ... + + window.jQuery = jQuery; // сделать переменную глобальной + +})(window); +``` + +Переменные `window` и `undefined` стали локальными. Это позволит сжимателю заменить их на более короткие. + +## ООП без прототипов + +Приватные переменные будут сжаты и заинлайнены. + +Например, этот код хорошо сожмётся: + +```js +function User(firstName, lastName) { + var fullName = firstName + ' ' + lastName; + + this.sayHi = function() { + showMessage(fullName); + } + + function showMessage(msg) { + alert('**' + msg + '**'); + } +} +``` + +..А этот -- плохо: + +```js +function User(firstName, lastName) { + this._firstName = firstName; + this._lastName = lastName; +} + +User.prototype.sayHi = function() { + this._showMessage(this._fullName); +} + + +User.prototype._showMessage = function(msg) { + alert('**' + msg + '**'); +} +``` + +Сжимаются только локальные переменные, свойства объектов не сжимаются, поэтому эффект от сжатия для второго кода будет совсем небольшим. + +При этом, конечно, нужно иметь в виду общий стиль ООП проекта, достоинства и недостатки такого подхода. + +## Сжатие под платформу, define + +Можно делать разные сборки в зависимости от платформы (мобильная/десктоп) и браузера. + +Ведь не секрет, что ряд функций могут быть реализованы по разному, в зависимости от того, поддерживает ли среда выполнения нужную возможность. + +### Способ 1: локальная переменная + +Проще всего это сделать локальной переменной в модуле: + +```js +(function($) { + +*!* + /** @const */ + var platform = 'IE'; +*/!* + + // ..... + + if (platform == 'IE') { + alert('IE'); + } else { + alert('NON-IE'); + } + +})(jQuery); +``` + +Нужное значение директивы можно вставить при подготовке JavaScript к сжатию. + +Сжиматель заинлайнит её и оптимизирует соответствующие IE. + +### Способ 2: define + +UglifyJS и GCC позволяют задать значение глобальной переменной из командной строки. + +В GCC эта возможность доступна лишь в "продвинутом режиме" работы оптимизатора, который мы рассмотрим далее (он редко используется). + +Удобнее в этом плане устроен UglifyJS. В нём можно указать флаг `-d SYMBOL[=VALUE]`, который заменит все переменные `SYMBOL` на указанное значение `VALUE`. Если `VALUE` не указано, то оно считается равным `true`. + +Флаг не работает, если переменная определена явно. + +Например, рассмотрим код: + +```js +// my.js +if (isIE) { + alert("Привет от IE"); +} else { + alert("Не IE :)"); +} +``` + +Сжатие вызовом `uglifyjs -d isIE my.js` даст: + +```js +alert("Привет от IE"); +``` + +..Ну а чтобы код работал в обычном окружении, нужно определить в нём значение переменной по умолчанию. Это обычно делается в каком-то другом файле (на весь проект), так как если объявить `var isIE` в этом, то флаг `-d isIE` не сработает. + +Но можно и "хакнуть" сжиматель, объявив переменную так: + +```js +// объявит переменную при отсутствии сжатия +// при сжатии не повредит +window.isIE = window.isIE || getBrowserVersion(); +``` + +## Убираем вызовы console.* + +Минификатор имеет в своём распоряжении дерево кода и может удалить ненужные вызовы. Хотя по умолчанию в UglifyJS и GCC такого флага нет, код минификатора можно легко расширить, добавив эту возможность. + +
                            +
                          • Для UglifyJS функция обхода дерева: + +```js +//+ hide="Раскрыть функцию" +function ast_squeeze_console(ast) { + var w = pro.ast_walker(), + walk = w.walk, + scope; + return w.with_walkers({ + "stat": function(stmt) { + if(stmt[0] === "call" && stmt[1][0] == "dot" && stmt[1][1] instanceof Array && stmt[1][1][0] == 'name' && stmt[1][1][1] == "console") { + return ["block"]; + } + return ["stat", walk(stmt)]; + }, + "call": function(expr, args) { + if(expr[0] == "dot" && expr[1] instanceof Array && expr[1][0] == 'name' && expr[1][1] == "console") { + return ["atom", "0"]; + } + } + }, function() { + return walk(ast); + }); +}; +``` + +Полный код, использующий эту функцию и модуль uglify-js для сжатия с убиранием вызова: [myuglify.js](/files/tutorial/compress/myuglify.js). + +Вы можете добавить свои любимые опции и использовать его вместо поставляемого "из коробки". +
                          • +
                          • В GCC соответствующие опции называются `stripNameSuffixes` и `stripTypePrefixes`, но они скрыты при запуске минификатора из командной строки или через веб-сервис. + +Чтобы их использовать, нужно либо поставить утилиту [plovr](http://plovr.com/), что эквивалентно стрельбе из пушки по воробьям, либо написать свой Java-код для вызова компилятора, который будет ставить опции. + +Выглядеть этот код может [примерно так](https://groups.google.com/d/msg/closure-compiler-discuss/qqdnzikzpWA/8cPHKIR1svgJ). +
                          • +
                          + +Аналогичным образом можно убрать и любой другой код, например вызовы вашего собственного логгера. + + + + + + diff --git a/3-more/5-compress/3-gcc-advanced-optimization/article.md b/3-more/5-compress/3-gcc-advanced-optimization/article.md new file mode 100644 index 00000000..8e7bf6ca --- /dev/null +++ b/3-more/5-compress/3-gcc-advanced-optimization/article.md @@ -0,0 +1,518 @@ +# GCC: продвинутые оптимизации + +Продвинутый режим оптимизации google closure compiler включается опцией --compilation_level ADVANCED_OPTIMIZATIONS. + +Слово "продвинутый" (advanced) здесь, пожалуй, не совсем подходит. Было бы более правильно назвать его "супер-агрессивный-ломающий-ваш-неподготовленный-код-режим". Кардинальное отличие применяемых оптимизаций от обычных (simple) -- в том, что они небезопасны. + +Чтобы им пользоваться -- надо уметь это делать. +[cut] + +## Основной принцип продвинутого режима + +
                            +
                          • Если в обычном режиме переименовываются только локальные переменные внутри функций, то в "продвинутом" -- на более короткие имена заменяется все.
                          • +
                          • Если в обычном режиме удаляется недостижимый код после return, то в продвинутом -- вообще весь код, который не вызывается в явном виде.
                          • +
                          + +Например, если запустить продвинутую оптимизацию на таком коде: + +```js +// my.js +function test(node) { + node.innerHTML = "newValue" +} +``` + +Строка запуска компилятора: + +``` +java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js my.js +``` + +...То результат будет -- пустой файл. Google Closure Compiler увидит, что функция test не используется, и с чистой совестью вырежет ее. + +А в следующем скрипте функция сохранится: + +```js +function test(n) { + alert("this is my test number " + n); +} +test(1); +test(2); +``` + +После сжатия: + +```js +function a(b) { + alert("this is my test number " + b) +} +a(1); +a(2); +``` + +Здесь в скрипте присутствует явный вызов функции, поэтому она сохранилась. + +Конечно, есть способы, чтобы сохранить функции, вызов которых происходит вне скрипта, и мы их обязательно рассмотрим. + +**Продвинутый режим сжатия не предусматривает сохранения глобальных переменных. Он переименовывает, инлайнит, удаляет вообще все символы, кроме зарезервированных.** + +Иначе говоря, продвинутый режим (ADVANCED_OPTIMIZATIONS), в отличие от простого (SIMPLE_OPTIMIZATIONS -- по умолчанию), вообще не заботится о доступности кода извне и сохранении ссылочной целостности относительно внешних скриптов. + +Единственное, что он гарантирует -- это внутреннюю ссылочную целостность, и то -- при соблюдении ряда условий и практик программирования. + +Собственно, за счет такого агрессивного подхода и достигается дополнительный эффект оптимизации и сжатия скриптов. + +[summary] +То есть, продвинутый режим - это не просто "улучшенный обычный", а принципиально другой, небезопасный и обфусцирующий подход к сжатию. + +Этот режим является "фирменной фишкой" Google Closure Compiler, недоступной при использовании других компиляторов. +[/summary] + +Для того, чтобы эффективно сжимать Google Closure Compiler в продвинутом режиме, нужно понимать, что и как он делает. Это мы сейчас обсудим. + +### Сохранение ссылочной целостности + +Чтобы использовать сжатый скрипт, мы должны иметь возможность вызывать функции под теми именами, которые им дали. + +То есть, перед нами стоит задача *сохранения ссылочной целостности*, которая заключается в том, чтобы обеспечить доступность нужных функций для обращений по исходному имени извне скрипта. + +Существует два способа сохранения внешней ссылочной целостности: экстерны и экспорты. Мы в подробностях рассмотрим оба, но перед этим необходимо упомянуть о модулях -- другой важнейшей возможности GCC. + +### Модули + +При сжатии GCC можно указать одновременно много JavaScript-файлов. "Эка невидаль, " -- скажете вы, и будете правы. Да, пока что ничего особого. + +Но в дополнение к этому можно явно указать, какие исходные файлы сжать в какие файлы результата. То есть, разбить итоговую сборку на модули. + +Так что страницы могут грузить модули по мере надобности. Например, по умолчанию -- главный, а дополнительная функциональность -- загружаться лишь там, где она нужна. + +Для такой сборки используется флаг компилятора `--module имя:количество файлов`. + +Например: + +``` +java -jar compiler.jar --js base.js --js main.js --js admin.js --module +first:2 --module second:1:first +``` + +Эта команда создаст модули: first.js и second.js. + +Первый модуль, который назван "first", создан из объединённого и оптимизированного кода первых двух файлов (`base.js` и `main.js`). + +Второй модуль, который назван "second", создан из `admin.js` -- это следующий аргумент `--js` после включенных в первый модуль. + +Второй модуль в нашем случае зависит от первого. Флаг `--module second:1:first` как раз означает, что модуль `second` будет создан из одного файла после вошедших в предыдущий модуль (`first`) и зависит от модуля `first`. + +А теперь -- самое вкусное. + +**Ссылочная целостность между всеми получившимися файлами гарантируется.** + +Если в одном функция `doFoo` заменена на `b`, то и в другом тоже будет использоваться `b`. + +Это означает, что проблем между JS-файлами не будет. Они могут свободно вызывать друг друга без экспорта, пока находятся в единой модульной сборке. + +### Экстерны + +Экстерн (extern) -- имя, которое числится в специальном списке компилятора. Он должен быть определен вне скрипта, в файле экстернов. + +**Компилятор никогда не переименовывает экстерны.** + +Например: + +```js +document.onkeyup = function(event) { alert(event.type) } +``` + +После продвинутого сжатия: + +```js +document.onkeyup = function(a) { alert(a.type) } +``` + +Как видите, переименованной оказалась только переменная `event`. Такое переименование заведомо безопасно, т.к. `event` -- локальная переменная. + +Почему компилятор не тронул остального? Попробуем другой вариант: + +```js +document.blabla = function(event) { alert(event.megaProperty) } +``` + +После компиляции: + +```js +document.a = function(a) { alert(a.b) } +``` + +Теперь компилятор переименовал и blabla и megaProperty. + +Дело в том, что названия, использованные до этого, были во внутреннем списке экстернов компилятора. Этот список охватывает основные объекты браузеров и находится (под именем externs.zip) в корне архива compiler.jar. + +**Компилятор переименовывает имя списка экстернов только когда так названа локальная переменная.** + +Например: + +```js +window.resetNode = function(node) { + var innerHTML = "test"; + node.innerHTML = innerHTML; +} +``` + +На выходе: + +```js +window.a = function(a) { + a.innerHTML = "test" +}; +``` + +Как видите, внутренняя переменная innerHTML не просто переименована - она заинлайнена (заменена на значение). Так как переменная локальна, то любые действия внутри функции с ней безопасны. + +А свойство innerHTML не тронуто, как и объект window -- так как они в списке экстернов и не являются локальными переменными. + +Это приводит к следующему побочному эффекту. Иногда свойства, которые следовало бы сжать, не сжимаются. Например: + +```js +window['User'] = function(name, type, age) { + this.name = name + this.type = type + this.age = age +} +``` + +После сжатия: + +```js +window.User = function(a, b, c) { + this.name = a; + this.type = b; + this.a = c +}; +``` + +Как видно, свойство age сжалось, а name и type -- нет. Это побочный эффект экстернов: name и type -- в списке объектов браузера, и компилятор просто старается не наломать дров. + +Поэтому отметим еще одно полезное правило оптимизации: + +**Названия своих свойств не должны совпадать с зарезервированными словами (экстернами). Тогда они будут хорошо сжиматься.** + +Для задания списка экстернов их достаточно перечислить в файле и указать этот файл флагом --externs <файл экстернов.js>. + +При перечислении объектов в файле экстернов - объявляйте их и перечисляйте свойства. Все эти объявления никуда не идут, они используются только для создания списка, который обрабатывается компилятором. + +Например, файл `myexterns.js`: + +```js +var dojo = {} +dojo._scopeMap; +``` + +Использование такого файла при сжатии (опция --externs myexterns.js) приведет к тому, что все обращения к символам dojo и к dojo._scopeMap будут не сжаты, а оставлены "как есть". + + +### Экспорт + +*Экспорт* -- программный ход, основанный на следующем правиле поведения компилятора. + +**Компилятор заменяет обращения к свойствам через кавычки на точку, и при этом не трогает название свойства.** + +Например, window['User'] превратится в window.User, но не дальше. + +Таким образом можно *"экспортировать"* нужные функции и объекты: + +```js +function SayWidget(elem) { + this.elem = elem + this.init() +} +window['SayWidget'] = SayWidget; +``` + +На выходе: + +```js +function a(b) { + this.a = b; + this.b() +} +window.SayWidget = a; +``` + +Обратим внимание -- сама функция SayWidget была переименована в a. Но затем -- экспортирована как window.SayWidget, и таким образом доступна внешним скриптам. + +Добавим пару методов в прототип: + +```js +function SayWidget(elem) { + this.elem = elem; + this.init(); +} + +SayWidget.prototype = { + init: function() { + this.elem.style.display = 'none' + }, + + setSayHandler: function() { + this.elem.onclick = function() { + alert("hi") + }; + } +} + +window['SayWidget'] = SayWidget; +SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler; +``` + +После сжатия: + +```js +function a(b) { + this.a = b; + this.b() +} +a.prototype = {b:function() { + this.a.style.display = "none" +}, c:function() { + this.a.onclick = function() { + alert("hi") + } +}}; +window.SayWidget = a; +a.prototype.setSayHandler = a.prototype.c; +``` + +Благодаря строке + +```js +SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler +``` + +метод setSayHandler экспортирован и доступен для внешнего вызова. + +Сама строка экспорта выглядит довольно глупо. По виду -- присваиваем свойство самому себе. + +Но логика сжатия GCC работает так, что такая конструкция является экспортом. Справа переименование свойства setSayHandler происходит, а слева -- нет. + +[smart header="Планируйте жизнь после сжатия"] + +Рассмотрим следующий код: + +```js +window['Animal'] = function() { + this.blabla = 1; + this['blabla'] = 2; +} +``` + +После сжатия: + +```js +window.Animal = function() { + this.a = 1; + this.blabla = 2 +}; +``` + +Как видно, первое обращение к свойству blabla сжалось, а второе (как и все аналогичные) -- преобразовалось в синтаксис через точку. +В результате получили некорректное поведение кода. + +Так что, используя продвинутый режим оптимизации, планируйте поведение кода после сжатия. + +**Если где-то возможно обращение к свойствам через квадратные скобки по полному имени -- такое свойство должно быть экспортировано.** +[/smart] + +### goog.exportSymbol и goog.exportProperty + +В библиотеке [Google Closure Library](https://developers.google.com/closure/library/) для экспорта есть специальная функция goog.exportSymbol. Вызывается так: + +```js +goog.exportSymbol('my.SayWidget', SayWidget) +``` + +Эта функция по сути работает также, как и рассмотренная выше строка с присвоением свойства, но при необходимости создает нужные объекты. + +Она аналогична коду: + +```js +window['my'] = window['my'] || {} +window['my']['SayWidget'] = SayWidget +``` + +То есть, если путь к объекту не существует -- exportSymbol создаст нужные пустые объекты. + +Функция goog.exportProperty экспортирует свойство объекта: + +```js +goog.exportProperty(SayWidget.prototype, 'setSayHandler', SayWidget.prototype.setSayHandler) +``` + +Строка выше - то же самое, что и: + +```js +SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler +``` + +Зачем они нужны, если все можно сделать простым присваиванием? + +Основная цель этих функций -- во взаимодействии с Google Closure Compiler. Они дают информацию компилятору об экспортах, которую он может использовать. + +Например, есть недокументированная внутренняя опция externExportsPath, которая генерирует из всех экспортов файл экстернов. Таким образом можно распространять откомпилированный JavaScript-файл как внешнюю библиотеку, с файлом экстернов для удобного внешнего связывания. + +Кроме того, экспорт через эти функциями удобен и нагляден. + +Если вы используете продвинутый режим оптимизации, то можно взять их из файла base.js Google Closure Library. Можно и подключить этот файл целиком -- оптимизатор при продвинутом сжатии вырежет из него почти всё лишнее, так что overhead будет минимальным. + +### Отличия экспорта от экстерна + +Между экспортом и экстерном есть кое-что общее. И то и другое дает возможность доступа к объектам под исходным именем, до переименования. + +Но, в остальном, это совершенно разные вещи. + + + + + + + + + + + + + + + + + + + + +
                          ЭкстернЭкспорт
                          Служит для тотального запрета на переименование всех обращений к свойству. +Задумано для сохранения обращений к стандартным объектам браузера, внешним библиотекам.Служит для открытия доступа к свойству извне под указанным именем. +Задумано для открытия внешнего интерфейса к сжатому скрипту.
                          Работает со свойством, объявленным вне скрипта. +Вы не можете объявить новое свойство в скрипте и сделать его экстерном.Создает ссылку на свойство, объявленное в скрипте.
                          Если window - экстерн, то все обращения к window в скрипте останутся как есть.Если user экспортируется, то создается только одна ссылка под полным именем, а все остальные обращения будут сокращены.
                          + + +## Стиль разработки + +Посмотрим, как сжиматель поведёт себя на следующем, типичном, объявлении библиотеки: + +```js +(function(window, undefined) { + + // пространство имен и локальная перменная для него + var MyFramework = window.MyFramework = {}; + + // функция фреймворка, доступная снаружи + MyFramework.publicOne = function() { + makeElem(); + }; + + // приватная функция фреймворка + function makeElem() { + var div = document.createElement('div'); + document.body.appendChild(div); + } + + // еще какая-то функция + MyFramework.publicTwo = function() {}; + +})(window); + +// использование +MyFramework.publicOne(); +``` + +Результат компиляции в обычном режиме: + +```js +// java -jar compiler.jar --js myframework.js --formatting PRETTY_PRINT +(function(a) { + a = a.MyFramework = {}; + a.publicOne = function() { + var a = document.createElement("div"); + document.body.appendChild(a) + }; + a.publicTwo = function() { + } +})(window); +MyFramework.publicOne(); +``` + +Это -- примерно то, что мы ожидали. Неиспользованный метод `publicTwo` остался, локальные свойства переименованы и заинлайнены. + +А теперь продвинутый режим: + +```js +// --compilation_level ADVANCED_OPTIMIZATIONS +window.a = {}; +MyFramework.b(); +``` + +Оно не работает! Компилятор попросту не разобрался, что и как вызывается, и превратил рабочий JS-файл в один сплошной баг. + +В зависимости от версии GCC у вас может быть и что-то другое. + +Всё дело в том, что такой стиль объявления нетипичен для инструментов, которые в самом Google разрабатываются и сжимаются этим минификатором. + +Типичный правильный стиль: + +```js +// пространство имен и локальная перменная для него +var MyFramework = {}; + +MyFrameWork._makeElem = function() { + var div = document.createElement('div'); + document.body.appendChild(div); +}; + +MyFramework.publicOne = function() { + MyFramework._makeElem(); +}; + +MyFramework.publicTwo = function() {}; + +// использование +MyFramework.publicOne(); +``` + +Обычное сжатие здесь будет бесполезно, а вот продвинутый режим идеален: + +```js +// в зависимости от версии GCC результат может отличаться +MyFrameWork.a = function() { + var a = document.createElement("div"); + document.body.appendChild(a) +}; +MyFrameWork.a(); +``` + +Google Closure Compiler не только разобрался в структуре и удалил лишний метод - он заинлайнил функции, чтобы итоговый размер получился минимальным. + +Как говорится, преимущества налицо. + +## Резюме + +Продвинутый режим оптимизации сжимает, оптимизирует и, при возможности, удаляет все свойства и методы, за исключением экстернов. + +Это является принципиальным отличием, по сравнению с другими упаковщиками. + +Отказ от сохранения внешней ссылочной целостности с одной стороны позволяет увеличить уровень сжатия, но требует поддержки со стороны разработчика. + +Основная проблема этого сжатия -- усложнение разработки. Добавляется дополнительный уровень возможных проблем: сжатие. Конечно, можно отлаживать и сжатый код, для этого придуманы [Source Maps](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/), но клиентская разработка и без того достаточно сложна. + +Поэтому его используют редко. + +Как правило, есть две причины для использования продвинутого режима: +
                            +
                          1. **Обфускация кода.** + +Если в коде после обычного сжатия ещё как-то можно разобраться, то после продвинутого -- уже нет. Всё переименовано и заинлайнено. В теории это, конечно, возможно, но "порог входа" в такой код несоизмеримо выше. + +Судя по виду скриптов на сайтах, созданных Google, сам Google жмет свои скрипты именно продвинутым режимом оптимизации. И библиотека Google Closure Library тоже рассчитана на него.
                          2. +
                          3. **Хорошие сжатие виджетов, счётчиков.** + +Небольшой код, который отдаётся наружу, может быть сжат в продвинутом режиме. Так как он небольшой -- все ошибки можно легко исправить, а продвинутый режим гарантирует наилучшее сжатие.
                          4. +
                          diff --git a/3-more/5-compress/4-gcc-check-types/article.md b/3-more/5-compress/4-gcc-check-types/article.md new file mode 100644 index 00000000..293a4dda --- /dev/null +++ b/3-more/5-compress/4-gcc-check-types/article.md @@ -0,0 +1,175 @@ +# GCC: статическая проверка типов + +Google Closure Compiler, как и любой кошерный компилятор, старается проверить правильность кода и предупредить о возможных ошибках. + +Первым делом он, разумеется, проверяет структуру кода и сразу же выдает такие ошибки как пропущенная скобка или лишняя запятая. + +Но, кроме этого, он умеет проверять типы переменных, используя как свои собственные знания о встроенных javascript-функциях и преобразованиях типов, +так и информацию о типах из JSDoc, указываемую javascript-разработчиком. + +Это обеспечивает то, чем так гордятся компилируемые языки -- статическую проверку типов, что позволяет избежать лишних ошибок во время выполнения. +[cut] + +Для вывода предупреждений при проверки типов используется флаг `--jscomp_warning checkTypes`. + +## Задание типа при помощи аннотации + +Самый очевидный способ задать тип -- это использовать аннотацию. Полный список аннотаций вы найдете в документации. + +В следующем примере параметр id функции f1 присваивается переменной boolVar другого типа: + +```js +/** @param {number} id */ +function f(id) { + /** @type {boolean} */ + var boolVar; + + boolVar = id; // (!) +} +``` + +Компиляция с флагом `--jscomp_warning checkTypes` выдаст предупреждение: + +``` +f.js:6: WARNING - assignment +found : number +required: boolean + boolVar = id; // (!) + ^ +``` + +Действительно: произошло присвоение значения типа number переменной типа boolean. + +Типы отслеживаются по цепочке вызовов. + +Еще пример, на этот раз вызов функции с некорректным параметром: + +```js +/** @param {number} id */ +function f1(id) { + f2(id); // (!) +} + +/** @param {string} id */ +function f2(id) { } +``` + +Такой вызов приведёт к предупреждению со стороны минификатора: + +``` +f2.js:3: WARNING - actual parameter 1 of f2 does not match formal parameter +found : number +required: string + f2(id); // (!) + ^ +``` + +Действительно, вызов функции f2 произошел с числовым типом вместо строки. + +**Отслеживание приведений и типов идёт при помощи графа взаимодействий и выведению (infer) типов, который строит GCC по коду.** + +## Знания о преобразовании типов + +Google Closure Compiler знает, как операторы javascript преобразуют типы. Такой код уже не выдаст ошибку: + +```js +/** @param {number} id */ +function f1(id) { + /** @type {boolean} */ + var boolVar; + + boolVar = !!id +} +``` + +Действительно - переменная преобразована к типу boolean двойным оператором НЕ. +А код boolVar = 'test-'+id выдаст ошибку, т.к. конкатенация со строкой дает тип string. + +## Знание о типах встроенных функций, объектные типы + +Google Closure Compiler содержит описания большинства встроенных объектов и функций javascript вместе с типами параметров и результатов. + +Например, объектный тип Node соответствует узлу DOM. + +Пример некорректного кода: + +```js +/** @param {Node} node */ +function removeNode(node) { + node.parentNode.removeChild(node) +} +document.onclick = function() { + removeNode("123") +} +``` + +Выдаст предупреждение + +``` +f3.js:7: WARNING - actual parameter 1 of removeNode does not match formal parameter +found : string +required: (Node|null) + removeNode("123") + ^ +``` + +Обратите внимание - в этом примере компилятор выдает required: Node|null. Это потому, что указание объектного типа (не элементарного) подразумевает, что в функцию может быть передан null. + +В следующем примере тип указан жестко, без возможности обнуления: + +```js +*!* +/** @param {!Node} node */ +*/!* +function removeNode(node) { + node.parentNode.removeChild(node) +} +``` + +Восклицательный знак означает, что параметр обязатален. + +Найти описания встроенных типов и объектов javascript вы можете в файле экстернов: externs.zip находится в корне архива compiler.jar, или в соответствующей директории SVN: http://closure-compiler.googlecode.com/svn/trunk/externs/. + +## Интеграция с проверками типов из Google Closure Library + +В Google Closure Library есть функции проверки типов: goog.isArray, goog.isDef, goog.isNumber и т.п. + +Google Closure Compiler знает о них и понимает, что внутри следующего if переменная может быть только функцией: + +```js +var goog = { + isFunction: function(f) { return typeof f == 'function' } +} + +if (goog.isFunction(func)) { + func.apply(1, 2) +} +``` + +Сжатие с проверкой выдаст предупреждение: + +``` +f.js:6: WARNING - actual parameter 2 of Function.apply does not match formal parameter +found : number +required: (Object|null|undefined) + func.apply(1, 2) + ^ ^ +``` + +То есть, компилятор увидел, что код, использующий func находится в `if (goog.isFunction(func))` и сделал соответствующий вывод, что это в этой ветке `func` является функцией, а значит вызов `func.apply(1,2)` ошибочен (второй аргумент не может быть числом). + +Дело тут именно в интеграции с Google Closure Library. Если поменять `goog` на `g` -- предупреждения не будет. + +## Резюме + +Из нескольких примеров, которые мы рассмотрели, должна быть понятна общая логика проверки типов. + +Соответствующие различным типам и ограничениям на типы аннотации вы можете найти в Документации Google. В частности, возможно указание нескольких возможных типов, типа undefined и т.п. + +Также можно указывать количество и тип параметров функции, ключевого слова this, объявлять классы, приватные методы и интерфейсы. + +Проверка типов javascript, предоставляемая Google Closure Compiler - пожалуй, самая продвинутая из существующих на сегодняшний день. + +C ней аннотации, документирующие типы и параметры, становятся не просто украшением, а реальным средством проверки, уменьшающим количество ошибок на production. + +Очень подробно проверка типов описана в книге [Closure: The Definitive Guide](http://www.ozon.ru/context/detail/id/6089988/), автора Michael Bolin. \ No newline at end of file diff --git a/3-more/5-compress/5-gcc-closure-library/article.md b/3-more/5-compress/5-gcc-closure-library/article.md new file mode 100644 index 00000000..ffd5d881 --- /dev/null +++ b/3-more/5-compress/5-gcc-closure-library/article.md @@ -0,0 +1,183 @@ +# GCC: интеграция с Google Closure Library + +Google Closure Compiler содержит ряд специальных возможностей для интеграции с Google Closure Library. + +Здесь важны две вещи. +
                            +
                          1. Для их использования возможно использовать минимум от Google Closure Library. Например, взять одну или несколько функций из библиотеки.
                          2. +
                          3. GCC -- расширяемый компилятор, можно добавить к нему свои "фазы оптимизации" для интеграции с другими инструментами и фреймворками.
                          4. +
                          +[cut] + +Интеграция с Google Closure Library подключается флагом --process_closure_primitives, который по умолчанию установлен в true. То есть, она включена по умолчанию. + +Этот флаг запускает специальный проход компилятора, описанный классом ProcessClosurePrimitives и подключает дополнительную проверку типов ClosureReverseAbstractInterpreter. + +Мы рассмотрим все действия, которые при этом происходят, а также некоторые опции, которые безопасным образом используют символы Google Closure Library без объявления флага. + +## Преобразование основных символов + +Следующие действия описаны в классе ProcessClosurePrimitives. + +### Замена константы COMPILED + +В Google Closure Library есть переменная: + +```js +/** + * @define {boolean} ... + */ +var COMPILED = false; +``` + +Проход ProcessClosurePrimitives переопределяет ее в true и использует это при оптимизациях, удаляя ветки кода, не предназначены для запуска на production. + +Такие функции существуют, например, в ядре Google Closure Library. К ним в первую очередь относятся вызовы, предназначенные для сборки и проверки зависимостей. Они содержат код, обрамленный проверкой COMPILED, например: + +```js +goog.require = function(rule) { + // ... + if (!COMPILED) { + // основное тело функции + } +} +``` + +Аналогично может поступить и любой скрипт, даже без использования Google Closure Library: + +```js +/** @define {boolean} */ +var COMPILED = false + +Framework = { } + +Framework.sayCompiled = function() { + if (!COMPILED) { + alert("Not compressed") + } else { + alert("Compressed") + } +} +``` + +Для того, чтобы сработало, нужно сжать в продвинутом режиме: + +```js +Framework = {}; +Framework.sayCompiled = Framework.a = function() { + alert("Compressed"); +}; +``` + +Компилятор переопределил COMPILED в true и произвел соответствующие оптимизации. + +### Автоподстановка локали + +В Google Closure Compiler есть внутренняя опция locale + +Эта опция переопределяет переменную goog.LOCALE на установленную при компиляции. + +Для использования опции locale, на момент написания статьи, ее нужно задать в Java коде компилятора, т.к. соответствующего флага нет. + +Как и COMPILED, константу goog.LOCALE можно и использовать в своем коде без библиотеки Google Closure Library. + +### Проверка зависимостей + +Директивы goog.provide, goog.require, goog.addDependency обрабатываются особым образом. + +Все зависимости проверяются, а сами директивы проверки -- удаляются из сжатого файла. + +### Экспорт символов + +Вызов goog.exportSymbol задаёт экспорт символа. + +Если подробнее, то код goog.exportSymbol('a',myVar) эквивалентен +`window['a'] = myVar`. + + +### Автозамена классов CSS + +Google Closure Library умеет преобразовывать классы CSS на более короткие по списку, который задаётся при помощи `goog.setCssNameMapping`. + +Например, следующая функция задает такой список. + +```js + +goog.setCssNameMapping({ + "goog-menu": "a", + "goog-menu-disabled": "a-b", + "CSS_LOGO": "b", + "hidden": "c" +}); +``` + +Тогда следующий вызов преобразуется в "a a-b": + +```js +goog.getCssName('goog-menu') + ' ' + goog.getCssName('goog-menu', 'disabled') +``` + +Google Closure Compiler производит соответствующие преобразования в сжатом файле и удаляет вызов setCssNameMapping из кода. + +Чтобы это сжатие работало, в HTML/CSS классы тоже должны сжиматься. По всей видимости, в приложениях Google это и происходит, но соответствующие инструменты закрыты от публики. + +### Генерация списка экстернов + +При объявлении внутренней опции externExportsPath, содержащей путь к файлу, в этот файл будут записаны все экспорты, описанные через goog.exportSymbol/goog.exportProperty. + +В дальнейшем этот файл может быть использован как список экстернов для компиляции. + +Эта опция может быть полезна для создания внешних библиотек, распространяемых со списком экстернов. + +Для её использования нужна своя обёртка вокруг компилятора на Java. Соответствующий проход компилятора описан в классе ExternExportsPass. + +### Проверка типов + +В Google Closure Library есть ряд функций для проверки типов. Например: goog.isArray, goog.isString, goog.isNumber, goog.isDef и т.п. + +Компилятор использует их для проверки типов, более подробно см. [](/gcc-check-types) + +Эта логика описана в классе ClosureReverseAbstractInterpreter. Названия функций, определяющих типы, жестко прописаны в Java-коде, поменять их на свои без модификации исходников нельзя. + +### Автогенерация экспортов из аннотаций + +Для этого в Google Closure Compiler есть внутренняя опция generateExports. + +Эта недокументированная опция добавляет проход компилятора, описанный классом GenerateExports. + +Он читает аннотации @export и создает из них экспортирующие вызовы goog.exportSymbol/exportProperty. Название экспортирующих функций находится в классе соглашений кодирования, каким по умолчанию является GoogleCodingConvention. + +Например: + +```js +/** @export */ +function Widget() { +} +/** @export */ +Widget.prototype.hide = function() { + this.elem.style.display = 'none' +} +``` + +После компиляции в продвинутом режиме: + +```js +function a() { +} +goog.d("Widget", a); +a.prototype.a = function() { + this.b.style.display = "none" +}; +goog.c(a.prototype, "hide", a.prototype.a); +``` + +Свойства благополучно экспортированы. Удобно. + +### Резюме + +Google Closure Compiler содержит дополнительные фичи, облегчающие интеграцию с Google Closure Library. Некоторые из них весьма полезны, но требуют создания своего Java-файла, который ставит внутренние опции. + +При обработке символов компилятор не смотрит, подключена ли библиотека, он находит обрабатывает их просто по именам. Поэтому вы можете использовать свою реализацию соответствующих функций. + +Google Closure Compiler можно легко расширить, добавив свои опции и проходы оптимизатора, для интеграции с вашими инструментами. + diff --git a/3-more/5-compress/index.md b/3-more/5-compress/index.md new file mode 100644 index 00000000..9bee2987 --- /dev/null +++ b/3-more/5-compress/index.md @@ -0,0 +1,2 @@ +# Сжатие JavaScript + diff --git a/3-more/6-extra/1-memory-removechild-innerhtml/article.md b/3-more/6-extra/1-memory-removechild-innerhtml/article.md new file mode 100644 index 00000000..257fe6c1 --- /dev/null +++ b/3-more/6-extra/1-memory-removechild-innerhtml/article.md @@ -0,0 +1,139 @@ +# Очистка памяти при removeChild/innerHTML + +Управление памятью в случае с DOM работает по сути так же, как и с обычными JavaScript-объектами. Пока объект достижим -- он остаётся в памяти. + +Но есть и особенности, поскольку DOM весь переплетён ссылками. +[cut] +## Пример +Для примера рассмотрим следующий HTML: + + + + + + + + + +
                          КодСтруктура в памяти
                          + +```html + + +
                          +
                            +
                          • Список
                          • +
                          + Сосед +
                          + + +``` + +
                          + +
                          + +## Удаление removeChild + +Операция `removeChild` разрывает все связи удаляемым узлом и его родителем. + +Поэтому, если удалить `DIV` из `BODY`, то всё поддерево под `DIV` станет недостижимым и будет удалено. + +А что происходит, если на какой-то элемент внутри удаляемого поддерева есть ссылка? + +Например, `UL` сохранён в переменную `list`: + +```js +var list = document.getElementsByTagName('UL')[0]; +document.body.removeChild(document.body.children[0]); +``` + +**В этом случае, так как DOM взаимосвязан, то он полностью остаётся в памяти!** Включая детей, родителей, соседей и т.п... Всё из-за внешней ссылки `list`, которая делает их достижимыми. + + + +То есть, в этом случае DOM работает по той же логике, что и обычные объекты. + +## Удаление через innerHTML + +Удаление через очистку `elem.innerHTML="..."` браузеры интерпретируют по-разному. + +По идее, при присвоении `innerHTML` из DOM должны удаляться текущие узлы и добавляться новые (из html). Но стандарт ничего не говорит о том, что делать с узлами после удаления. И тут разные браузеры имеют разное мнение. + +Посмотрим, что произойдёт с DOM-структурой при очистке `BODY`, если на какой-либо элемент есть ссылка. + +```js +var list = document.getElementsByTagName('UL')[0]; +document.body.innerHTML = ""; +``` + +Обращаю внимание -- связь разрывается только между `DIV` и `BODY`, т.е. на верхнем уровне, а `list` -- это произвольный элемент. + +Чтобы увидеть, что останется в памяти, а что нет -- запустим код: + +```html + +
                          +
                            +
                          • Список
                          • +
                          + Сосед +
                          + + +``` + +Как ни странно, браузеры ведут себя по-разному: + + + + + + + + + + + + + + + + + + + + + + + + + +
                          `parentNode``nextSibling``children.length`
                          Chrome/Safari/Opera`null``null``1`
                          FirefoxЭлементЭлемент`1`
                          IE 6-9`null``null``0`
                          + +Иными словами, браузеры ведут себя с различной степенью агрессивности по отношению к элементам. + +
                          +
                          Firefox, Opera
                          +
                          Главные пацифисты. Оставляют всё, на что есть ссылки, т.е. элемент, его родителя, соседей и детей.
                          +
                          Chrome/Safari
                          +
                          Считают, что раз мы задали ссылку на `UL`, то нам нужно только это поддерево, а остальные (соседи, родитель) можно удалить.
                          +
                          Internet Explorer
                          +
                          Как ни странно, самый агрессивный. Удаляет вообще всё, кроме узла, на который есть ссылка. Это поведение одинаково для всех версий IE.
                          +
                          + +На иллюстрации ниже показано, какую часть DOM оставит каждый из браузеров: + + +Таким образом, кросс-браузерно, при поддержке IE, гарантировано одно: **сам узел, на который есть ссылка, останется в памяти.** + +Это поведение специфично для `innerHTML`. При обычном удалении узла из DOM все браузеры ведут себя как `Firefox/Opera`, т.е. остаётся всё. + diff --git a/3-more/6-extra/1-memory-removechild-innerhtml/html-innerhtml.png b/3-more/6-extra/1-memory-removechild-innerhtml/html-innerhtml.png new file mode 100755 index 00000000..b26311fb Binary files /dev/null and b/3-more/6-extra/1-memory-removechild-innerhtml/html-innerhtml.png differ diff --git a/3-more/6-extra/1-memory-removechild-innerhtml/html-list.png b/3-more/6-extra/1-memory-removechild-innerhtml/html-list.png new file mode 100755 index 00000000..ceaf0ffe Binary files /dev/null and b/3-more/6-extra/1-memory-removechild-innerhtml/html-list.png differ diff --git a/3-more/6-extra/1-memory-removechild-innerhtml/html.png b/3-more/6-extra/1-memory-removechild-innerhtml/html.png new file mode 100755 index 00000000..7c3e8898 Binary files /dev/null and b/3-more/6-extra/1-memory-removechild-innerhtml/html.png differ diff --git a/3-more/6-extra/10-cookie/article.md b/3-more/6-extra/10-cookie/article.md new file mode 100644 index 00000000..3b232a91 --- /dev/null +++ b/3-more/6-extra/10-cookie/article.md @@ -0,0 +1,253 @@ +# Куки, document.cookie + +Для чтения и записи cookie используется свойство `document.cookie`. Однако, оно представляет собой не объект, а строку в специальном формате, для удобной манипуляций с которой нужны дополнительные функции. + +[cut] + +## Чтение document.cookie + +Наверняка у вас есть cookie, которые привязаны к этому сайту. Давайте полюбуемся на них. Вот так: + +```js +//+ run +alert( document.cookie ); +``` + +Эта строка состоит из пар `ключ=значение`, которые перечисляются через точку с запятой с пробелом `; `. + +Значит, чтобы прочитать cookie, достаточно разбить строку по `; `, и затем найти нужный ключ. Это можно делать либо через `split` и работу с массивом, либо через регулярное выражение. + +### Функция getCookie(name) + +Следующая функция `getCookie(name)` возвращает cookie с именем `name`: + +```js +// возвращает cookie с именем name, если есть, если нет, то undefined +function getCookie(name) { + var matches = document.cookie.match(new RegExp( + "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" + )); + return matches ? decodeURIComponent(matches[1]) : undefined; +} +``` + +Обратим внимание, что значение может быть любым. Если оно содержит символы, нарушающие форматирование, например, пробелы или `;`, то оно кодируется при помощи `encodeURIComponent`. Функция `getCookie` автоматически раскодирует его. + +## Запись в document.cookie + +В `document.cookie` можно писать. При этом запись не перезаписывает существующие cookie, а дополняет к ним! + +Например, такая строка поставит cookie с именем `userName` и значением `Vasya`: + +```js +//+ run +document.cookie = "userName=Vasya"; +``` + +...Однако, всё не так просто. У cookie есть ряд важных настроек, которые очень желательно указать, так как значения по умолчанию у них неудобны. + +Эти настройки указываются после пары ключ=значение, каждое -- после точки с запятой: +
                          +
                          `path=/mypath`
                          +
                          Путь, внутри которого будет доступ к cookie. Если не указать, то имеется в виду текущий путь и все пути ниже него. + +Как правило, используется `path=/`, то есть cookie доступно со всех страниц сайта.
                          +
                          `domain=site.com`
                          +
                          Домен, на котором доступно cookie. Если не указать, то текущий домен. Допустимо указывать текущий домен `site.com` и его поддомены, например `forum.site.com`. + +Если указать специальную маску `.site.com`, то cookie будет доступно на сайте и всех его поддоменах. Это используется, например, в случаях, когда кука содержит данные авторизации и должна быть доступна как на `site.com`, так и на `forum.site.com`. +
                          +
                          `expires=Tue, 19 Jan 2038 03:14:07 GMT`
                          +
                          Дата истечения куки в формате GMT. Получить нужную дату можно, используя объект `Date`. Его можно установить в любое время, а потом вызвать `toUTCString()`, например: + +```js +// +1 день от текущего момента +var date = new Date; +date.setDate( date.getDate() + 1 ); +alert( date.toUTCString() ); +``` + +Если дату не указать, то cookie будет считаться "сессионным". Такое cookie удаляется при закрытии браузера. +Если дата в прошлом, то кука будет удалена. +
                          +
                          `secure`
                          +
                          Cookie можно передавать только по HTTPS.
                          +
                          + +Например, чтобы поставить cookie `name=value` по текущему пути с датой истечения через 60 секунд: + +```js +//+ run +var date = new Date( new Date().getTime() + 60*1000 ); +document.cookie="name=value; path=/; expires="+date.toUTCString(); +``` + +Чтобы удалить это cookie: + +```js +//+ run +var date = new Date(0); +document.cookie="name=; path=/; expires="+date.toUTCString(); +``` + +При удалении значение не важно. Можно его не указывать, как сделано в коде выше. + +### Функция setCookie(name, value, options) + +Если собрать все настройки воедино, вот такая функция ставит куки: + +```js +function setCookie(name, value, options) { + options = options || {}; + + var expires = options.expires; + + if (typeof expires == "number" && expires) { + var d = new Date(); + d.setTime(d.getTime() + expires*1000); + expires = options.expires = d; + } + if (expires && expires.toUTCString) { + options.expires = expires.toUTCString(); + } + + value = encodeURIComponent(value); + + var updatedCookie = name + "=" + value; + + for(var propName in options) { + updatedCookie += "; " + propName; + var propValue = options[propName]; + if (propValue !== true) { + updatedCookie += "=" + propValue; + } + } + + document.cookie = updatedCookie; +} +``` + +Аргументы: +
                          +
                          name
                          название cookie
                          +
                          value
                          значение cookie (строка)
                          +
                          options
                          +Объект с дополнительными свойствами для установки cookie: +
                          +
                          expires
                          Время истечения cookie. Интерпретируется по-разному, в зависимости от типа: +
                            +
                          • Число -- количество секунд до истечения. Например, `expires: 3600` -- кука на час.
                          • +
                          • Объект типа [:Date] -- дата истечения.
                          • +
                          • Если expires в прошлом, то cookie будет удалено.
                          • +
                          • Если expires отсутствует или `0`, то cookie будет установлено как сессионное и исчезнет при закрытии браузера.
                          • +
                          +
                          +
                          path
                          Путь для cookie.
                          +
                          domain
                          Домен для cookie.
                          +
                          secure
                          Если `true`, то пересылать cookie только по защищенному соединению.
                          +
                          +
                          +
                          + +### Функция deleteCookie(name) + +Здесь всё просто -- удаляем вызовом `setCookie` с датой в прошлом. + +```js +function deleteCookie(name) { + setCookie(name, "", { expires: -1 }) +} +``` + +## Сторонние cookie + +При работе с cookie есть важная тонкость, которая касается внешних ресурсов. + +Теоретически, любой ресурс, который загружает браузер, может поставить cookie. + +Например: +
                            +
                          • Если на странице есть ``, то вместе с картинкой в ответ сервер может прислать заголовки, устанавливающие cookie.
                          • +
                          • Если на странице есть `
                          • +
                          • +*!* +
                          • +*/!* + +``` + + +
                            Пустой `src`
                            +
                            Атрибут `src` может использовать протокол `javascript:...`. При этом код выполняется и его результат будет содержимым ифрейма. Этот способ описан в стандарте и поддерживается всеми браузерами. + +Атрибут `src` является обязательным, и его отсутствие может привести к проблемам, вплоть до игнорирования ифрейма браузером. + +**Чтобы ничего не загружать в ифрейм, укажите `src="javascript:false"`.** +
                            +
                            Атрибут `name` и создание ифрейма в IE<8
                            +
                            В старых IE нельзя менять атрибут `name` после создания ифрейма: + +```js +var iframe = document.createElement('iframe'); +iframe.name = 'iName'; // в IE<8 не сработает +``` + +Поэтому, если нужна совместимость, создавайте ифреймы через `innerHTML`: + +```js +var tmp = document.createElement('div'); +tmp.innerHTML = ''; + +var iframe = tmp.firstChild; +``` + +
                            +
                      + +## Ифрейм: доступ к document и window + +Элемент `iframe` является обычным узлом DOM, как и любой другой. Существенное отличие -- в том, что с ним связан объект `window` внутреннего окна. Он доступен по ссылке `iframe.contentWindow`. + +Таким образом, `iframe.contentWindow.document` будет внутренним документом. + +[smart header="Когда-то..."] +В старых браузерах использовались дополнительные свойства, такие как `iframe.contentDocument` и даже `iframe.document`, но они давно не нужны. +[/smart] + +В примере ниже JavaScript получает документ внутри ифрейма и модифицирует его: + +```html + + + + +``` + +[smart] +Обратите внимание, HTML `'тест'` в примере выше -- невалидный, там нет `BODY`. Поэтому ифрейм будет отображаться в режиме совместимости. + +Но браузер исправляет структуру и гарантирует, что после загрузки документа у него всегда есть `document.body` и ровно одно. +[/smart] + +В целях безопасности возможность доступа к документу в ифрейме ограничена. Если он с другого домена, на другом порту или протоколе, то доступ запрещён. Подробнее об этом ограничении и как его можно обойти -- далее, в главе [](/same-origin-policy). + +## Иерархия window.frames + +Альтернативный способ доступа к окну ифрейма -- это получить его из коллекции `window.frames`. + +Есть два способа доступа: +
                        +
                      1. `window.frames[0]` -- доступ по номеру.
                      2. +
                      3. `window.frames.iframeName` -- доступ по `name` ифрейма.
                      4. +
                      + +В коллекции хранится именно окно (`contentWindow`), а не тег. + +Например: + +```html + + + + +``` + +Внутри ифрейма могут быть свои вложенные ифреймы. Всё это вместе образует иерархию. + +Ссылки для навигации по ней: + +
                        +
                      • **По детям:** `window.frames` -- коллекция "детей" (вложенных ифреймов)
                      • +
                      • **На родителя:** `window.parent` -- содержит ссылку на родительское окно, позволяет обратиться к нему из ифрейма. + +Всегда верно: + +```js +// (из окна со фреймом) +window.frames[0].parent === window; // true +``` + +
                      • +
                      • **На корень:** `window.top` -- содержит ссылку на самое верхнее окно. + +Всегда верно: + +```js +// (в предположении, что вложенные фреймы существуют) +window.frames[0].frames[0].frames[0].top === window +``` + +
                      • +
                      + +**Свойство `top` позволяет легко проверить, во фрейме ли находится текущий документ:** + +```js +//+ run +if (window == top) { + alert('Этот скрипт является окном верхнего уровня в браузере'); +} else { + alert('Этот скрипт исполняется во фрейме!'); +} +``` + +## Событие onload + +У ифрейма есть своё событие `onload`, которое не связано с `onload` основного окна. + +Иными словами, `onload` основного окна не ждёт, пока ифрейм догрузится. + +**Событие `onload` есть и на теге `iframe` и на его окне.** + +Это важно, так как обработчик на теге более универсален. + +**В случае, когда документ с другого домена, внешний документ сможет отследить его загрузку только по `onload` на теге.** + +Например, попытаемся отследить загрузку ифрейма с сайта `http://vk.com/`: + +```html + + + + +``` + +Запустите пример выше. Вы увидите, что работает только `iframe.onload`. А если бы ифрейм был с того же домена, то сработали бы оба обработчика. + + +## Атрибуты seamless и sandbox + +В стандарте HTML5 для [iframe](http://www.w3.org/wiki/HTML/Elements/iframe) предусмотрены атрибуты `seamless` и `sandbox`. + +Оба они на момент написания (конец 2012) поддерживаются в Chrome, в других браузерах поддержка тоже не за горами. + +### seamless + +Атрибут `seamless` полностью интегрирует ифрейм в документ, убирая рамку и применяя CSS-стили внешнего окна к содержимому ифрейма, как будто это обычный элемент. + +То есть, если в основном документе есть стиль `p { font-weight: bold }`, то он повлияет и на содержимое ифрейма. + +Для JavaScript он не важен. + +### sandbox + +Атрибут `sandbox` позволяет построить "песочницу" вокруг ифрейма, запретив ему выполнять ряд действий. + +Наличие атрибута `sandbox`: +
                        +
                      • Заставляет браузер считать ифрейм загруженным с другого домена, так что он и внешнее окно больше не могут обращаться к переменным друг друга.
                      • +
                      • Отключает формы и скрипты в ифрейме.
                      • +
                      • Запрещает менять `parent.location` из ифрейма.
                      • +
                      + +Пример ниже загружает в такой ифрейм документ: + +```html + + +``` + +Содержимое файла `sandboxed.html`: + +```html + + +
                      + +
                      +``` + +Запустите пример выше для просмотра. Ни форма ни скрипты не сработают. + +**Атрибут `sandbox` может содержать флаги через пробел, которые убирают ограничения:** +
                      +
                      allow-same-origin
                      +
                      Браузер может не считать документ в ифрейме пришедшим с другого же домена. Если ифрейм *и так* с другого домена, то ничего не меняется.
                      +
                      allow-top-navigation
                      +
                      Разрешает ифрейму менять `parent.location`.
                      +
                      allow-forms
                      +
                      Разрешает отправлять формы из `iframe`.
                      +
                      allow-scripts
                      +
                      Разрешает выполнение скриптов из ифрейма. Но скриптам, всё же, будет запрещено открывать попапы.
                      +
                      + +Цель атрибута `sandbox` -- наложить дополнительные ограничения. Он не может снять уже существующие, в частности, убрать ограничения безопасности, если ифрейм с другого домена. \ No newline at end of file diff --git a/3-more/7-frames-and-windows/5-same-origin-policy/article.md b/3-more/7-frames-and-windows/5-same-origin-policy/article.md new file mode 100644 index 00000000..7cb92d97 --- /dev/null +++ b/3-more/7-frames-and-windows/5-same-origin-policy/article.md @@ -0,0 +1,163 @@ +# Ограничение "Same Origin" + +Ограничение "Same Origin" ("тот же источник") ограничивает доступ окон и фреймов друг к другу, а также влияет на AJAX-запросы к серверу. + +Причина, по которой оно существует -- безопасность. Если есть два окна, в одном из которых `vasya-pupkin.com`, а в другом `gmail.com`, то мы бы не хотели, чтобы скрипт из первого мог читать нашу почту. + +Сама концепция проста, но есть много важных исключений и особенностей, которые нужно знать для полного понимания этого правила. +[cut] + +## Same Origin [[#same-origin] + +[summary] +Два URL считаются имеющим один источник ("same origin"), если у них одинаковый протокол, домен и порт. +[/summary] + +Эти URL имеют один источник: +
                        +
                      • `http://site.com`
                      • +
                      • `http://site.com`/
                      • +
                      • `http://site.com/my/page.html`
                      • +
                      + +А вот эти -- все из других источников: +
                        +
                      • http://www.site.com (другой домен)
                      • +
                      • http://site.org (другой домен)
                      • +
                      • https://site.com (другой протокол)
                      • +
                      • http://site.com:8080 (другой порт)
                      • +
                      + +Существует ряд исключений, позволяющих-таки окнам с разных доменов обмениваться информацией, но прямой вызов методов друг друга и чтение свойств запрещены. + +## Пример ограничений + +Если одно окно попытается обратиться к другому, то браузер проверит, из одного ли они источника. Если нет -- доступ будет запрещён. + +Например: + +```html + + + + +``` + +**При запуске примера выше Safari/Chrome могут вместо ошибки вывести `undefined`.** + +Это их способ (некорректный) показать, что "чтение запрещено". Будем надеяться, исправят. + + + +## Исключение: запись в location + +Окно и вложенный в него `iframe` могут менять `location` друг друга, даже если они из разных источников. + +Причём *читать* `location` нельзя, одно окно не имеет право знать, на каком URL пользователь в другом. А вот *запись* браузеры считают безопасной. + +Например, открыв на `javascript.ru` ифрейм с `vk.com`, из этого ифрейма нельзя будет узнать URL, а вот поменять его -- запросто: + +```html + + + + +``` + +## Исключение: поддомен 3го уровня + +Ещё одно важное исключение касается доменов третьего уровня. + +Например, у нас есть окно на `http://site.com` и два ифрейма: первый с источника `http://john.site.com`, а второй -- с `http://peter.site.com`. + +**Если несколько окон присваивают в `document.domain` свой общий поддомен 2го уровня, то все ограничения снимаются.** + +Важно: +
                        +
                      1. Должен быть общий поддомен второго уровня. + +Можно поставить `document.domain='site.com'` на странице с `my.site.com`, но нельзя это сделать на странице с `vaysa-pupkin.ru`.
                      2. +
                      3. Свойство `document.domain` должно быть присвоено на всех окнах, участвующих в коммуникации, в том числе на том, которое и так с этого домена. + +Выглядит абсурдно, но на документе с `site.com` нужно вызвать: `document.domain=document.domain`. Тогда будет работать.
                      4. +
                      + + +## Исключение: порт в IE + +В браузере Internet Explorer порт не входит в понятие "источник" (origin). + +Это означает, что окно с `http://site.com` может свободно общаться с `http://site.com:8080`. + +Это иногда используют для общения разных сервисов, использующих один IP-адрес. Но допустимо такое только в IE. + +## Исключение: зона в IE + +Если сайт находится в зоне "Надёжные узлы", то в Internet Explorer ограничения к нему не применяются. + +При этом подразумевается, что для этой зоны в параметрах "Безопасность" включена опция "Доступ к источникам данных за пределами домена". + +## Общение между окнами с разных источников + +Если ни одно из исключений выше не подошло, то есть три основных способа, как окнам общаться между собой: + +
                        +
                      1. Все современные браузеры, включая IE8+, поддерживают специальный интерфейс `postMessage` для общения между окнами с разных доменов. Мы рассмотрим его в отдельной главе [](/cross-window-messaging-with-postmessage).
                      2. +
                      3. Одно окно может поменять другому `location.hash` -- часть пути после `#`. При этом не произойдёт смены `URL`, но другое окно, увидев это, может прочитать из хэша информацию и, в свою очередь, ответить. + +Этот способ испольуется там, где требуется поддержка IE<8. +
                      4. +
                      + +В совокупности с этим можно использовать интерфейс `localStorage` для оповещении всех окон и вкладок о событии. При сохранении данных в `localStorage` генерируется событие `onstorage`, причём сразу на всех окнах, фреймах и табах с тем же доменом. + +Кроме того, если вы используете `IFRAME` вместе с AJAX, то, возможно, вам пригодятся способы, описанные в главе [](/ajax-iframe-xdomain). + + + +## Итого + +Ограничение "одного источника" запрещает окнам и фреймам с разных источников вызывать методы друг друга и читать данные друг из друга. + +При этом "из одного источника" означает "совпадают протокол, домен и порт". + +Как ни странно, у этого подхода ряд существенных исключений: + +
                        +
                      • Свойство `window.location` нельзя читать, но можно менять.
                      • +
                      • Домены третьего уровня с общим наддоменом могут поменять `document.domain` на их общий домен второго уровня, и тогда они смогут взаимодействовать без ограничений. +
                      • +
                      • IE не включает порт в понятие источника. Кроме того, он позволяет снять ограничения для конкретного сайта включением в доверенную зону.
                      • +
                      + +В современных браузерах (IE8+) кросс-доменное общение можно организовать через `location.hash` и [postMessage](/cross-window-messaging-with-postmessage). При этом IE8 не умеет передавать данные между окнами, но, если нужно, `localStorage` передаёт всё и везде :) \ No newline at end of file diff --git a/3-more/7-frames-and-windows/6-cross-window-messaging-with-postmessage/article.md b/3-more/7-frames-and-windows/6-cross-window-messaging-with-postmessage/article.md new file mode 100644 index 00000000..382032e4 --- /dev/null +++ b/3-more/7-frames-and-windows/6-cross-window-messaging-with-postmessage/article.md @@ -0,0 +1,106 @@ +# Общение окон с разных доменов: postMessage + +Интерфейс `postMessage` позволяет общаться друг с другом окнам и ифреймам с разных доменов. + +Он очень удобен, например, для взаимодействия внешних виджетов и сервисов, подключённых через ифрейм с основной страницей. +[cut] + +## Интерфейс + +Интерфейс состоит из двух частей. + +### Отправитель: метод postMessage + +Первая часть состоит из метода [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage). Его вызывает окно, которое хочет отправить сообщение, в контексте окна-получателя. + +Проще говоря, если мы хотим отправить сообщение в окно `win`, то нужно вызвать `win.postMessage(data, targetOrigin)`. + +Аргументы: + +
                      +
                      data
                      +
                      Данные. По спецификации, это может быть любой объект, который будет *клонирован с сохранением структуры* при передаче. + +Но IE поддерживает только строки, поэтому обычно данные JSON-сериализуют.
                      +
                      targetOrigin
                      +
                      Разрешить получение сообщения только окнам с данного источника. + +Мы ведь не можем из JavaScript узнать, на каком именно URL находится другое окно. Но иногда хочется быть уверенным, что данные передаются в доверенный документ. Для этого и нужен этот параметр. Проверку осуществляет браузер. При указании `'*'` ограничений нет.
                      +
                      + +[warn header="В IE8 можно использовать `postMessage` только для ифреймов"] + +В браузере IE8, интерфейс `postMessage` работает только с ифреймами. Он не работает между табами и окнами. + +Это ошибка в данном конкретном браузере, в других -- всё в порядке. +[/warn] + +### Получатель: событие onmessage + +Чтобы получить сообщение, окно должно поставить обработчик на событие `onmessage`. + +Свойства объекта события: +
                      +
                      `data`
                      +
                      Присланные данные
                      +
                      `origin`
                      +
                      Источник, из которого пришло сообщение, например `http://javascript.ru`.
                      +
                      `source`
                      +
                      Ссылка на окно, с которого пришло сообщение. Можно тут же ответить.
                      +
                      + +Назначать обработчик нужно обязательно через методы `addEventListener/attachEvent`, например: + +```js +function listener(event) { + if( event.origin != 'http://learn.javascript.ru') { + // что-то прислали с чужого домена - проигнорируем.. + return; + } + + document.getElementById("msg").innerHTML = "получено: " + event.data; +} + +if (window.addEventListener){ + window.addEventListener("message", listener, false); +} else { + window.attachEvent("onmessage", listener); +} +``` + +Этот код содержит ифрейм ниже, с именем `name="receiveFrame"`: + + + +Запустите код для отправки ему сообщения: + +```js +//+ run +var win = frames.receiveFrame; +win.postMessage("Привет!", "http://learn.javascript.ru"); +``` + +## Задержка + +Задержки между отправкой и получением нет, совсем. + +Если для `setTimeout` стандарт предусматривает минимальную задержку 4мс, то для `postMessage` она равна 0мс. + +Поэтому `postMessage` можно, в том числе, использовать как мгновенную альтернативу `setTimeout`. + +## Итого + +Интерфейс `postMessage` позволяет общаться окнам и ифреймам с разных доменов (в IE8 -- только ифреймы), при этом обеспечивая проверки безопасности. + +
                        +
                      1. Отправитель вызывает `targetWin.postMessage(data, targetOrigin)`.
                      2. +
                      3. Если `targetOrigin` не `'*'`, то браузер проверяет, совпадает ли источник с `targetWin`.
                      4. +
                      5. Если совпадает, то на `targetWin` генерируется событие `onmessage`, в котором передаются: +
                          +
                        • `origin` -- источник, с которого пришло сообщение.
                        • +
                        • `source` -- ссылка на окно-отправитель.
                        • +
                        • `data` -- данные. Везде, кроме IE, допустимы объекты, которые клонируются, а в IE -- только строка.
                        • +
                        +
                      6. +
                      7. Обработчик на `onmessage` необходимо вешать при помощи специализированных методов `addEventListener/attachEvent`.
                      8. +
                      diff --git a/3-more/7-frames-and-windows/7-clickjacking/article.md b/3-more/7-frames-and-windows/7-clickjacking/article.md new file mode 100644 index 00000000..e0a78c18 --- /dev/null +++ b/3-more/7-frames-and-windows/7-clickjacking/article.md @@ -0,0 +1,250 @@ +# Атака Clickjacking и защита от неё + +Атака "кликджекинг" (англ. Clickjacking) позволяет хакеру выполнить клик на сайте-жертве *от имени посетителя*. + +В русском языке встречается дословный перевод термина clickjacking: "угон клика". Так же применительно к clickjacking-атаке можно встретить термины "перекрытие iframe" и "подмена пользовательского интерфейса". + +Кликджекингу подверглись в своё время Twitter, Facebook , PayPal, YouTube и многие другие сайты. Сейчас, конечно, они уже защищены. +[cut] +## Идея атаки + +В целом идея очень проста. + +Вот как выглядит «угон клика» пользователя, который зарегистрирован на facebook: + +
                        +
                      1. На вредоносной странице пользователю подсовывается безобидная ссылка +(скажем, что-то скачать, «разбогатеть сейчас», посмотреть ролик или просто перейти по ссылке на интересный ресурс).
                      2. +
                      3. Поверх этой заманчивой ссылки помещен прозрачный iframe со страницей facebook.com, так что кнопка "Like" находится чётко над ней. + +Кликая на ссылку, посетитель на самом деле нажимает на эту кнопку. +
                      4. +
                      + +## Пример + +Вот пример (для наглядности `iframe` -- полупрозрачный): + +```html + + + +
                      Нажмите, чтобы разбогатеть сейчас:
                      + + + + +Нажми тут! + +
                      ..И всё получится (хе-хе, у меня, злого хакера, получится)!
                      +``` + +**При клике на ссылку на самом деле происходит клик на iframe (на «Like»).** + +Если посетитель авторизован на facebook (а в большинстве случаев так и есть), то facebook.com получает щелчок от имени посетителя. + +На Twitter это была бы кнопка «Follow». + +Тот же самый код, но как это было бы в реальности, с `opacity:0` для iframe (щелкните, чтобы увидеть захваченную кнопку): + +[iframe src="clickjacking" height=120 link edit] + +Итак, все, что нужно для проведения атаки -- это правильно расположить iframe на вредоносной странице. В большинстве случаев это делается средствами HTML/CSS. + +События клавиатуры захватить гораздо труднее, потому что, если `iframe` является невидимым, то текст в поле ввода также будет невидимым. Посетитель начнёт печатать, но, не увидев текст, прекратит свои действия. + +## Защита и способы обхода. + +Самый старый метод защиты -- это код JavaScript, не позволяющий отобразить веб-страницу внутри фрейма (*framebusting*, также его называют *framekilling* и *framebreaking*). + +Добавьте к документу следующий код, если не хотите, чтобы документ загружался в iframe: + + + +То есть, если окно обнаруживает, что оно загружено во фрейме, то оно автоматически делает себя верхним. + +В настоящий момент это уже не является сколько-нибудь надежной защитой. + +Есть несколько способов обхода framebusting. Давайте рассмотрим некоторые из них. + +### Блокировка top-навигации. + +Можно заблокировать переход, инициированный сменой `top.location`, в событии [onbeforeunload](#window.onbeforeunload). + +Обработчик этого события ставится на внешней (хакерской) странице и, при попытке `iframe` поменять `top.location`, спросит посетителя, хочет он покинуть данную страницу. В большинстве браузеров хакер может спросить посетителя, используя своё сообщение. + +Так что, скорее всего, посетитель ответит на такой странный вопрос отрицательно (он же не знает про ифрейм, видит только страницу, причины для ухода нет). + +В приведенном ниже примере представлен "защищенный" `iframe`: + +```html + +``` + +А вот -- хакерская страница с этим `iframe`, которая отменяет перезагрузку верхнего окна при помощи `onbeforeunload` (предполагается, что посетитель нажмёт «отмену»): + +```html + + + +``` + +[Открыть в отдельном окне (сразу спросит)](/files/tutorial/window/cj_location_hack.html). + +### security = "restricted" + +В IE8 есть особый атрибут `security = "restricted"` -- отключающий исполнение скриптов во фрейме. + +Пример использования: + +```html + +``` + +При этом клик во фрейме, если он не требует JavaScript, всё равно сработает, а вот защита -- нет. + +### HTML5 + +Одна из возможностей HTML5 -- атрибут [sandbox](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-iframe-element.html#attr-iframe-sandbox) + +Он позволяет разрешить во фрейме скрипты `allow-scripts` и формы `allow-forms`, но запретить top-навигацию (не указать `allow-top-navigation`). + +Выглядеть это будет так: + +```html + +``` + +Сработает в браузерах, которые поддерживают `sandbox`, например -- в Chrome. + +### Ещё приёмы + +Есть и еще приёмы для обхода этой простейшей защиты. + +Firefox и старый IE могут активировать designMode на исходной странице, это также предотвращает framebusting (спасибо за идею owasp.org, страница clickjacking). + +Как мы видим, она не только не выдерживает реальной атаки, но и может скомпрометировать сайт (программист-то думает, что защитил его). + +## Надежная защита от Clickjacking + +### Заголовок X-Frame-Options + +Все современные браузеры поддерживают заголовок `X-Frame-Options`. + +Он разрешает или запрещает отображение страницы, если она открыта во фрейме. + +Браузеры игнорируют заголовок, если он определен в МЕТА тег. Таким образом, `` будет проигнорирован. + +У заголовка может быть три значения: + +
                      +
                      SAMEORIGIN
                      +
                      Рендеринг документа, при открытии во фрейме, производится только в том случае, когда верхний (top) документ -- с того же домена.
                      +
                      DENY
                      +
                      Рендеринг документа внутри фрейма запрещён.
                      +
                      ALLOW-FROM domain
                      +
                      Разрешает рендеринг, если внешний документ с данного домена (не поддерживается в Safari, Firefox).
                      +
                      + +К примеру, Twitter использует `X-Frame-Options: SAMEORIGIN`. Результат: + +```html + +``` + + + +В зависимости от браузера, `iframe` выше либо пустой, либо в нём находится сообщение о невозможности отобразить его (IE). + +### Приостановка показа документа + +Если нужно поддерживать старые браузеры (IE<8), то можно и просто отменить показ документа: + +```html + + + + + + +``` + +[warn header="Почему не `document.body`?"] +В приведенном выше примере, мы используем `document.getElementsByTagName`, вместо `document.body`, потому что это способ получения `BODY` работает во всех браузерах, когда документ еще не готов. +[/warn] + +Обратите внимание -- изначально документ скрыт. Мало ли, вдруг JavaScript во фрейме отключен -- защита сработает. + +Он будет показан только в том случае, если заведомо всё в порядке: не отключён JavaScript и страница не во фрейме. + +### Показ с отключённым функционалом + +Заголовок `X-Frame-Options` имеет неприятный побочный эффект. Иногда поисковики, анонимайзеры или другие сайты хотели бы отобразить страницу в `iframe`, по вполне "легальным" причинам, но не могут. + +Хорошо бы показывать их посетителям не пустой `iframe`, а нечто, что может быть более интересно. + +Например, можно изначально "накрывать" документ `div` с `height:100%;width:100%`, который будет перехватывать все клики. И поставить на нём ссылку, ведующую на страницу в новом окне. + +```html + + + + + +``` + +Если страница -- не во фрейме или домен совпадает, то посетитель не увидит его. + +## Заключение + +Атаку "Clickjacking" легко осуществить, если на сайте есть действие, активируемое с помощью одного клика. + +Злоумышленник может осуществить атаку через целенаправленно на посетителей ресурса -- опубликовав ссылку на форуме, или «счастливой рассылкой». Существует масса вариантов. + +
                        +
                      • Рекомендуется использовать X-Frame-Options на страницах, заведомо не предназначеных для запуска во фрейме и на важнейших страницах (финансовые транзакции).
                      • +
                      • Используйте защиту через framebusting для защиты IE<8, т.к. там не поддерживается X-Frame-Options.
                      • +
                      • Используйте перекрывающий DIV, если это допустимо вашим проектом и вы хотите разрешить безопасный показ документа во фреймах с любых доменов.
                      • +
                      + +
                      При участии Марата Шагиева
                      \ No newline at end of file diff --git a/3-more/7-frames-and-windows/7-clickjacking/cj_location.html b/3-more/7-frames-and-windows/7-clickjacking/cj_location.html new file mode 100755 index 00000000..1c2ba89a --- /dev/null +++ b/3-more/7-frames-and-windows/7-clickjacking/cj_location.html @@ -0,0 +1,14 @@ + + + + +
                      Меняет top.location на google.com
                      + + + + + + + diff --git a/3-more/7-frames-and-windows/7-clickjacking/clickjacking.view/index.html b/3-more/7-frames-and-windows/7-clickjacking/clickjacking.view/index.html new file mode 100755 index 00000000..430149de --- /dev/null +++ b/3-more/7-frames-and-windows/7-clickjacking/clickjacking.view/index.html @@ -0,0 +1,25 @@ + + + + + +
                      Нажмите, чтобы разбогатеть сейчас:
                      + + + +Нажми тут! + +
                      ..И ваша жизнь удалась!
                      + + + diff --git a/3-more/7-frames-and-windows/index.md b/3-more/7-frames-and-windows/index.md new file mode 100644 index 00000000..680aa953 --- /dev/null +++ b/3-more/7-frames-and-windows/index.md @@ -0,0 +1,2 @@ +# Окна и Фреймы + diff --git a/3-more/8-regular-expressions-javascript/1-regexp-introduction/article.md b/3-more/8-regular-expressions-javascript/1-regexp-introduction/article.md new file mode 100644 index 00000000..28a288b5 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/1-regexp-introduction/article.md @@ -0,0 +1,104 @@ +# Паттерны и флаги + +Регулярные выражения –- мощное средство поиска и замены в строке. + +В JavaScript регулярные выражения реализованы отдельным объектом `RegExp` и интегрированы в методы строк. +[cut] + +## Регэкспы + +Регулярное выражение (оно же "регэксп", "регулярка" или просто "рег"), состоит из *паттерна* (он же "шаблон") и необязательных *флагов*. + +Синтаксис создания регулярного выражения: + +```js +var regexp = new RegExp("шаблон", "флаги"); +``` + +Как правило, используют более короткую запись через слеши `/`: + +```js +var regexp = /шаблон/; // без флагов +var regexp = /шаблон/gmi; // с флагами gmi (изучим их дальше) +``` + +Слэши `'/'` говорят JavaScript о том, что это регулярное выражение. Они играют здесь ту же роль, что и кавычки для обозначения строк. + +## Использование + +Основа регулярного выражения -- паттерн. Это строка, которую можно расширить специальными символами, которые делают поиск намного мощнее. + +Если флагов и специальных символов нет, то поиск по паттерну -- то же самое, что и обычный поиск подстроки: + +```js +//+ run +var str = "Я люблю JavaScript!"; // будем искать в этой строке + +var regexp = /лю/; +alert( str.search(regexp) ); // 2 +``` + +Сравните с обычным поиском: + +```js +//+ run +var str = "Я люблю JavaScript!"; + +var substr = "лю"; +alert( str.indexOf(substr) ); // 2 +``` + +Как видим, то же самое, разве что для регэкспа использован метод [:String#search(reg)], а для строки [:String#indexOf(substr)]. Но это соответствие лишь кажущееся. Очень скоро мы усложним регулярные выражения, и тогда появятся отличия. + +[smart header="Цветовые обозначения"] +Здесь и далее используется следующая цветовая схема: +
                        +
                      • регэксп (регулярное выражение) - красный
                      • +
                      • строка - синий
                      • +
                      • результат - зеленый
                      • +
                      +[/smart] + +## Флаги + +Регулярные выражения могут иметь флаги, которые влияют на поиск. + +В JavaScript их всего три: + +
                      +
                      `i`
                      +
                      Если этот флаг есть, то регэксп ищет независимо от регистра, то есть не различает между `А` и `а`.
                      +
                      `g`
                      +
                      Если этот флаг есть, то регэксп ищет все совпадения, иначе -- только первое.
                      +
                      `m`
                      +
                      Многострочный режим.
                      +
                      + +Самый очевидный из этих флагов -- безусловно, `i`. + +Пример его использования: + +```js +//+ run +var str = "Я люблю JavaScript!"; // будем искать в этой строке + +alert( str.search( *!*/ЛЮ/*/!* ) ); // -1 +alert( str.search( *!*/ЛЮ/i*/!* ) ); // 2 +``` + +
                        +
                      1. С регом `/ЛЮ/` вызов вернул `-1`, что означает "не найдено" (то же соглашение, что и `indexOf`),
                      2. +
                      3. С регом `/ЛЮ/i` вызов нашёл совпадение на позиции 2, так как стоит флаг `i`, а значит "лю" тоже подходит.
                      4. +
                      + +Следующий, пожалуй, самый важный флаг -- это `g`. + +Мы рассмотрим его в следующей секции, вместе с основными методами поиска по регулярным выражениям в JavaScript. + +## Итого + +
                        +
                      • Регулярное выражение состоит из шаблона и необязательных флагов `g`, `i` и `m`.
                      • +
                      • Поиск по регулярному выражению /регэксп/ без флагов и спец. символов, которые мы изучим далее -- это то же самое, что и обычный поиск подстроки в строке.
                      • +
                      • Метод строки `str.search(regexp)` возвращает индекс, на котором найдено совпадение.
                      • +
                      diff --git a/3-more/8-regular-expressions-javascript/10-regexp-backreferences/article.md b/3-more/8-regular-expressions-javascript/10-regexp-backreferences/article.md new file mode 100644 index 00000000..f2f69286 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/10-regexp-backreferences/article.md @@ -0,0 +1,52 @@ +# Обратные ссылки \\n и $n + +На скобочные группы можно ссылаться как в самом паттерне, так и в строке замены. +[cut] + +Ссылки в строке замены мы уже видели: они имеют вид `$n`, где `n` -- это номер скобочной группы. Вместо `$n` подставляется содержимое соответствующей скобки: + +```js +//+ run +var name = "Александр Пушкин"; + +name = name.replace(/([а-яё]+) ([а-яё]+)/i, "$2, $1"); +alert(name); // Пушкин, Александр +``` + +К скобочной группе можно также обратиться в самом шаблоне. + +Рассмотрим это в реальном примере -- необходимо найти строку в кавычках. Эта строка может быть в одинарных кавычках '...' или в двойных "..." -- не важно, в каких именно, но открывающая и закрывающая кавычки должны быть одинаковыми. + +Как такие строки искать? Регэксп `['"](.*?)['"]` позволяет использовать разные кавычки, но он даст неверный ответ в случае, если одна кавычка ненароком оказалась внутри другой, как например в строке "She's the one": + +```js +//+ run +str = "He said:\"She's the one\"." + +reg = /['"](.*?)['"]/g + +alert( str.match(reg) ) // "She' +``` + +Как видно, регэксп нашёл открывающую кавычку ", затем текст, вплоть до новой кавычки ', которая закрывает соответствие. + +Для того, чтобы попросить регэксп искать закрывающую кавычку -- такую же, как открывающую, мы обернём её в скобочную группу и используем обратную ссылку на неё: + +```js +//+ run +str = "He said:\"She's the one\"." + +reg = /(['"])(.*?)\1/g + +alert( str.match(reg) ) // "She's the one" +``` + +Теперь работает верно! + +Обратим внимание на два нюанса: + +
                        +
                      • В строке замены ссылка на первую скобочную группу выглядит как `$1`, а в шаблоне нужно использовать `\1`.
                      • +
                      • Чтобы обращаться к скобочной группе -- не важно откуда, она не должна быть исключена из запоминаемых при помощи `?:`, то есть `(?:['"])` не подошло бы.
                      • +
                      + diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/article.md b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/article.md new file mode 100644 index 00000000..a036a6c6 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/article.md @@ -0,0 +1,82 @@ +# Чёрная дыра бэктрекинга + +Некоторые регулярные выражения, с виду являясь простыми, могут выполняться оооочень долго, и даже подвешивать браузер. + +[cut] +Например, попробуйте пример ниже в Chrome или IE (осторожно, подвесит браузер!): + +```js +//+ run +alert( '123456789012345678901234567890z'.match(/(\d+)*$/) ); +``` + +Некоторые движки регулярных выражений (Firefox) справляются с таким регэкспом, а некоторые (IE, Chrome) -- нет. + +В чём же дело, что не так с регэкспом? + +Да с регэкспом-то всё так, синтаксис вполне допустимый. Проблема в том, как выполняется поиск по нему. + +Для краткости рассмотрим более короткую строку: 1234567890z: + +
                        +
                      1. Первым делом, движок регэкспов пытается найти \d+. Плюс + является жадным по умолчанию, так что он хватает все цифры, какие может. + +Затем движок пытается применить звёздочку вокруг скобок (\d+)*, но больше цифр нет, так что звёздочка не даёт повторений. + +После этого в паттерне остаётся $, а в тексте -- символ z. + + + +Так как соответствия нет, то жадный плюс + отступает на один символ (бэктрекинг, зелёная стрелка на рисунке выше). +
                      2. +
                      3. После бэктрекинга, \d+ содержит всё число, кроме последней цифры. Затем движок снова пытается найти совпадение, уже с новой позиции (`9`). + +Звёздочка (\d+)* теперь может быть применена -- она даёт ещё одно число 9: + + + +Движок пытается найти `$`, но это ему не удаётся -- на его пути опять `z`: + + + +Так как совпадения нет, то поисковой движок отступает назад ещё раз. +
                      4. +
                      5. Теперь первое число \d+ будет содержать 8 цифр, а остаток строки 90 становится вторым \d+: + + + +Увы, всё ещё нет соответствия для $. + +Поисковой движок снова должен отступить назад. При этом последний жадный квантификатор отпускает символ. В данном случае это означает, что укорачивается второй \d+, до одного символа 9. +
                      6. +
                      7. Теперь движок регулярных выражений снова может применить звёздочку и находит третье число \d+: + + + +...И снова неудача. Второе и третье \d+ отступили по-максимуму, так что сокращается снова первое число. +
                      8. +
                      9. Теперь есть 7 цифр в первом \d+. Поисковой движок видит место для второго \d+, теперь уже с позиции 8: + + + +Так как совпадения нет, второй \d+ отступает назад.... +
                      10. +
                      11. ...И так далее, легко видеть, что поисковой движок будет перебирать *все возможные комбинации* \d+ в числе. А их много.
                      12. +
                      + +На этом месте умный читатель может воскликнуть: "Бэктрекинг? Давайте включим ленивый режим -- и не будет никакого бэктрекинга!" + +Что ж, заменим \d+ на \d+? и посмотрим (аккуратно, может подвесить браузер): + +```js +//+ run +alert( '123456789012345678901234567890z'.match(/(\d+?)*$/) ); +``` + +Не помогло! + +**Ленивые регулярные выражения делают то же самое, но в обратном порядке.** + +Просто подумайте о том, как будет в этом случае работать поисковой движок. + +Некоторые движки регулярных выражений, например Firefox, содержат хитрые проверки, в дополнение к алгоритму выше, которые позволяют избежать бесконечного перебора или кардинально ускорить его, но все движки и не всегда. diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy1.png b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy1.png new file mode 100755 index 00000000..5bebdcf1 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy1.png differ diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy11.png b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy11.png new file mode 100755 index 00000000..3ebb6196 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy11.png differ diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy2.png b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy2.png new file mode 100755 index 00000000..6dccbe98 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy2.png differ diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy3.png b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy3.png new file mode 100755 index 00000000..170cdebf Binary files /dev/null and b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy3.png differ diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy4.png b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy4.png new file mode 100755 index 00000000..b64e4952 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy4.png differ diff --git a/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy5.png b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy5.png new file mode 100755 index 00000000..5aa6792d Binary files /dev/null and b/3-more/8-regular-expressions-javascript/11-regexp-infinite-backtracking-problem/bad_backtrack_greedy5.png differ diff --git a/3-more/8-regular-expressions-javascript/12-regexp-alternation/article.md b/3-more/8-regular-expressions-javascript/12-regexp-alternation/article.md new file mode 100644 index 00000000..b9a4cb55 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/12-regexp-alternation/article.md @@ -0,0 +1,26 @@ +# Альтернация (или) | + +Альтернация -- термин в регулярных выражениях, которому в русском языке соответствует слово "ИЛИ". Она обозначается символом вертикальной черты | и позволяет выбирать между вариантами. + +[cut] + +Например, нам нужно найти языки программирования: HTML, PHP, Java и JavaScript. + +Соответствующее регулярное выражение: /html|php/java(script)?/: + +```js +//+ run +var reg = /html|php|css|java(script)?/gi + +var str = "Сначала появился HTML, затем CSS, потом JavaScript" + +alert( str.match(reg) ) // 'HTML', 'CSS', 'JavaScript' +``` + +**Альтернация имеет очень низкий приоритет.** + +Чтобы регэксп находил одновременно gray и grey, можно использовать gr(a|e)y или gr[ae]y, но не gra|ey. Последний регэксп применит альтернацию к подвыражениям: gra (ИЛИ) ey. + + + + diff --git a/3-more/8-regular-expressions-javascript/13-regexp-ahchors-and-multiline-mode/article.md b/3-more/8-regular-expressions-javascript/13-regexp-ahchors-and-multiline-mode/article.md new file mode 100644 index 00000000..736de5d2 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/13-regexp-ahchors-and-multiline-mode/article.md @@ -0,0 +1,62 @@ +# Начало ^ и конец $ строки, многострочный режим + +Знак каретки '^' и доллара '$' имеют в регулярном выражении особый смысл. Их называют "якорями" (anchor - англ.). +[cut] + +Каретка ^ совпадает в начале текста, а доллар $ -- в конце. + +**Якоря являются не символами, а проверками. Они совпадают на "позиции".** + +Это очень важное отличие по сравнению с символьными классами. Если движок регулярных выражений видит ^ -- он *проверяет* начало текста, $ -- проверяет конец текста, при этом *никаких символов к совпадению не добавляется*. + +Каретку ^ обычно используют, чтобы указать, что регулярное выражение необходимо проверить именно с начала текста. + +Например, без каретки: + +```js +//+ run +var str = '100500 попугаев съели 500100 бананов!'; +alert( str.match( /\d+/ig ) // 100500, 500100 (все числа) +``` + +А с кареткой: + +```js +//+ run +var str = '100500 попугаев съели 500100 бананов!'; +alert( str.match( /^\d+/ig ) // 100500 (только в начале строки)*!* +``` + +Знак доллара $ используют, чтобы указать, что паттерн должен заканчиваться в конце текста. + +Тот же пример с долларом: + +```js +//+ run +var str = '100500 попугаев съели 500100 бананов!'; +alert( str.match( /\d+$/ig ) // null (в начале строки чисел нет)*!* +``` + +Якоря используют одновременно, чтобы указать, что паттерн должен охватывать текст с начала и до конца. Обычно это требуется при валидации. + +Например, мы хотим проверить, что в переменной `num` хранится именно десятичная дробь. + +Ей соответствует регэксп \d+\.\d+. Но простая проверка найдёт дробь в любом тексте: + +```js +//+ run +var num = "ля-ля 12.34"; +alert( num.match( /\d+\.\d+/ig) ); // 12.34 +``` + +Если мы хотим проверить, что `num` *целиком* соответствует паттерну \d+\.\d+, то укажем якоря по обе стороны от него: + +```js +//+ run +var num = "ля-ля 12.34"; +alert( num.match( /^\d+\.\d+$/ig) ); // null, не дробь + +var num = "12.34"; +alert( num.match( /^\d+\.\d+$/ig) ); // 12.34, дробь! +``` + diff --git a/3-more/8-regular-expressions-javascript/14-regexp-multiline-mode/article.md b/3-more/8-regular-expressions-javascript/14-regexp-multiline-mode/article.md new file mode 100644 index 00000000..d2e147e8 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/14-regexp-multiline-mode/article.md @@ -0,0 +1,59 @@ +# Многострочный режим, флаг "m" + +Многострочный режим включается, если у регэкспа есть флаг /m. +[cut] + +В этом случае изменяется поведение ^ и $. + +**В многострочном режиме якоря означают не только начало/конец текста, но и начало/конец строки.** + +В примере ниже текст состоит из нескольких строк. Паттерн /^\d+/gm берёт число с начала каждой строки: + +```js +//+ run +var str = '1е место: Винни-пух\n' + + '2е место: Пятачок\n' + + '33е место: Слонопотам'; + +alert( str.match(/^\d+/gm ) ); // 1, 2, 33*!* +``` + +Обратим внимание -- без флага /m было бы только первое число: + +```js +//+ run +var str = '1е место: Винни-пух\n' + + '2е место: Пятачок\n' + + '33е место: Слонопотам'; + +alert( str.match(/^\d+/g ) ); // 1 +``` + +Это потому что в обычном режиме каретка ^ -- это только начало текста. + +Символ доллара $ ведёт себя точно так же. + +Следующий пример находит последнее слово в строке: + +TODO: указать на коренное отличие $ от \n: доллар не матчит символ, а \n матчит!!!! + +```js +//+ run +showMatch( + '1st: *!*John*!*\n' + + '2nd: *!*Mary*/!*\n' + + '33rd: *!*Peter*/!*', /\w+$/gm ) // John, Mary, Peter +``` + +Please note that $ as well as ^ doesn't add \n to the match. They only check that the position is right. + + +[summary] +
                        +
                      • The caret '^' matches the position at the the text start. *In multiline mode* it also matches after the newline symbol.
                      • +
                      • The dollar '$' matches the position at the text end. *In multiline mode* it also matches before the newline symbol.
                      • +
                      + +**For both anchors, the regexp engine only checks the position, and doesn't match a character.** +[/summary] + diff --git a/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/article.md b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/article.md new file mode 100644 index 00000000..09ddc758 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/article.md @@ -0,0 +1,69 @@ +# Word boundary + +Another position check is a *word boundary* \b. It doesn't match a character, but matches in situations when a wordly character follows a non-wordly or vice versa. A "non-wordly" may also be text start or end. +[cut] +For example, \bdog\b matches a standalone dog, not doggy or catdog: + +```js +//+ run +showMatch( "doggy catdog dog", /\bdog\b/ ) // "dog" +``` + +Here, dog matches, because the previous char is a space (non-wordly), and the next position is text end. + +Normally, \w{4} matches 4 consequent word characters. +If the word is long enough, it may match multiple times: + +```js +//+ run +showMatch( "Boombaroom", /\w{4}/g) // 'Boom', 'baro' +``` + +Appending \b causes \w{4}\b to match only at word end: + +```js +//+ run +showMatch( "Because life is awesome", /\w{4}\b/g) // 'ause', 'life', 'some' +``` + +**The word boundary \b like ^ and $ doesn't match a char. It only performs the check.** + +Let's add the check from another side, \b\w{4}\b: + +```js +//+ run +showMatch( "Because life is awesome", /\b\w{4}\b/g) // 'life' +``` + +Now there is only one result life. + +
                        +
                      1. The regexp engine matches first word boundary \b at zero position: + +
                      2. +
                      3. Then it successfully matches \w{4}, but fails to match finishing \b. + + + +So, the match at position zero fails. +
                      4. +
                      5. The search continues from position 1, and the closest \b is right after Because (position 9): + + + +Now \w{4} doesn't match, because the next character is a space. +
                      6. +
                      7. The search continues, and the closest \b is right before life at position 11. + + + +Finally, \w{4} matches and the position check \b after it is positive. We've got the result. +
                      8. +
                      9. The search continues after the match, but doesn't yield new results.
                      10. +
                      + +**The word boundary check /\b/ works only for words in latin alphabet,** because it is based on \w as "wordly" chars. Sometimes that's acceptable, but limits the application range of the feature. + +And, for completeness.. +**There is also an inverse check \B, meaning a position other than \b.** It is extremely rarely used. + diff --git a/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary1.png b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary1.png new file mode 100755 index 00000000..cc687e52 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary1.png differ diff --git a/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary2.png b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary2.png new file mode 100755 index 00000000..71570c22 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary2.png differ diff --git a/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary3.png b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary3.png new file mode 100755 index 00000000..b766ab34 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary3.png differ diff --git a/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary4.png b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary4.png new file mode 100755 index 00000000..e14352f6 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/15-regexp-word-boundary/boundary4.png differ diff --git a/3-more/8-regular-expressions-javascript/16-regexp-practice/article.md b/3-more/8-regular-expressions-javascript/16-regexp-practice/article.md new file mode 100644 index 00000000..e30a77b3 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/16-regexp-practice/article.md @@ -0,0 +1,7 @@ +# Practice + +Here you found tasks which help in understanding regexp construction principles. +[cut] + + +The tutorial is not finished. Till now, we have it up to this part... \ No newline at end of file diff --git a/3-more/8-regular-expressions-javascript/17-regexp-orphans/article.md b/3-more/8-regular-expressions-javascript/17-regexp-orphans/article.md new file mode 100644 index 00000000..5be77f5f --- /dev/null +++ b/3-more/8-regular-expressions-javascript/17-regexp-orphans/article.md @@ -0,0 +1,5 @@ +# Задачи-сироты по регекспам + + + + diff --git a/3-more/8-regular-expressions-javascript/2-regexp-methods/article.md b/3-more/8-regular-expressions-javascript/2-regexp-methods/article.md new file mode 100644 index 00000000..69c9f736 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/2-regexp-methods/article.md @@ -0,0 +1,376 @@ +# Методы RegExp и String + +В JavaScript методы для работы с регулярными выражениями есть в классе `RegExp`, а также встроены в обычные строки `String` + +Всего этих методов немного, поэтому мы сначала рассмотрим их по отдельности, а затем -- рецепты по решению стандартных задач с ними. + +[cut] + +## Методы строк + +### str.search(regexp) + +Этот метод мы уже видели. + +Он возвращает позицию первого совпадения или `-1`, если ничего не найдено. + +```js +//+ run +var str = "Люблю регэкспы я, но странною любовью"; + +alert( str.search( *!*/лю/i*/!* ) ); // 0 +``` + +**Ограничение метода `search` -- он всегда ищет только первое совпадение.** + +Нельзя заставить `search` искать дальше первого совпадения, такой синтаксис попросту не предусмотрен. Но есть другие методы, которые это умеют. + +### str.match(regexp) без флага g, скобочные выражения + +Метод `str.match` работает по-разному, в зависимости от наличия или отсутствия флага `g`, поэтому сначала мы разберём вариант, когда его нет. + +В этом случае `str.match(regexp)` находит только одно, первое совпадение. + +Результат вызова -- это массив, состоящий из этого совпадения, с дополнительными свойствами `index` -- позиция, на которой оно обнаружено и `input` -- строка, в которой был поиск. + +Например: + +```js +//+ run +var str = "ОЙ-Ой-ой"; + +var result = str.match( *!*/ой/i*/!* ); + +alert( result[0] ); // ОЙ (совпадение) +alert( result.index ); // 0 (позиция) +alert( result.input ); // ОЙ-Ой-ой (поисковая строка) +``` + +У этого массива не всегда только один элемент. + +**Если часть шаблона обозначена скобками, то она станет отдельным элементом массива.** + +Например: + +```js +//+ run +var str = "javascript - это такой язык"; + +var result = str.match( *!*/JAVA(SCRIPT)/i*/!* ); + +alert( result[0] ); // javascript (всё совпадение полностью) +alert( result[1] ); // script (часть совпадения, соответствующая скобкам) +// также есть свойства result.index, result.input +``` + +Благодаря флагу `i` поиск не обращает внимание на регистр буквы, поэтому находит javascript. При этом часть строки, соответствующая SCRIPT, выделена в отдельный элемент массива. Позже мы используем это для поиска с заменой. + +### str.match(regexp) с флагом g + +При наличии флага `g`, вызов `match` возвращает обычный массив из всех совпадений. + +Никаких дополнительных свойств у массива в этом случае нет, скобки дополнительных элементов не порождают. + +Например: + +```js +//+ run +var str = "ОЙ-Ой-ой"; + +var result = str.match( *!*/ой/ig*/!* ); + +alert( result ); // ОЙ, Ой, ой +``` + +Пример со скобками: + +```js +//+ run +var str = "javascript - это такой язык"; + +var result = str.match( *!*/JAVA(SCRIPT)/gi*/!* ); + +alert( result[0] ); // javascript +alert( result.length ); // 1 +alert( result.index ); // undefined +``` + +Из последнего примера видно, что элемент в массиве ровно один, и свойства `index` также нет. Такова особенность глобального поиска при помощи `match` -- он просто возвращает все совпадения. + +Для расширенного глобального поиска, который позволит получить все позиции и, при желании, скобки, нужно использовать метод [:RegExp#exec], которые будет рассмотрен далее. + + +[warn header="В случае, если совпадений не было, `match` возвращает `null`"] +Обратите внимание, это важно -- если `match` не нашёл совпадений, он возвращает не пустой массив, а именно `null`. + +Это важно иметь в виду, чтобы не попасть в такую ловушку: + +```js +//+ run +var str = "Ой-йой-йой"; + +alert( str.match( /лю/gi ).length ) // ошибка! нет свойства length у null +``` + +[/warn] + +### str.split(regexp|substr, limit) + +Разбивает строку в массив по разделителю -- регулярному выражению `regexp` или подстроке `substr`. + +Обычно мы используем метод `split` со строками, вот так: + +```js +//+ run +alert( '12-34-56'.split('-') ) // [12, 34, 56] +``` + +Можно передать в него и регулярное выражение, тогда он разобьёт строку по всем совпадениям. + +Тот же пример с регэкспом: + +```js +//+ run +alert( '12-34-56'.split(/-/) ) // [12, 34, 56] +``` + +### str.replace(regexp, newSubStr|function) + +Швейцарский нож для работы со строками, поиска и замены любого уровня сложности. + +Его простейшее применение -- поиск и замена подстроки в строке, вот так: + +```js +//+ run +// заменить дефис на двоеточие +alert( '12-34-56'.replace("-", ":" ) ) // 12:34-56 +``` + +При вызове со строкой замены `replace` всегда заменяет только первое совпадение. + +**Чтобы заменить *все* совпадения, нужно использовать для поиска не строку `"-"`, а регулярное выражение /-/g, причём обязательно с флагом `g`:** + +```js +//+ run +// заменить дефис на двоеточие +alert( '12-34-56'.replace( *!*/-/g*/!*, ":" ) ) // 12:34:56 +``` + +В строке для замены можно использовать специальные символы: + + + + + + + + + + + + + + + + + + + + + + + + + + +
                      СпецсимволыДействие в строке замены
                      `$$`Вставляет `"$"`.
                      `$&`Вставляет всё найденное совпадение.
                      $`Вставляет часть строки до совпадения.
                      + $' + Вставляет часть строки после совпадения.
                      + $*n* + где `n` -- цифра или двузначное число, обозначает `n-ю` по счёту скобку, если считать слева-направо.
                      + +Пример использования скобок и `$1`, `$2`: + +```js +//+ run +var str = "Василий Пупкин"; + +alert( str.replace( /(Василий) (Пупкин)/ ,'$2, $1') ) // Пупкин, Василий +``` + +Ещё пример, с использованием `$&`: + +```js +//+ run +var str = "Василий Пупкин"; + +alert( str.replace( /Василий Пупкин/ ,'Великий $&!') ) // Великий Василий Пупкин! +``` + +**Для ситуаций, который требуют максимально "умной" замены, в качестве второго аргумента предусмотрена функция.** + +Она будет вызвана для каждого совпадения, и её результат будет вставлен как замена. + +Например: + +```js +//+ run +var i = 0; + +// заменить каждое вхождение "ой" на результат вызова функции +alert( "ОЙ-Ой-ой".replace( /ой/gi, function() { return ++i; }) ); // 1-2-3 +``` + +Эта функция также получает аргументы: + +
                        +
                      1. `str` -- найденное совпадение,
                      2. +
                      3. `p1, p2, ..., pn` -- содержимое скобок
                      4. +
                      5. `offset` -- позиция, на которой найдено совпадение
                      6. +
                      7. `s` -- исходная строка
                      8. +
                      + +Если скобок в регулярном выражении нет, то у функции всегда будет ровно 3 аргумента. + +Используем это, чтобы вывести полную информацию о совпадениях: + +```js +//+ run +// вывести и заменить все совпадения +function replacer(str, offset, s) { + alert("Найдено: " + str + " на позиции: " + offset + " в строке: " + s); + return ":" +} + +var result = "ОЙ-Ой-ой".replace( /ой/gi, replacer); +alert('Результат: ' + result); +``` + +Со скобочными выражениями: + +```js +//+ run +function replacer(str, name, surname, offset, s) { + return surname +", " + name; +} + +alert( str.replace( /(Василий) (Пупкин)/ , replacer) ) // Пупкин, Василий +``` + +Функция для замены -- это самое мощное средство, какое только может быть. Она владеет всей информацией о совпадении и имеет доступ к замыканию, поэтому может всё. + +## Методы объектов RegExp + +Регэкспы являются объектами класса [:RegExp]. + +У них есть два основных метода. + +### regexp.test(str) + +Проверяет, есть ли хоть одно совпадение в строке `str`. Возвращает `true/false`. + +Работает, по сути, так же, как и проверка `str.search(regexp) != -1`, например: + +```js +//+ run +var str = "Люблю регэкспы я, но странною любовью"; + +alert( *!*/лю/i*/!*.test(str) ) // true +alert( str.search(*!*/лю/i*/!*) != -1 ) // true +``` + +Пример с отрицательным результатом: + +```js +//+ run +var str = "Ой, цветёт калина..."; + +alert( *!*/javascript/i*/!*.test(str) ) // false +alert( str.search(*!*/javascript/i*/!*) != -1 ) // false +``` + +### regexp.exec(str) + +Этот метод -- самое мощное, что только есть для поиска с использованием регулярных выражений. + +Он ведёт себя по-разному, в зависимости от того, есть ли у регэкспа флаг `g`. + +
                        +
                      • Если флага `g` нет, то `regexp.exec(str)` ищет и возвращает первое совпадение, является полным аналогом вызова `str.match(regexp)`.
                      • +
                      • Если флаг `g` есть, то вызов `regexp.exec` возвращает первое совпадение и *запоминает* его позицию в свойстве `regexp.lastIndex`. Последующий поиск он начнёт уже с этой позиции. Если совпадений не найдено, то сбрасывает `regexp.lastIndex` в ноль.
                      • +
                      + +Второй вариант запуска обычно используют в цикле: + +```js +//+ run +var str = 'Многое по JavaScript можно найти на сайте http://javascript.ru'; + +var regexp = /javascript/ig; + +alert("Начальное значение lastIndex: " + regexp.lastIndex); + +while( result = regexp.exec(str) ) { + alert('Найдено: ' + result[0] + ' на позиции:' + result.index); + alert('Свойство lastIndex: ' + regexp.lastIndex); +} + +alert('Конечное значение lastIndex: ' + regexp.lastIndex); +``` + +Здесь цикл продолжается до тех пор, пока `regexp.exec` не вернёт `null`, что означает "совпадений больше нет". + +Найденные результаты последовательно помещаются в `result`, причём находятся там в том же формате, что и `match` -- с учётом скобок, со свойствами `result.index` и `result.input`. + +[smart header="Поиск с нужной позиции"] +Технически, можно заставить `regexp.exec` искать сразу с нужной позиции, если поставить `lastIndex` вручную: + +```js +//+ run +var str = 'Многое по JavaScript можно найти на сайте http://javascript.ru'; + +var regexp = /javascript/ig; +regexp.lastIndex = 40; + +alert( regexp.exec(str).index ); // 49, поиск начат с 40й позиции +``` + +[/smart] + +## Итого, рецепты + +Методы становятся гораздо понятнее, если разбить их использование по задачам, которые нужны в реальной жизни. + +
                        +
                      • Для поиска только одного совпадения: +
                        +
                        Найти позицию первого совпадения
                        +
                        `str.search(regexp)`
                        +
                        Найти само совпадение
                        +
                        `str.match(regexp)`
                        +
                        Проверить, есть ли хоть одно совпадение
                        +
                        `regexp.test(str)`, также можно использовать `str.search(regexp) != -1`
                        +
                        Найти совпадение с нужной позиции
                        +
                        `regexp.exec(str)`, начальную позицию поиска задать в `regexp.lastIndex`
                        +
                        +
                      • +
                      • Для поиска всех совпадений: +
                        +
                        Найти массив совпадений
                        +
                        `str.match(regexp)`, с флагом `g`
                        +
                        Получить все совпадения, с подробной информацией о каждом
                        +
                        `regexp.exec(str)` с флагом `g`, в цикле
                        +
                        +
                      • +
                      • Дополнительно: +
                        +
                        Для поиска-и-замены
                        +
                        `str.replace(regexp, str|func)`
                        +
                        Для разбивки строки на части
                        +
                        `str.split(regexp)`
                        +
                        +
                      • +
                      + +Далее мы перейдём к более подробному изучению синтаксиса регулярных выражений. + diff --git a/3-more/8-regular-expressions-javascript/3-regexp-character-classes/article.md b/3-more/8-regular-expressions-javascript/3-regexp-character-classes/article.md new file mode 100644 index 00000000..83b2337c --- /dev/null +++ b/3-more/8-regular-expressions-javascript/3-regexp-character-classes/article.md @@ -0,0 +1,152 @@ +# Символьные классы + +Рассмотрим задачу -- есть телефонный номер `"+7(903)-123-45-67"`, и нам нужно найти в этой строке цифры, а остальные символы нас не интересуют. + +**Для поиска символов определённого вида, в регулярных выражениях предусмотрены "классы символов".** +[cut] +Класс символов -- это, в первую очередь, специальное обозначение. + +Например, в данном случае нам нужен класс "произвольная цифра", он обозначается `\d`. + +Это обозначение вставляется в паттерн наравне с остальными символами. При поиске под него подходит любая цифра. + +Пример ниже ищет все цифры в строке: + +```js +//+ run +var str = "+7(903)-123-45-67"; + +var reg = /\d/g + +alert( str.match(reg) ); // 7,9,0,3,1,2,3,4,5,6,7 +``` + +Есть и другие классы. Самые полезные: +
                      +
                      `\d` (от английского "digit" - "цифра")
                      +
                      Цифра, символ от `0` до `9`.
                      +
                      `\s` (от английского "space" - "пробел")
                      +
                      Пробельный символ, включая табы, переводы строки и т.п.
                      +
                      `\w` (от английского "word" -- "слово")
                      +
                      Символ латинского алфавита или цифра или подчёркивание `'_'`
                      +
                      + +**Регулярное выражение обычно содержит одновременно и обычные символы и классы**: + +Например, найдём строку `CSS` с любой цифровой версией: + +```js +//+ run +var str = "Стандарт CSS4 - наше будущее"; +var reg = /CSS\d/ + +alert( str.match(reg) ); +``` + +Несколько классов в одном регэкспе: + +```js +//+ run +showMatch( "Я люблю HTML5!", /\s\w\w\w\w\d/ ); // 'HTML5' +``` + +Совпадение (каждому классу в регэкспе соответствует один символ результата): + + + +**Также существуют обратные символьные классы:** + +
                      +
                      `\D`
                      +
                      Не-цифра, любой символ кроме `\d`
                      +
                      `\S`
                      +
                      Не-пробел, любой символ кроме `\s`.
                      +
                      `\W`
                      +
                      Символ, не принадлежащий латиннице, а также не буква и не подчёркивание, алфавиту, т.е. любой кроме `\w`
                      +
                      + +Например, мы хотим получить из телефона +7(903)-123-45-67 только цифры. + +Есть два способа сделать это. + +
                        +
                      1. Первый -- найти все цифры и объединить их: +Например: + +```js +//+ run +var str = "+7(903)-123-45-67"; + +var digits = str.match( /\d/g ).join(""); +alert(digits); // 79031234567 +``` + +
                      2. +
                      3. Второй -- найти все НЕцифры и удалить их из строки: + +```js +//+ run +var str = "+7(903)-123-45-67"; + +alert( str.replace(/\D/g, "") ); // 79031234567 +``` + +Второй способ короче, не правда ли? +
                      4. +
                      + +**Регулярное выражение может также содержать стандартные спецсимволы строк, такие как `\n, \t` и другие.** + +Они являются обычными символами. Отличить их от классов очень просто -- для классов зарезервированы другие буквы. + +[warn header="Пробелы важны!"] +Обычно мы не обращаем внимание на пробелы. Для нашего взгляда строки 1-5 и 1 - 5 почти идентичны. + +Но в регулярных выражениях **пробел - такой же символ, как и другие**. + +Поиск ниже не сработает, т.к. не учитывает пробелы вокруг дефиса: + +```js +//+ run +alert( "1 - 5".match (/\d-\d/) ); // null, нет совпадений! +``` + +Поправим это, добавив в паттерн пробелы: + +```js +//+ run +alert( "1 - 5".match (/\d - \d/) ); // работает, пробелы вокруг дефиса +``` + +В регулярные выражения также не надо вставлять лишние пробелы. Все они имеют значение: + +```js +//+ run +alert( "1-5".match( /\d - \d/ ) ); // null, так как в строке 1-5 нет пробелов +``` + +[/warn] + +Особым классом символов является точка `"."`. + +**В регулярном выражении, точка "." обозначает *любой символ*, кроме перевода строки**: + +```js +//+ run +var re = /CS.4/; + +alert( "Стандарт CSS4".match(re) ); // найдено "CSS4" +alert( "Сталь CS-4".match(re) ); // найдено "CS-4" +alert( "CS 4".match(re) ); // найдено "CS 4", пробел тоже символ +``` + +Обратим внимание -- точка означает именно "произвольный символ". + +То есть какой-то символ на этом месте в строке должен быть: + +```js +//+ run + +alert( "CS4".match (/CS.4/) ); // нет совпадений, так как для точки нет символа +``` + diff --git a/3-more/8-regular-expressions-javascript/3-regexp-character-classes/love_html5.png b/3-more/8-regular-expressions-javascript/3-regexp-character-classes/love_html5.png new file mode 100755 index 00000000..13b27b6f Binary files /dev/null and b/3-more/8-regular-expressions-javascript/3-regexp-character-classes/love_html5.png differ diff --git a/3-more/8-regular-expressions-javascript/4-regexp-special-characters/article.md b/3-more/8-regular-expressions-javascript/4-regexp-special-characters/article.md new file mode 100644 index 00000000..1f08dd20 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/4-regexp-special-characters/article.md @@ -0,0 +1,46 @@ +# Экранирование специальных символов + +В регулярных выражениях есть и другие символы, имеющие особый смысл. + +Они используются, чтобы расширить возможности поиска. + +Вот их полный список: [ \ ^ $ . | ? * + ( ). + +Не пытайтесь запомнить его -- когда мы разберёмся с каждым из них по отдельности, он запомнится сам собой. + +**Чтобы использовать специальный символ в качестве обычного, он должен быть *экранирован*.** + +Или, другими словами, перед символом должен быть обратный слэш `'\'`. + +Например, нам нужно найти точку '.'. В регулярном выражении она означает "любой символ, кроме новой строки", поэтому чтобы найти именно сам символ "точка" -- её нужно экранировать: \.. + +```js +//+ run +alert( "Глава 5.1".match( /\d\.\d/ ) ); // 5.1 +``` + +Круглые скобки также являются специальными символами, так что для поиска именно скобки нужно использовать `\(`. Пример ниже ищет строку `"g()"`: + +```js +//+ run +alert( "function g()".match( /g\(\)/ ) ); // "g()" +``` + +**Слэш `'/'`, хотя и не является специальными символом, но открывает-закрывает регэксп в синтаксисе /...pattern.../. Поэтому его тоже нужно экранировать: '\/'**. + +Так выглядит поиск слэша `'/'`: + +```js +//+ run +alert( "/".match( /\// ) ); // '/' +``` + +Ну и, наконец, если нам нужно найти сам обратный слэш `\`, то его нужно просто задублировать. + +Так выглядит поиск обратного слэша `"\"`: + +```js +//+ run +alert( "1\2".match( /\\/ ) ); // '\' +``` + diff --git a/3-more/8-regular-expressions-javascript/5-regexp-character-sets-and-ranges/article.md b/3-more/8-regular-expressions-javascript/5-regexp-character-sets-and-ranges/article.md new file mode 100644 index 00000000..ee32ba08 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/5-regexp-character-sets-and-ranges/article.md @@ -0,0 +1,149 @@ +# Наборы и диапазоны [...] + +Если в регулярном выражении нескольки символов или символьных классов заключены в квадратные скобки `[…]`, то это означает "искать любой символ из указанных в `[…]`". + +Например, [еао] означает любой символ из этих трёх: `'а'`, `'е'`, или `'о'`. +[cut] +Можно использовать квадратные скобки вместе с обычными символами. + +Например: + +```js +//+ run +// найти [г или т], а затем "оп" +alert( "Гоп-стоп".match( /[гт]оп/gi ) ); // "Гоп", "топ" +``` + +Несмотря на то, что в квадратных скобках несколько символов, в совпадении должен присутствовать *ровно один* из них. + +Поэтому в примере ниже нет результатов: + +```js +//+ run +// найти "В", затем [у или а], затем "ля" +alert( "Вуаля".match( /В[уа]ля/ ) ); // совпадений нет +``` + +Совпадение было бы, если бы после `"Ву"` шло сразу `"ля"`, например Вуля или если бы `"Ва"` сопровождалось `"ля"`, то есть Валя. + +**Квадратные скобки могут также содержать *диапазоны символов*.** + +Например, [a-z] -- произвольный символ от `a` до `z`, [0-5] -- цифра от `0` до `5`. + +В примере ниже мы будем искать `"x"`, после которого идёт два раза `[0-9A-F]` -- цифра или буква от A до F: + +```js +//+ run +// найдёт "xAF" +alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); +``` + +Обратим внимание, в слове Exception есть сочетание xce, но оно не подошло, потому что буквы в нём маленькие, а в диапазоне -- большие. + +Если хочется искать и его тоже, можно добавить в скобки диапазон `a-f`: [0-9A-Fa-f]. Или же просто указать у всего регулярного выражения флаг `i`. + +**Символьные классы -- всего лишь более короткие записи для диапазонов, в частности:** + +
                        +
                      • **\d** -- то же самое, что [0-9],
                      • +
                      • **\w** -- то же самое, что [a-zA-Z0-9_],
                      • +
                      • **\s** -- то же самое, что [\t\n\v\f\r ] плюс несколько юникодных пробельных символов.
                      • +
                      + +**В квадратных скобках можно использовать и диапазоны и символьные классы -- вместе.** + +Например, нам нужно найти слова в тексте. Если оно на английском -- это достаточно просто: + +```js +//+ run +var str = "The sun is rising!"; + +alert( str.match( /\w+/g ) ); // The, sun, is, rising*!* +``` + +А если есть слова и на русском? + +```js +//+ run +var str = "Солнце встаёт!"; + +alert( str.match( /\w+/g ) ); // null*!* +``` + +Ничего не найдено! Это можно понять, ведь \w -- это именно английская букво-цифра, как можно видеть из аналога [a-zA-Z0-9_]. + +Чтобы находило слово на русском -- нужно использовать диапазон, например /[а-я]/. А чтобы на обоих языках -- и то и другое вместе: + +```js +//+ run +var str = "Солнце (the sun) встаёт!"; + +alert( str.match( /[\wа-я]+/gi ) ); // Солнце, the, sun, вста, т*!* +``` + +...Присмотритесь внимательно к предыдущему примеру! Вы видите странность? Оно не находит букву ё, более того -- считает её разрывом в слове. Причина -- в кодировке юникод, она подробно раскрыта в главе [](/string). Буква `ё` лежит в стороне от основной кириллицы и её следует добавить в диапазон дополнительно, вот так: + +```js +//+ run +var str = "Солнце (the sun) встаёт!"; + +alert( str.match( /[\wа-яё]+/gi ) ); // Солнце, the, sun, встаёт*!* +``` + +Теперь всё в порядке. + +**Кроме обычных, существуют также *исключающие* диапазоны: [^…].** + +Квадратные скобки, начинающиеся со знака каретки: [^…] находят любой символ, *кроме указанных*. + +Например: + +
                        +
                      • [^аеуо] -- любой символ, кроме `'a'`, `'e'`, `'y'`, `'o'`
                      • +
                      • [^0-9] -- любая не-цифра, то же что `\D`
                      • +
                      • [^\s] -- любой не-пробельный символ, как и `\S`
                      • +
                      + +Пример ниже ищет любые символы, кроме букв, цифр и пробелов: + +```js +//+ run +alert( "alice15@gmail.com".match( /[^\d\sA-Z]/gi ) ); // "@", "." +``` + +**В квадратных скобках большинство специальных символов можно использовать без экранирования.** + +Обычно, если мы хотим искать именно точку, а не любой символ, или именно `"\d"`, а не произвольную цифру, то мы используем экранирование: указываем `\.` или `\\d`. + +Но квадратные скобки подразумевают поиск одного символа из нескольких или из диапазона. Использование некоторых специальных символов внутри них не имело бы смысла, и они воспринимаются как обычные символы. + +Это касается символов: +
                        +
                      • точка '.'
                      • +
                      • плюс '+'
                      • +
                      • круглые скобки '( )'
                      • +
                      • дефис '-', если он находится в начале или конце квадратных скобок, то есть не выделяет диапазон.
                      • +
                      • символ каретки '^', если не находится в начале квадратных скобок, то есть кроме [\^..].
                      • +
                      • а также открывающая квадратная скобка '['
                      • +
                      + +То есть, точка `"."` в квадратных скобках означает не "любой символ", а обычную точку. + +Например, регэксп [-().^+] ищет один из символов `-().^`. В данном контексте эти символы не являются специальными. + +```js +//+ run +var re = /[-().^+]/g; + +alert( "1 + 2 - 3".match(re) ); // найдёт +, - +``` + +...Впрочем, даже если вы решите "на всякий случай" заэкранировать эти символы, поставив перед ними обратный слэш `\` -- вреда не будет: + +```js +//+ run +var re = /[\-\(\)\.\^\+]/g; + +alert( "1 + 2 - 3".match(re) ); // тоже работает: +, - +``` + diff --git a/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/1-find-text-manydots/solution.md b/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/1-find-text-manydots/solution.md new file mode 100644 index 00000000..350fdc0e --- /dev/null +++ b/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/1-find-text-manydots/solution.md @@ -0,0 +1,9 @@ +Символ `.` является специальным, значит его надо экранировать. +Регулярное выражение: + +```js +//+ run +var reg = /\.{3,}/g; +alert("Привет!... Как дела?.....".match(reg)); // ..., ..... +``` + diff --git a/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/1-find-text-manydots/task.md b/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/1-find-text-manydots/task.md new file mode 100644 index 00000000..750f6c59 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/1-find-text-manydots/task.md @@ -0,0 +1,14 @@ +# Как найти многоточие... ? + +[importance 5] + +Напишите регулярное выражения для поиска многоточий: трёх или более точек подряд. + + +Проверьте его: + +```js +var reg = /ваше выражение/g; +alert("Привет!... Как дела?.....".match(reg)); // ..., ..... +``` + diff --git a/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/article.md b/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/article.md new file mode 100644 index 00000000..0eced219 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/6-regexp-numeric-quantifiers/article.md @@ -0,0 +1,47 @@ +# Цифровые квантификаторы {n} + +Рассмотрим задачу -- взять телефон вида `+7(903)-123-45-67` и найти все числа в нём. То есть, нас интересует результат вида `7, 903, 123, 45, 67`. + +Нечто похожее мы уже делали ранее -- мы искали цифры. Для этого было достаточно класса `\d`. Но здесь нужно искать *числа* -- последовательности из 1 или более цифр. + +**Количество повторений символа можно указать с помощью числа в фигурных скобках: `{n}`.** + +
                      +
                      Точное количество: `{5}`
                      +
                      Паттерн \d{5} обозначает 5 цифр, как и +
                      Количество от-до: `{3,5}`
                      +
                      Для того, чтобы найти, например, числа размером от трёх до пяти знаков, нужно указать границы в фигурных скобках: `\d{3,5}` + +```js +//+ run +alert( "Мне не 12, а 1234 года".match( /\d{3,5}/ ) ); // "1234" +``` + +Последнее значение можно и не указывать. Тогда выражение `\d{3,}` найдет числа, длиной от трех знаков: + +```js +//+ run +alert( "Мне не 12, а 345678 лет".match( /\d{3,5}/ ) ); // "345678" +``` + +
                      +
                      + +В случае с телефоном нам нужны числа - одна или более цифр подряд. Этой задаче соответствует регулярное выражение \d{1,}: + +```js +//+ run +var str = "+7(903)-123-45-67"; + +alert( str.match( /\d{1,}/g ) ); // 7,903,123,45,67 +``` + diff --git a/3-more/8-regular-expressions-javascript/7-regexp-quantifiers/article.md b/3-more/8-regular-expressions-javascript/7-regexp-quantifiers/article.md new file mode 100644 index 00000000..f71e8c51 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/7-regexp-quantifiers/article.md @@ -0,0 +1,113 @@ +# Квантификаторы +, * и ? + +Для самые часто востребованных квантификаторов есть специальные короткие обозначения. + +[cut] +
                      +
                      `+`
                      +
                      Означает "один или более", то же что `{1,}`. + +Например, \d+ находит числа -- последовательности из 1 или более цифр: + +```js +//+ run +var str = "+7(903)-123-45-67"; + +alert( str.match( /\d+/g ) ); // 7,903,123,45,67 +``` + +
                      +
                      `?`
                      +
                      Означает "ноль или один", то же что и `{0,1}`. По сути, делает символ необязательным. + +Например, ou?r найдёт or в слове color и our в его британском варианте написания colour. + +```js +//+ run +var str = "Можно писать color или colour"; + +alert( str.match( /colou?r/g ) ); // color, colour +``` + +
                      +
                      `*`
                      +
                      Означает "ноль или более", то же что `{0,}`. То есть, символ может повторяться много раз или вообще отсутствовать. + +Пример ниже находит цифру, после которой идёт один или более нулей: + +```js +//+ run +alert( "100 10 1".match( /\d0*/g ) ); // 100, 10, 1 +``` + +Сравните это с `'+'` (один или более): + +```js +//+ run +alert( "100 10 1".match( /\d0+/g ) ); // 100, 10 +``` + +
                      +
                      + +Эти квантификаторы -- одни из важнейших "строительных блоков" для сложных регулярных выражений, поэтому мы рассмотрим ещё примеры. + +
                      +
                      Десятичная дробь (число с точкой внутри): \d+\.\d+
                      +
                      + +```js +//+ run +alert( "0 1 12.345 7890".match( /\d+\.\d+/g ) ); // 123.45 +``` + +
                      +
                      Открывающий HTML-тег без атрибутов, такой как `` или `

                      `: /<[a-z]+>/i

                      +
                      Пример: + +```js +//+ run +alert( " ... ".match ( /<[a-z]+>/gi ) ); // +``` + +Это регулярное выражение ищет символ '<', за которым идут одна или более букв английского алфавита, и затем '>'. +
                      +
                      Открывающий HTML-тег без атрибутов (лучше): /<[a-z][a-z0-9]*>/i
                      +
                      +Здесь регулярное выражение расширено: в соответствие со стандартом, HTML-тег может иметь символ на любой позиции, кроме первой, например `

                      `. + +```js +//+ run +alert( "

                      Привет!

                      ".match( /<[a-z][a-z0-9]*>/gi ) ); //

                      +``` + +

                      +
                      Открывающий или закрывающий HTML-тег без атрибутов: /<\/?[a-z][a-z0-9]*>/i
                      +
                      В предыдущий паттерн добавили необязательный слэш /? перед тегом. Его понадобилось заэкранировать, чтобы JavaScript не принял его за конец шаблона. + +```js +//+ run +alert( "

                      Привет!

                      ".match( /<\/?[a-z][a-z0-9]*>/gi ) ); //

                      ,

                      +``` + +
                      +
                      + + +[smart header="Точнее -- значит сложнее"] +Здесь мы видим классическую ситуацию, которая повторяется из раза в раз. + +Чем точнее регулярное выражение, тем оно длиннее и сложнее. + +Например, для HTML-тегов, скорее всего, подошло бы и более короткое регулярное выражение <\w+>. + +Так как класс `\w` означает "любая цифра или английская буква или _`, то под такой шаблон подойдут и не теги, например <_>, однако он гораздо проще, чем <[a-z][a-z0-9]*>. + +Подойдёт ли нам <\w+> или нужно использовать именно <[a-z][a-z0-9]*>? + +В реальной жизни допустимы оба варианта. Ответ на подобные вопросы зависит от того, насколько реально важна точность и насколько сложно потом будет отфильтровать лишние совпадения (если появятся). +[/smart] + + + + diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/article.md b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/article.md new file mode 100644 index 00000000..2e3bbd51 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/article.md @@ -0,0 +1,179 @@ +# Жадный и ленивый режимы + +Теперь время залезть "под капот" регулярных выражений и посмотреть, как происходит поиск. + +Это понимание необходимо для того, чтобы искать что-либо сложнее чем /\d+/. +[cut] + +Для примера рассмотрим задачу, которая часто возникает в типографике -- заменить кавычки вида `"..."` на "кавычки-лапки": `«...»`. + +Шаблон, который мы будем использовать для поиска подстрок в кавычках, будет выглядеть так: + +```js +//+ run +var reg = /".+"/g; + +var str = 'a "witch" and her "broom" is one'; + +alert( str.match(reg) ); +``` + +Запустите этот пример... + +Упс! Он не работает! Вместо того, чтобы найти два совпадения "witch" и "broom", он находит одно: "witch" and her "broom". + +Это как раз тот случай, когда *жадность* -- причина всех зол. + +## Алгоритм поиска + +Движок регулярных выражений пытается проверить строку на соответствие шаблону, начиная с самой первой, нулевой позиции. Если не получается, он идёт вперёд и пытается найти с 1й позиции и так далее. + +Чтобы сделать происходящее максимально наглядным и понятным, проследим, что именно он делает для паттерна ".+". + +
                        +
                      1. Первый символ шаблона -- это кавычка ". + +Движок регулярных выражений пытается найти её на 0й позиции, но там совсем другой символ, поэтому на 0й позиции соответствия явно нет. + +Далее он переходит 1ю, 2ю позицию в исходной строке и, наконец, обнаруживает кавычку на 3й позиции: + +
                      2. +
                      3. Кавычка найдена, далее движок проверяет, есть ли соответствие для остальной части паттерна. + +В данном случае следующий символ паттерна -- `.` (точка). Она обозначает "любой символ", так что следующая буква строки 'w' вполне подходит: + +
                      4. +
                      5. Далее "любой символ" повторяется, так как стоит квантификатор .+. Движок регулярных выражений берёт один символ за другим, до тех пор, пока у него это получается. + +В данном случае это означает "до конца строки": + +
                      6. +
                      7. Итак, текст закончился, движок регулярных выражений больше не может найти "любой символ", он закончил строить соответствие для .+ и очень рад по этому поводу. + +Следующий символ шаблона -- это кавычка. Её тоже необходимо найти, чтобы соответствие было полным. А тут -- беда, ведь поисковый текст завершился! + +Движок регулярных выражений понимает, что, наверное, взял многовато .+ и начинает отступать обратно ("фаза бэктрекинга" -- backtracking на англ.). + +Иными словами, он сокращает текущее совпадение на один символ: + + + +После этого он ещё раз пытается подобрать соответствие для остатка паттерна. Но кавычка '"' не совпадает с 'e'.
                      8. +
                      9. ...Так что движок уменьшает число повторений .+ ещё раз: + + + +Кавычка '"' не совпадает с 'n'. Опять неудача.
                      10. +
                      11. Движок продолжает отступать, он уменьшает количество повторений точки '.' до тех пор, пока остаток паттерна не совпадёт: + + +
                      12. +
                      13. Мы получили результат. Так как у регэкспа есть флаг `g`, то поиск продолжится, однако это произойдёт после первого совпадения и не даст новых результатов.
                      14. +
                      + +**В жадном режиме (по умолчанию) регэксп повторяет квантификатор настолько много раз, насколько это возможно, чтобы найти соответствие.** + +Возможно, это не совсем то, что мы хотели, но так это работает. + +## Ленивый режим + +Ленивый режим работы квантификаторов -- противоположность жадному, он означает "повторять минимальное количество раз". + +Его можно включить, если поставить знак вопроса '?' после квантификатора, так что он станет таким: *? или +? или даже ?? для '?'. + +Чтобы не возникло путаницы -- важно понимать: обычно `?` сам является квантификатором (ноль или один). Но если он стоит *после другого квантификатора (или даже после себя)*, то обретает другой смысл -- в этом случае он меняет режим его работы на ленивый. + +Регэксп /".+?"/g работает, как задумано -- находит отдельно witch и broom: + +```js +//+ run +var reg = /".+?"/g; + +var str = 'a "witch" and her "broom" is one'; + +alert( str.match(reg) ); // witch, broom +``` + +Чтобы в точности понять, что происходим, разберём в деталях, как ищется ".+?". + +
                        +
                      1. Первый шаг -- тот же, кавычка '"' найдена на 3й позиции: + +
                      2. + +
                      3. Второй шаг -- тот же, находим произвольный символ '.': + +
                      4. + +
                      5. А вот дальше -- так как стоит ленивый режим работы `+`, то движок пытается повторять точку (произвольный символ) *минимальное количество раз*. + +Так что он тут же пытается проверить, достаточно ли повторить 1 раз -- и для этого пытается найти соответствие остальной части шаблона, то есть '"': + + +Нет, один раз повторить недостаточно. В данном случае, символ `'i' != '"'`, но если бы оставшаяся часть паттерна была бы более сложной -- алгоритм остался бы тем же. Если остаток шаблона не находится -- увеличиваем количество повторений. +
                      6. +
                      7. Движок регулярных выражений увиличивает количество повторений точки на одно и пытается найти соответствие остатку шаблона ещё раз: + + +Опять неудача. Тогда поисковой движок увеличивает количество повторений ещё и ещё... +
                      8. +
                      9. Только на 5м шаге поисковой движок наконец находит соответствие для остатка паттерна: + + +
                      10. +
                      11. Так как поиск происходит с флагом `g`, то он продолжается с конца текущего совпадения, давая ещё один результат: + + +
                      12. +
                      + +В примере выше продемонстрирована работа ленивого режима для +?. Квантификаторы +? и ?? ведут себя аналогично -- "ленивый" движок увеличивает количество повторений только в том случае, если для остальной части шаблона на данной позиции нет соответствия, в то время как жадный сначала берёт столько повторений, сколько возможно, а потом отступает назад. + +**Ленивость распространяется только на тот квантификатор, после которого стоит `?`.** + +Прочие квантификаторы остаются жадными. + +Например: + +```js +//+ run +alert( "123 456".match ( /\d+ \d+?/g) ); // 123 4 +``` + +
                        +
                      1. Подпаттерн \d+ пытается найти столько символов, сколько возможно (работает жадно), так что он находит 123 и останавливается, поскольку символ пробела ' ' не подходит под \d.
                      2. +
                      3. Далее идёт пробел, и в игру вступает \d+?. + +Он находит один символ '4' и пытатся проверить, есть ли совпадение с остатком шаблона (после \d+?). + +Здесь мы ещё раз заметим -- ленивый режим без необходимости ничего не возьмёт. + +Так как шаблон закончился, то поиск завершается и 123 4 становится результатом.
                      4. +
                      5. Следующий поиск продолжится с `5`, но ничего не найдёт.
                      6. +
                      + +[smart header="Конечные автоматы и не только"] +Современные движки регулярных выражений могут иметь более хитрую реализацию внутренних алгоритмов, чтобы искать быстрее. + +Однако, чтобы понять, как работает регулярное выражение, и строить регулярные выражения самому, знание этих хитрых алгоритмов ни к чему. Они служат лишь внутренней оптимизации способа поиска, описанного выше. + +Кроме того, сложные регулярные выражения плохо поддаются всяким оптимизациям, так что поиск вполне может работать и в точности как здесь описано. +[/smart] + +## Альтернативный подход + +В данном конкретном случае, возможно искать строки в кавычках, оставаясь в жадном режиме, с использованием регулярного выражения "[^"]+": + +```js +//+ run +var reg = /"[^"]+"/g; + +var str = 'a "witch" and her "broom" is one'; + +alert( str.match(reg) ); // witch, broom +``` + +Регэксп "[^"]+" даст правильные результаты, поскольку ищет кавычку '"', за которой идут столько не-кавычек (исключающие квадратные скобки), сколько возможно. Так что вторая кавычка автоматически прекращает повторения [^"]+ и позволяет найти остаток шаблона ". + + + diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy1.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy1.png new file mode 100755 index 00000000..744e8cb6 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy1.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy2.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy2.png new file mode 100755 index 00000000..495b0a16 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy2.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy3.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy3.png new file mode 100755 index 00000000..5afa50be Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy3.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy4.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy4.png new file mode 100755 index 00000000..730afe3e Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy4.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy5.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy5.png new file mode 100755 index 00000000..a5b3f42a Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy5.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy6.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy6.png new file mode 100755 index 00000000..b428bda3 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_greedy6.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy3.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy3.png new file mode 100755 index 00000000..3dc3b35e Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy3.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy4.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy4.png new file mode 100755 index 00000000..4330a8db Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy4.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy5.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy5.png new file mode 100755 index 00000000..1b1de1bb Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy5.png differ diff --git a/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy6.png b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy6.png new file mode 100755 index 00000000..c5598c3f Binary files /dev/null and b/3-more/8-regular-expressions-javascript/8-regexp-greedy-and-lazy/witch_lazy6.png differ diff --git a/3-more/8-regular-expressions-javascript/9-regexp-groups/article.md b/3-more/8-regular-expressions-javascript/9-regexp-groups/article.md new file mode 100644 index 00000000..dcfd7072 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/9-regexp-groups/article.md @@ -0,0 +1,126 @@ +# Группы + +Часть шаблона может быть заключена в скобки (...). Такие выделенные части шаблона называют "скобочными выражениями" или "скобочными группами". + +У такого выделения есть два эффекта: +
                        +
                      1. Он позволяет выделить часть совпадения в отдельный элемент массива при поиске через [:String#match] или [:RegExp#exec].
                      2. +
                      3. Если поставить квантификатор после скобки, то он применится *ко всей скобке*, а не всего лишь к одному символу.
                      4. +
                      + +[cut] +В примере ниже, шаблон (go)+ находит один или более повторяющихся 'go': + +```js +//+ run +alert( 'Gogogo now!'.match( /(go)+/i ); // "Gogogo" +``` + +Без скобок, шаблон /go+/ означал бы g, после которого идёт одна или более o, например: goooo. + + +**Скобки нумеруются слева направо. Поисковой движок запоминает содержимое каждой скобки и позволяет обращаться к нему, в том числе -- в шаблоне и строке замены.** + +Например, найти HTML-тег можно шаблоном <.*?>. Скорее всего, после поиска мы захотим что-то сделать с результатом, и нас будет интересовать содержимое `<...>`. + +Для удобства заключим его в скобки: <(.*?)>. Тогда содержимое скобок можно будет получить отдельно. + +Используем метод [:String#match]. В результирующем массиве будет сначала всё совпадение, а далее -- скобочные группы, в данном случае -- только одна: + +```js +//+ run +var str = '

                      Привет, мир!

                      ' +var reg = /<(.*?)>/ + +alert( str.match(reg) ) // массив:

                      , h1 +``` + +Для поиска всех совпадений, как мы обсуждали ранее, используется метод [:RegExp#exec]. + +**Скобки могут быть и вложенными. В этом случае нумерация также идёт слева направо.** + +Например, в строке <span class="my"> нас может интересовать отдельно тег `span` и, для примера, его первая буква. + +Добавим скобки в регулярное выражение: + +```js +//+ run +var str = ''; + +reg = /<(([a-z])[a-z0-9]*).*?>/; + +alert( str.match(reg) ); // , span, s +``` + +Вот так выглядят скобочные группы: + + +На нулевом месте -- всегда совпадение полностью, далее -- группы. Их вложенность означает всего лишь, что группа 1 содержит группу 2. Нумерация всегда идёт слева направо, по открывающей скобке. + +**Даже если скобочная группа необязательна и не входит в совпадение, соответствующий элемент массива существует (и равен `undefined`).** + +Например, рассмотрим регэксп a(z)?(c)?. Он ищет `"a"`, за которой не обязательно идёт буква `"z"`, за которой необязательно идёт буква `"c"`. + +Если напустить его на строку из одной буквы `"a"`, то результат будет таков: + +```js +//+ run +match = 'a'.match( /a(z)?(c)?/ ) + +alert(match.length); // 3 +alert(match[0]); // a +alert(match[1]); // undefined +alert(match[2]); // undefined +``` + +Массив получился длины `3`, но все скобочные группы -- `undefined`. + +А теперь более сложная ситуация, строка ack: + +```js +//+ run +match = 'ack'.match( /a(z)?(c)?/ ) + +alert(match.length); // 3 +alert(match[0]); // ac, всё совпадение +alert(match[1]); // undefined, для (z)? ничего нет +alert(match[2]); // c +``` + +Длина массива результатов по-прежнему `3`. Она постоянна. А вот для скобочной группы (z)? в ней ничего нет. + +**Скобочную группу можно исключить из запоминаемых и нумеруемых, добавив в её начало ?:** + +Бывает так, что скобки нужны, чтобы квантификатор правильно применился, а вот запоминать её в массиве не нужно. Тогда мы просто ставим сразу после открывающей скобки `?:` + +В примере ниже есть скобочная группа (go-?), которая сама по себе не интересна, но входит в результаты: + +```js +//+ run +var str = "Go-go John!"; +*!* +var reg = /(go-?)* (\w+)/i; +*/!* + +var result = str.match(reg); + +alert( result[0] ); // Go-go John +alert( result[1] ); // go +alert( result[2] ); // John +``` + +Исключим её из запоминаемых: + +```js +//+ run +var str = "Go-go John!"; +*!* +var reg = /(?:go-?)* (\w+)/i; +*/!* + +var result = str.match(reg); + +alert( result[0] ); // Go-go John +alert( result[1] ); // John +``` + diff --git a/3-more/8-regular-expressions-javascript/9-regexp-groups/groups.png b/3-more/8-regular-expressions-javascript/9-regexp-groups/groups.png new file mode 100755 index 00000000..41f1b555 Binary files /dev/null and b/3-more/8-regular-expressions-javascript/9-regexp-groups/groups.png differ diff --git a/3-more/8-regular-expressions-javascript/index.md b/3-more/8-regular-expressions-javascript/index.md new file mode 100644 index 00000000..c021f806 --- /dev/null +++ b/3-more/8-regular-expressions-javascript/index.md @@ -0,0 +1,9 @@ +# Регулярные выражения [в работе] + +Регулярные выражения -- мощный способ поиска и замены строк, который используется в самых разных языках, включая, конечно, JavaScript. + +У меня здесь для вас две новости. +
                        +
                      1. Первая плохая -- в JavaScript они на редкость убогие, хуже большинства существующих языков.
                      2. +
                      3. Вторая хорошая -- зато нам будет проще их изучить.
                      4. +
                      diff --git a/3-more/9-tools/1-tools-browser-extensions/article.md b/3-more/9-tools/1-tools-browser-extensions/article.md new file mode 100644 index 00000000..c91ffc92 --- /dev/null +++ b/3-more/9-tools/1-tools-browser-extensions/article.md @@ -0,0 +1,129 @@ +# Полезные расширения Firefox и Chrome + +Здесь мы посмотрим ряд полезных расширений, которые, надеюсь, смогут вам пригодиться. +[cut] + +Если какое-то из расширений вас заинтересует -- его можно поставить. В Firefox расширения устанавливаются из меню "Инструменты -> Дополнения", в Chrome -- через меню "Настройки -> Расширения -> (внизу) Ещё расширения" или напрямую из "магазина" `https://chrome.google.com/webstore/category/home`. + +Для расширений других браузеров такого единого способа нет. + +## Web Developer (FF, Ch) + +Это расширение добавляет наверх браузера панель с разнообразными инструментами: + + + + +Большинство возможностей, скорее, полезны для верстальщиков, но и для разработчиков кое-что есть. + +Например: + +
                        +
                      • **Disable Cache** -- полностью отключает кэш браузера
                      • +
                      • **Disable Cookies** -- браузер перестанет посылать и принимать Cookie. Впрочем, Firebug это тоже умеет (вкладка Cookie)
                      • +
                      • **Disable JavaScript** -- нужно редко, но иногда полезно
                      • +
                      • **Resize** на нужное разрешение -- у разработчиков мониторы большие, а у пользователей разные. Можно добавить своё разрешение.
                      • +
                      + +Иконок там много, поэтому наверняка вы чем-то не будете пользоваться. Лишние иконки можно убрать. В Firefox(Win) это делается так: Правый клик на панель -> Настроить -> Перетащить лишние иконки с панели. + +## DNS Flusher (FF) / DNS Flusher for Chrome + +Это расширение позволяет сбросить кэш DNS одним кликом. + +Оно нужно в тех случаях, когда вы меняете адреса в файле `hosts` и хотите, чтобы изменения вступили в действие тут же. + +При установке в статусной строке, снизу, появляется кнопочка с IP-адресом. По клику на ней кэш DNS сбрасывается. + + + + +## Более удобный Firebug (FF) + +Здесь собраны расширения, улучшающие работу отладчика Firebug для Firefox. + +
                      +
                      Fire Rainbow
                      +
                      Подсветка кода для Firebug
                      +
                      FireQuery
                      +
                      Для разработки под jQuery -- выводит в разных местах вспомогательную информацию.
                      +
                      Firebug Autocompleter
                      +
                      Включает автодополнение для консоли, когда она в многострочном режиме.
                      +
                      [CSS-X-Fire](http://code.google.com/p/css-x-fire/)
                      +
                      Позволяет редактировать CSS в Firebug и сохранять изменения, интегрировано с редакторами от JetBrains (IntelliJ IDEA и другие).
                      +
                      + +## JsonView (FF,Ch) + +Даёт возможность открыть JSON-документ прямо в браузере. + +Обычно браузер не понимает `Content-Type: application/json` и пытается сохранить JSON-файлы. + +Но если в нём стоит это расширение, то он покажет файл в удобном виде, с возможностью навигации: + + + +## Xml Tree (Ch) + +Расширение для просмотра XML для Chrome. + +Остальные браузеры умеют это делать "из коробки". + + +## YSlow, PageSpeed Insights (FF, Ch) + +Эти два расширения позволяют по-быстрому оценить скорость загрузки страницы, проанализировать массу возможных причин тормозов и получить советы по улучшению производительности. + +Они очень похожи, но не идентичны, так что можно поставить оба. + + + +## AdBlock (FF, Ch) + +Расширение для отключения назойливой рекламы и баннеров. + +Режет не всё, но многое. Да, оно не для разработки, но настолько полезное, что я не смог удержаться и опубликовал его здесь. + +Кстати, насчёт разработки... Бывает так, что AdBlock прячет рекламу на сайтах, которые мы разрабатываем. К хорошему быстро привыкаешь, и если юзер будет слать вам ошибки на страницах (а у вас их нет) -- проверьте! Может быть, стоит отключить AdBlock для конкретной страницы и посмотреть без него? Исключения можно поставить в настройках расширения. + +## DownloadHelper (FF) + +Расширение для скачивания видео из Youtube, Blip TV и других хостингов. + +С одной стороны, это расширение тоже напрямую не связано с разработкой... + +..Но с другой -- бывает ли, что работая на компьютере вы получаете ссылку на интересное видео с конференции? Или заходите на сайт и видите там набор прекрасных выступлений, которые хочется скачать и посмотреть в более удобное время с планшетника. Скажем, во время поездки, когда нет интернет. + +Если бывает, то это расширение -- для вас. Скачиваем отличные видео и смотрим, когда захотим. + +Способ использования: +
                        +
                      1. Зайти на страницу.
                      2. +
                      3. Начать смотреть видео.
                      4. +
                      5. Увидеть, как иконка расширения "ожила", кликнуть на нее и выбрать видео в нужном разрешении (если есть несколько).
                      6. +
                      7. Файл будет скачан в папку `dwhelper` (по умолчанию), место можно поменять в настройках.
                      8. +
                      + + + +## P.S. + +В этот список расширений я включил самое любимое и полезное, что связано именно с расширениями браузера. + +Есть дополнительные инструменты, которые становятся в систему, они идут в отдельном разделе. + +У вас есть, что добавить? Расширения, которые вам очень помогли? Укажите их в комментариях. + + + + + + + + + + + + + + diff --git a/3-more/9-tools/1-tools-browser-extensions/dnsflusher.png b/3-more/9-tools/1-tools-browser-extensions/dnsflusher.png new file mode 100755 index 00000000..76069bf6 Binary files /dev/null and b/3-more/9-tools/1-tools-browser-extensions/dnsflusher.png differ diff --git a/3-more/9-tools/1-tools-browser-extensions/downloadhelper.jpg b/3-more/9-tools/1-tools-browser-extensions/downloadhelper.jpg new file mode 100755 index 00000000..4ecdcb67 Binary files /dev/null and b/3-more/9-tools/1-tools-browser-extensions/downloadhelper.jpg differ diff --git a/3-more/9-tools/1-tools-browser-extensions/jsonview.png b/3-more/9-tools/1-tools-browser-extensions/jsonview.png new file mode 100755 index 00000000..efb92baa Binary files /dev/null and b/3-more/9-tools/1-tools-browser-extensions/jsonview.png differ diff --git a/3-more/9-tools/1-tools-browser-extensions/pageinsight.png b/3-more/9-tools/1-tools-browser-extensions/pageinsight.png new file mode 100755 index 00000000..e98d4193 Binary files /dev/null and b/3-more/9-tools/1-tools-browser-extensions/pageinsight.png differ diff --git a/3-more/9-tools/1-tools-browser-extensions/webdeveloper.png b/3-more/9-tools/1-tools-browser-extensions/webdeveloper.png new file mode 100755 index 00000000..d571b3af Binary files /dev/null and b/3-more/9-tools/1-tools-browser-extensions/webdeveloper.png differ diff --git a/3-more/9-tools/2-fiddler/article.md b/3-more/9-tools/2-fiddler/article.md new file mode 100644 index 00000000..c0da788e --- /dev/null +++ b/3-more/9-tools/2-fiddler/article.md @@ -0,0 +1,99 @@ +# Скриптуемый отладочный прокси Fiddler + +Fiddler -- прокси, который работает с трафиком между Вашим компьютером и удаленным сервером, и позволяет инспектировать и менять его. + +Fiddler можно расширять с помощью скриптов на языке JScript.NET (писать их очень просто), менять меню программы, и вообще -- замечательный инструмент. + +Использовать его можно с любым браузером. + +Эта статья описывает Fiddler 2.4. В вашей версии Fiddler какие-то возможности могут измениться, какие-то образоваться. +[cut] + +## Режимы подключения + +У Fiddler есть 2 способа подключения. + +
                        +
                      1. Первый -- это просто запустить его. При этом он автоматически будет работать для программ, использующих WinINET. + +Это, например, Internet Explorer, Chrome, приложения MS Office. + +Firefox тоже автоматически подхватит Fiddler, за счёт того что при установке Fiddler в него ставится соответствующее расширение: + + +
                      2. +
                      3. Второй -- это явно поставить Fiddler как прокси для браузера, по адресу `127.0.0.1:8888`. Например, так можно сделать для Opera, если уж не хочется перезапускать.
                      4. +
                      + +## Fiddler не под Windows + +Если вы работаете не под Windows, то Fiddler можно поставить в виртуальную машину. + +Чтобы сделать возможной подключение внешних браузеров, нужно включить настройках Fiddler: Tools -> Fiddler Options -> Connections(вкладка) галочку "Allow remote clients to connect". После этого Fiddler станет доступен как прокси на интерфейсе `0.0.0.0`, так что можно будет браузеру из внешней ОС указать в качестве прокси виртуальную машину. И пользоваться Fiddler. + +Если вы так захотите поступить, то вдобавок возьмите удобный переключатель прокси, например Elite Proxy Switcher под Firefox или [Proxy Pick](http://www.bayden.com/ietoys/) для IE, чтобы переключение на прокси осуществлялось в один клик. + +## Операции над запросами + +При заходе в Fiddler, открывается окно запросов слева и рабочие вкладки справа. + + + +Как видно, опций много, и на изображении они еле-еле помещаются. И, поверьте, возможностей -- ещё больше. + +## Возможности + +
                        +
                      • **В окне запросов** слева можно просматривать и выбирать запросы, смотреть их заголовки, сохранять их на диск все вместе или по отдельности.
                      • +
                      • **AutoResponder** -- позволяет подставить свой файл вместо сервера. + +Например, приятель попросил поправить скрипт `vasya.js` на сайте, но доступа к серверу не дал. + +С Fiddler задача решается просто -- сохраняете скрипт на диске, в AutoResponder указываете, что `vasya.js` нужно брать с диска, а не с сайта -- и исправляете, что нужно, перезагружаете страницу, проверяете -- всё с комфортом.
                      • +
                      • **Composer** -- позволяет составить запрос на сервер вручную. + +Например, вы хотите сделать такой же AJAX-запрос, как только что делали. Для этого можно просто выбрать его слева и нажать кнопку Replay (слева-сверху). + +А если хочется поменять? Нет ничего проще -- выбираем справа Composer и перетаскиваем запрос слева в него. После этого исправляем, что хотим и Execute.
                      • +
                      • **Filters** -- позволяет назначить действия в зависимости от вида запроса. Опции станут понятны после перехода на вкладку.
                      • +
                      • **FiddlerScript** -- основа мощи Fiddler: скрипт, задающий функционал. Его редактированием можно добавить или удалить пункты меню, колонки в списке запросов, и, вообще, поменять почти всё. + +Язык программирования JScript.NET, который здесь используется, может взаимодействовать с Windows в полном объеме, включая коммуникацию с базой данных, Word, Excel.
                      • + + +## Правила + +Слева-сверху в меню находится пункт Rules (правила). В нём изначально находятся некоторые возможности FiddlerScript, которые поставляются "из коробки". + +Хотите посмотреть, как ваш сайт будет грузиться "на GPRS"? Выбирайте Rules -> Performance -> Simulate Modem speeds. + +Для добавления новых правил можно их задать через пункт "Customize Rules" (на JScript.NET, разумеется). В открывающемся скрипте есть пункты меню и их реализация. + +При наступлении любого события из обширного списка, Fiddler вызывает соответствующий обработчик из правил. Например, onBeforeRequest, onShutdown. Стандартные правила отлично прокомментированы, и писать новые весьма просто. + +FiddlerScript позволяет манипулировать заголовками, запросом, менять ширину канала, управлять выводом запроса в Fiddler и так далее и т.п. + +## Брейкпойнт на запросе +В меню Rules -> Automatic Breakpoints можно включить автоматическое прерывание Fiddler при обработке запроса. + +После этого, если сделать запрос в браузере, подключенном к Fiddler, то его выполнение зависнет, а в левом окошке Fiddler этот запрос будет отмечен специальным значком. + +Если выбрать такой подвисший запрос мышкой, то во вкладке SessionInspector им можно управлять: менять сам запрос и ответ сервера (после Break on Response, когда сервер уже ответил). + +Задавать прерывание на запросах определенного вида также можно через Filters. + +## Отладка HTTPS + +Fiddler является прокси, а HTTPS шифруется от браузера до сервера-получателя, поэтому по умолчанию Fiddler не имеет доступа к содержимому HTTPS-запросов. + +Чтобы его получить, Fiddler должен сыграть роль хакера-перехватчика: расшифровывать запросы, и потом отправлять дальше. Это возможно, если инсталлировать специальный сертификат: Tools -> Fiddler Options -> HTTPS(вкладка) -> выбрать все галочки. + +После окончания отладки этот сертификат можно убрать. + +## Скачать + +Fiddler можно бесплатно скачать с сайта разработчика. Там же доступна документация и видео. + +К фиддлеру прилагается галерея расширений http://www.fiddlertool.com/fiddler2/extensions.asp. + +Примеры скриптов для Fiddler, которые дают общее представление о том, на что он может быть способен: http://www.fiddlertool.com/fiddler/dev/scriptsamples.asp. \ No newline at end of file diff --git a/3-more/9-tools/2-fiddler/fiddler-hook.png b/3-more/9-tools/2-fiddler/fiddler-hook.png new file mode 100755 index 00000000..bb62bfc9 Binary files /dev/null and b/3-more/9-tools/2-fiddler/fiddler-hook.png differ diff --git a/3-more/9-tools/2-fiddler/fiddler.png b/3-more/9-tools/2-fiddler/fiddler.png new file mode 100755 index 00000000..4f29e41e Binary files /dev/null and b/3-more/9-tools/2-fiddler/fiddler.png differ diff --git a/3-more/9-tools/3-ie-http-analyzer/article.md b/3-more/9-tools/3-ie-http-analyzer/article.md new file mode 100644 index 00000000..56818c7b --- /dev/null +++ b/3-more/9-tools/3-ie-http-analyzer/article.md @@ -0,0 +1,43 @@ +# IE HTTP Analyzer + +Под Windows есть как минимум два весьма основных инструмента для отладки на уровне HTTP. + +Один из них -- Fiddler. Другой -- IE HTTP Analyzer, который представляет собой "другой угол зрения" на задачу. +[cut] + +## Возможности +IE HTTP Analyzer -- платный, и имеет две ипостаси. + +Первая - это обычная программа, запущенная в отдельном окне. При нажатии на кнопку "Play" - начинает перехват HTTP для всех браузеров, включая Firefox. + + + +Вторая -- наподобие Firebug, раскрывающаяся полоса внизу браузера Internet Explorer. Это -- плагин, вызывается из панели или меню браузера. + + + +Существуют другие программы похожего вида, но, перебрав несколько, я остановился на этой, как самой удобной. + +Основные возможности сводятся к удобной отладке HTTP-запросов, да и кнопочки на скриншотах выше достаточно очевидны. + +## Просмотр и создание HTTP-запросов. +Этим сейчас никого не удивишь, если бы не одно БОЛЬШОЕ НО. + +Программа дает возможность удобно просматривать потоковые запросы. + +Т.е, например, отлаживаете Вы чат. А он держит непрерывный iframe, через который сервер передает данные. Программы типа Fiddler будут честно ждать конца запроса, чтобы показать Вам его, а IE HTTP Analyzer не менее честно будет сбрасывать поток пред Ваши светлы очи. + +Запросы могут поступать все новые и новые, но в панели инспектора есть кнопка , которая управляет автопереходом к следующему запросу. При анализе потока она должна быть выключена. + +Эта "потоковость" IE Http Analyzer- один из важных его плюсов. + +## Конструирование фильтров + +В отличие от правил на языке JScript.NET, которые используются для фильтрации в Fiddler, здесь фильтры на интересующие Вас запросы можно конструировать из стандартных компонент, объединяя их условиями ИЛИ/И. + +## Дополнительные возможности +Очистка куков и кеша браузера. Проверено, работает. + +Программа платная. + +Живет на http://www.ieinspector.com/httpanalyzer/. \ No newline at end of file diff --git a/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer-2.gif b/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer-2.gif new file mode 100755 index 00000000..cb7e588c Binary files /dev/null and b/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer-2.gif differ diff --git a/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer-3.gif b/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer-3.gif new file mode 100755 index 00000000..2ba8113d Binary files /dev/null and b/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer-3.gif differ diff --git a/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer.gif b/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer.gif new file mode 100755 index 00000000..10ca985b Binary files /dev/null and b/3-more/9-tools/3-ie-http-analyzer/ie-http-analyzer.gif differ diff --git a/3-more/9-tools/index.md b/3-more/9-tools/index.md new file mode 100644 index 00000000..a5dc0ab0 --- /dev/null +++ b/3-more/9-tools/index.md @@ -0,0 +1,2 @@ +# Сундучок с инструментами + diff --git a/3-more/index.md b/3-more/index.md new file mode 100644 index 00000000..91f11c38 --- /dev/null +++ b/3-more/index.md @@ -0,0 +1,2 @@ +# Дополнительные темы +