Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 1. Введение в JavaScript .NET Ecosystem confusion Anders Hejlsberg, (born 2 December 1960) is a prominent Danish software engineer who was the original author of Turbo Pascal and the chief architect of Delphi (at Borland, 1986). In 1989 he moved to California. He is a lead architect of C# and core developer on TypeScript and Roslyn. The oldest and most well-known runtime is the .NET Framework. You can build more application types with the .NET Framework, but the types are mainly Windows-centric. This is because the .NET Framework uses some Windows-specific API’s for some application types. NET Core was released in 2016 and can be used to create ASP.NET Core and Universal Windows Platform (UWP) applications with. It runs cross-platform and can be installed side-by-side, meaning that you can have many versions of .NET Core running on the same computer. It is also small and optimized for performance. NET Core is not the new version of the .NET Framework, it is just a different version that you can use for some use cases. NET Core is not going to replace the .NET Framework. Developers should use .NET Core when they want to create a cross-platform and open-source framework (Windows, Linux and macOS). It can be used to develop applications on any platform. Often it is used for cloud applications or refactoring large enterprise applications into microservices. The 2.1 version nightly builds of NET Core are on Github. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 The Xamarin and Mono projects worked to bring .NET to mobile devices, macOS, and Linux. Xamarin has been around since about 2011 and you can use it to create applications for iOS, for macOS and Android. This is used to build and run native or near-native mobile applications across mobile platforms. The Mono runtime with Xamarin uses specific API’s for iOS and Android and for building Xamarin.Mac applications. The three runtimes all implement .NET Standard. Docker Desktop provides an integrated container-native development experience; it launches as an application from your Mac or Windows toolbar and provides access to the largest library of community and certified Linux and Windows Server content from Docker Hub. Docker containers wrap up software and its dependencies into a standardized unit for software development that includes everything it needs to run: code, runtime, system tools and libraries . This guarantees that your application will always run the same and makes collaboration as simple as sharing a container image. .NET Core 2.1 Performance In general, the Just In Time (JIT) compiler is much smarter now and can optimize common scenarios, and a new set of APIs provides much more efficient resource usage then before, mostly by reducing allocations. The first NoSQL provider for EF Core that is made available by Microsoft. Still in preview, but you can already use it in most cases, to access your CosmosDB (formerly Azure DocumentDB) databases. Kestrel is a cross-platform web server for ASP.NET Core. ASP.NET Core project templates use Kestrel by default. In Program.cs, the template code calls CreateDefaultBuilder, which calls UseKestrel behind the scenes. A development certificate is created when the .NET Core SDK is installed. ASP.NET Core 2.1 and later project templates configure apps to run on HTTPS by default and include HTTPS redirection and HSTS support. Angular, React, and React with Redux are templates to use the Single Page Application approach. ASP.NET Core SignalR is an open-source library that simplifies adding real-time web functionality to apps. Real-time web functionality enables server-side code to push content to clients instantly. SignalR provides an API for creating server-toclient remote procedure calls (RPC). The RPCs call JavaScript functions on clients from server-side .NET Core code. _______________________________________________ Введение в JavaScript JavaScript (JS) — это язык объектно-ориентированного и функционального программирования. Автором языка является Brendan Eich, который создал его в 1995, работая в компании Netscape. Он заимствовал идеи некоторых других языков программирования, существовавших в то время. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 JS поддерживает динамику изменения типов переменных во время выполнения. Он прошел путь от интерпретатора до JIT-компилятора (Just In Time Compiler). Порядок работы Web-приложение (проект типа ASP.NET Web Application) представляет собой множество страниц, связанных между собой ссылками. В вашем проекте должны быть учебные страницы и две или более страниц, разработанных самостоятельно. Одна из них демонстрирует вашу технику программирования на JavaScript (ООП подход). Уровень ее сложности должен быть не ниже уровня последних двух примеров, приведенных во второй главе. Другие страницы типа Razor Page иллюстрируют ваше умение работать с базой данных в рамках технологии Entity Framework. Подробности описаны в документе "Introduction to Razor Pages". Также добавьте в ваш единственный проект типа ASP.NET Core Web Application все те страницы, которые вы использовали в процессе самостоятельного изучения курса. В первой главе этого документа описан процесс создания Webприложения и начальные шаги по разработке HTML-страниц, содержащих JavaScript. Вам необходимо выполнить все эти шаги и сохранить результат в проекте. Далее подключите к этому проекту все примеры, приведенные во второй главе. Они демонстрируют приемы объектно-ориентированного подхода к разработке JavaScript кода, которые вам следует использовать при разработке первой части задания. В третьей главе приведено описание некоторых теоретических аспектов языка JavaScript, которые помогут выполнить вам задание (пользуйтесь также материалами сайта JavaScript.ru). При разработке своего кода вы не должны пользоваться библиотеками типа: JQuery, React, Underscore, Webix и др. Вы можете использовать только код библиотеки Kit.js, которую мы вскоре создадим. Вы можете расширять эту библиотеку, но вряд ли стоит слепо копировать в нее все приемы из JQuery. Однако, если вам нравится какой-то прием, если он приносит пользу вашему проекту и вы можете объяснить как он работает, то смело вставляйте его в файл Kit.js и используйте при выполнении вашего задания. Добавляемый вами код не должен быть тривиальным и, желательно, быть красивым. Создание проекта типа Web Application Откройте Visual Studio и выберите команду File / New / Project... Выберите шаблон проекта VisualC# / Web / ASP.NET Core Web Application. Задайте имя проекта Messages, снимите (выключите) флаг Create directory for solution. Укажите путь (Location) к папке с решением D:\Users\, если вы работаете в классе, или D:\Projects, если вы работаете на своем компьютере. Нажмите OK. Студия сгенерирует второй диалог настроек проекта, в котором: Выберите шаблон Empty, убедитесь в том, что выбраны настройки (или установите) .Net Core, ASP.NET Core версии 2.2 или выше. Убедитесь, что выключен механизм аутентификации Снимите флаги Docker и Configure for HTTPS и нажмите OK. Папка с проектами, предлагаемая по умолчанию (C:\Users\UserName\source\repos\), плоха тем, что в случае гибели системы она (и вся ваша работа) погибает вместе с ней. Кроме того, короткий путь D:\Projects или D:\Users легко запоминается и им легко пользоваться. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 В окне Solution Explorer найдите узел Properties и сделайте над ним двойной щелчок мыши. В левой части диалога по установке свойств проекта найдите и выделите заголовок Application. Убедитесь, что мы создаем Console Application (что довольно необычно) и выбрана версия .NET Core 2.2 (или выше). Затем проверьте настройки под заголовком Debug. Убедитесь, что роль сервера будет выполнять служба IIS, что роль клиента будет выполнять браузер. В диалоге свойств проекта убедитесь, что переменная окружения ASPNETCORE_ENVIRONMENT установлена в значение Development (при сдаче проекта заказчику это значение следует заменить на Production). Смысл флагов Enable SSL и Enable anonimous Authentication прозрачен, но важно понять, что имя сервера будет localhost (синоним для имени машины). Номер порта выбран случайно в безопасном от коллизий диапазоне. Всего портов: 65536 (2 в степени 16), но какие-то из них заняты системой. В моем проекте адрес таков: https://localhost:44331. В вашем случае номер порта будет иным (скорее всего). В настройках Windows установите в качестве браузера по умолчанию либо Chrome, либо Edge. Это упростит процесс отладки кода JavaScript. Просьба, не комментируйте начальные версии кода, безжалостно вносите все предлагаемые изменения. Вы ничего не упустите, в конце-концов мы получим серьезный код, который полнее и лучше, чем тот, что присутствует в начальной заготовке. Откройте файл Program.cs, который находится в корневой директории проекта и замените все его содержимое, как показано ниже. using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace Messages { public class Program { public static void Main() { WebHost .CreateDefaultBuilder() .UseStartup<Startup>() .Build() .Run(); } } } Мы упростили код начальной заготовки, оставив в ней лишь один метод Main. Смысл этого кода создать объект, в котором размещается (hosted) Web-приложение, и запустить приложение в режиме прослушивания запросов. Замените весь код класса Startup, как показано ниже, (но не трогайте внешнюю декларацию блока namespace Messages). public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app .UseStaticFiles() .UseFileServer() .Run(async (c) => await c.Response.WriteAsync("Middleware could not handle this request...")); } } Запустите проект на выполнение — Ctrl+F5 (или F5, если вы, как Винни-Пух, никуда не торопитесь). Напомню, Ctrl+F5 — выполнение, F5 — отладка, которая требует значительно больше ресурсов. В браузере вы видите сообщение: "Middleware could not...". Это означает, что оба Use-сервиса не сработали. Обычный HTML Сервис UseStaticFiles включает возможность использования статических ресурсов — файлов типов: css, txt, json, xml и других. Сначала реализуем сервис UseFileServer. Он основан на использовании файлов: html, js. Все такие файлы по умолчанию должны располагаться в папке с именем wwwroot. Добавьте в проект такую папку и обратите внимание на то, что она особым образом (особой иконкой) отображается в окне Solution Explorer. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 В этой папке располагаются папки и файлы, которые реализуют традиционный подход к разработке сайтов. В папку wwwrot добавьте html-файл по имени default.html и множество папок, как показано выше. Совершите двойной щелчок над файлом default.html и внесите в него изменения, как показано ниже. <html> <head> <title>Default</title> <script> onload = () => { spDay.innerHTML=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][new Date().getDay()]; } </script> </head> <body> <h1>Using File Server</h1> Today is <span id="spDay"></span><br> </body> </html> Здесь вы видите код JavaScript, который запускается по окончании загрузки HTML в браузер. Вновь запустите приложение (Ctrl+F5) и убедитесь, что вы видите текущий день недели. Что произошло? Включение сервиса UseFileServer помогло обнаружить на сервере файл default.html и отправить его в браузер. Имя и расположение файла важно. Файл с именем index.html тоже подойдет. Однако, замените имя на d.html и запустите вновь. Скорее всего вы увидите день недели, но это обман, который спровоцирован кешем браузера. Теперь уже в браузере нажмите Ctrl+F5 (убрать кеш) и убедитесь, что вы вновь видите сообщение об ошибке "Middleware...". Правила по умолчанию не воспринимают файлы с произвольными именами в качестве стартовых документов сайта. Развитие стартовой страницы Обычно стартовая страница содержит текст общего характера, планку меню и множество гиперссылок, которые поволяют осуществлять навигацию по страницам сайта. Замените все содержимое файла default.html, как показано ниже. <html> <head> <meta charset="utf-8" /> <title>Default</title> <link rel="icon" type="image/png" href="images/favicon.png"> <link href="css/Site.css" rel="stylesheet"> <script src="../js/Kit.js"></script> <script> onload = () => { start.onclick = () => location.href = 'html/Start.html'; frm.onclick = () => location.href = 'html/Form.html'; frmJS.onclick = () => location.href = 'html/FormJS.html'; frmEl.onclick = () => location.href = 'html/FormElements.html'; frmM.onclick = () => location.href = 'html/FormModal.html'; calc.onclick = () => location.href = 'html/Calculator.html'; split.onclick = () => location.href = 'html/SplitQuery.html'; ball.onclick = () => location.href = 'html/Balls.html'; zoo.onclick = () => location.href = 'html/Zoo.html'; tb.onclick = () => location.href = 'html/Toolbar.html'; tbd.onclick = () => location.href = 'html/ToolbarDyn.html'; dnd.onclick = () => location.href = 'html/DnD.html'; sna.onclick = () => location.href = 'html/Snake.html'; evp.onclick = () => location.href = 'html/EventPropagation.html'; frs.onclick = () => location.href = 'html/Forest.html'; chess.onclick = () => location.href = 'html/Chess.html'; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 clock.onclick = () => location.href = 'html/Clock.html'; cSty.onclick = () => location.href = 'html/ClockStyled.html'; mEv.onclick = () => location.href = 'html/Event00.html'; ajx.onclick = () => location.href = 'html/AjaxFetcher.html'; err.onclick = () => location.href = 'Nosite.html'; spDay.innerHTML = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][new Date().getDay()]; } </script> </head> <body> <h1>Select a sample</h1> <form> <ul> <li id="start">Start</li> <li id="frm">Form</li> <li id="frmJS">Form created by JS</li> <li id="frmEl">Form Elements</li> <li id="frmM">Form Modal</li> <li id="calc">Calculator</li> <li id="split">Split Query</li> <li id="ball">Balls</li> <li id="zoo">Zoo</li> <li id="tb">Toolbar</li> <li id="tbd">Toolbar Dynamic</li> <li id="dnd">Drag and Drop</li> <li id="sna">Snake</li> <li id="evp">Event Propagation</li> <li id="frs">Forest</li> <li id="chess">Chess</li> <li id="clock">Clock</li> <li id="cSty">Clock Styled</li> <li id="mEv">Mouse Events</li> <li id="ajx">Ajax Fetcher</li> <li id="err">Imitate 404</li> </ul> </form> Today is <span id="spDay"></span><br> <a class="link" href="/Index">Go to MVC</a> </body> </html> Здесь содержится множество гиперссылок, которые позволят вам запускать отдельные учебные страницы. В данный момент этих страниц нет, поэтому при запуске приложения и выборе любой ссылки мы получим сообщение сервера: "Middleware could not...". Добавьте внутрь папки html новый html-файл с именем Start.html. Замените его содержимое, как показано ниже. <html> <head> <title>Start</title> </head> <body> <h1>Введение в технологию HTML</h1> <h4>HTML, CSS и JavaScript</h4> <p> Любой документ HTML состоит из комбинации тегов, атрибутов, их значений и простого текста. Эта комбинация определяет структуру и облик Web-страницы. Большинство тегов HTML спарены так, что за открывающим следует закрывающий тег, а между ними содержится текст и/или другие теги.<br> Пара тегов и содержимое между ними образуют блок (HTML-элемент). Взаимосвязанные элементы представляют собой иерархическую древовидную структуру. Модель программирования DOM (Document Object Model) описывает механизм доступа к произвольным элементам HTML-документа и управления его структурой. </p> </body> </html> При запуске приложения из студии (Ctrl+F5) или запуске процесса отладки (F5) сервер (роль которого выполняет служба IIS Express) пошлет в браузер страницу default.html. При выбре гиперссылки Start (в рамках страницы default) сервер пошлет в браузер файл Start.html. Стили HTML-документа При внесении изменений в HTML-документ не обязательно закрывать браузер и заново запускать приложение из студии. Достаточно сохранить изменения (нажав в студии Ctrl+S), перейти в браузер и обновить его. Но иногда браузер игнорирует изменения и воспроизводит старую версию, которую он берет из своего кэша. Способы обновления Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 браузера и очистки кэша постоянно меняются. Ранее кэш очищался с помощью комбинации Ctrl+F5, теперь эти трюки иногда дают сбой в браузере Chrome. Если ваши изменения не проходят, ищите в сети ответ на вопрос "Как очистить кэш браузера". Просто обновить браузер можно с помощью иконки или (с помощью F5). Поиграйте с кодом страницы Start, чтобы понять, как браузер воспроизводит стандартные теги. Например, добавьте пару тегов <br> (line break), затем тег <hr>. Сохраните и просмотрите результат. Теги <br> и <hr> (horizontal ruler) относятся к группе empty-тегов (самозакрывающихся). Добавьте еще один самозакрывающийся тег img (картинка, то есть, изображение произвольного типа). <img src="../images/USSR.jpg"> Для того, чтобы эта картинка загрузилась, нажмите Win+E, найдите папку Images в папке нашего курса на диске Public2, выделите все файлы (Ctrl+A) и перетащите их в папку images проекта (в окне Solution Explorer). Сохраните изменение и обновите браузер — на странице появится картинка. Рассмотрим, как добавить панель типа <div> (этог тег должен иметь закрывашку </div>). Внутрь открывашки <div> добавьте атрибут style, а между открывашкой и закрывашкой — текст, как показано ниже. <div style="background:#ffe;text-align:center;">This is a divider container</div> Так называемый разделяющий тег div содержит атрибут style. Каждый атрибут имеет имя и значение (имя — style, значение — коллекция пар вида key:value; Коллекция содержит компоненты стиля. Выделите значение #ffe ключа background и нажмите Ctrl+Space. Студия поможет выбрать цвет. Выделите значение атрибута src картинки и нажмите Ctrl+Space. Замените картинку с помощью подсказки. Сейчас важно понять, как работает язык разметки HTML (HyperText Markup Language) и как манипулировать стилями элементов. Стили включаются каскадом. Атрибут style внутри html-элемента является последним каскадом (он переопределяет установки всех предыдущих каскадов). Добавьте внутрь блока <head> перед закрывающим тегом </head> блок <style>, как показано ниже. <style> div { border:3px solid #3a8; } h1 { color: #ffc; background-color: #900; font-size: 1.6em; margin: 0; margin-bottom: 0.5em; padding: 0.25em; text-align: center; letter-spacing: 0.08em; border-bottom-style: solid; border-bottom-width: 0.2em; border-bottom-color: #c00; } </style> Этот каскад стиля действует на все элементы <div> и <h1> данного документа. Он включается до применения каскада, заданного непосредственно в элементе. Оцените результат, обновив браузер. Внутрь нового тега <style> добавьте еще одну настройку и оцените ее влияние: body { font-family: Trebuchet MS; font-size: 80%; padding: 0px 10px; } Упражнение Временно замените тег div (разделяющая панель типа block) на тег span (панель типа inline) и обратите внимание на результат (ширина панели станет минимально возможной). Отмените последнее изменение (Ctrl+Z), то есть, верните тег <div> и переместите тег <img> внутрь тега <div>, уберите из панели текст, а также уберите все теги, кроме заголовка <h1> и панели <div> с вложенной в нее картинкой. Документ должен приобрести примерно такой вид. Заметьте, что вертикальный размер панели <div> автоматически увеличился. Добавьте в открывающий тег img атрибут id="pic". Теперь код JS сможет отыскать картинку в дереве документа. Добавим Script Внутрь тега head (перед его закрывашкой </head>) вставьте такой блок кода JS. <script> function Init() { let pic = document.querySelector("#pic"); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 let s = pic.style; s.width = "200px"; } onload = Init; </script> Оператор языка JavaScript: onload=Init; указывает, что реакцией на событие загрузки документа в окно браузера должен быть вызов функции Init(). Заметьте, что Init — это адрес функции (а не ее вызов, который имел бы вид Init()). Событие onload принадлежит главному объекту JavaScript: window. Этот объект соответствует окну браузера. Вместо onload можно использовать window.onload, но мы этого не делаем, так как объект window глобален и все остальное является его свойствами. Каждое окно браузера или часть окна типа frame имеют свой собственный глобальный объект window. В глобальном пространстве имен на него указывает this. Любая переменная var v, объявленная в глобальном пространстве имен (то есть, вне какой-либо функции или объекта), является свойством объекта window. Поэтому компилятор JS будет искать onload среди свойств объекта window. Функция Init, которую создали мы, тоже становится свойством объекта window. В браузере роль глобального объекта всегда выполняет window. Но JavaScript работает не только в браузерах. Например, он используется в среде выполнения Node.js. Там имеется свой собственный объект global. Метод querySelector объекта document является мощным средством поиска элементов в дереве HTML-документа (используется предупорядоченный обход узлов в глубину до первого найденного узла). Мы передали в querySelector текстовую строку "#pic" — селектор, указывающий, что надо найти в документе элемент с атрибутом id="pic" и возвратить ссылку на этот элемент. Селекторы могут быть сложными, например: var o = document.querySelector("div.user-panel.main input[name=login]"); В этом случае ищется объект <input name="login">, расположенный внутри панели типа div с атрибутом class="userpanel main". Если такой панели (или такого элемента) не существует, то ссылка o получит значение null. В нашем случае мы ищем "#pic" и поиск оказывается успешным. Далее, в коде Init, мы увеличили размер картинки (значение свойства pic.style.width). Это сделано с помощью ссылки pic, указывающей на объект типа img. Модель DOM Консорциум W3C выработал стандарт модели DOM (Document Object Model). Он представляет собой набор правил и рекомендаций по разработке и преобразованию XML и HTML-документов. DOM — это объектно-ориентированная иерархическая модель (ООП-представление) XML и HTML-документов. Модель строится в процессе загрузки документа в браузер. В этот момент браузер конструирует DOM, то есть, коллекцию объектов JS, поддерживающих HTMLэлементы. Каждому тегу в HTML-документе (даже таким простым, как <p>, <hr>, <div>) соответствует определенный объект DOM. В соответствии со стандартом DOM модель содержит глобальный объект window, и вложенный в него объект document. В объект window, кроме document, вложены и другие объекты, например, navigator, который содержит информацию о типе браузера. Объекты, поддерживающие вложенные элементы HTML, являются свойствами контейнера—объекта document.body. Код JS в процессе выполнения взаимодействует со всеми объектами JS, он способен также менять структуру самого дерева DOM. Перемещение картинки Зададимся целью управлять позицией картинки в окне браузера с помощью клавиатуры. Перемещать объект можно, если отказаться от автоматического размещения визуального объекта внутри контейнера. Для этого надо установить в атрибуте style тега <img ...> следующие компоненты: position, left и top. Мы сделаем это не в коде HTML, а с помощью кода JS. Добавьте в конец функции Init такой фрагмент: s.position = "absolute"; s.left = "0px"; s.top = "50px"; Для управлением координатами элемента в окне браузера значение компонента position стиля элемента должно быть равно absolute, а позицию левого верхнего угла элемента можно установить, как показано в коде выше. Проверьте результат. Картинка переехала, а панель схлопнулась, так как ей не понравилось наше решение установить абсолютную позицию вложенного в нее child-элемента <img>. В качестве упражнения вытащите картинку из панели, а саму панель удалите. Ответьте на вопрос: "Внутри какого элемента расположена теперь картинка?". После указанных настроек мы можем управлять местоположением картинки, например, с помощью клавиатуры. Добавьте в конец функции Init такой код: document.body.onkeydown = function (e) // Анонимная функция (вся, вместе с потрохами, но без имени) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { e.preventDefault(); switch (e.keyCode) { case 37: Move(-2, 0); break; case 38: Move(0, -2); break; case 39: Move(2, 0); break; case 40: Move(0, 2); break; } } function Move(dx, dy) { pic.style.left = (parseInt(pic.style.left) + dx) + "px"; pic.style.top = (parseInt(pic.style.top) + dy) + "px"; } Возня внутри функции Move вызвана тем, что left и top имеют тип string, а мы хотим изменить только часть этой строки (то есть, работать с ней как с числом — тип Number). Запустите приложение (Ctrl+F5) и, нажимая клавиши стрелок, убедитесь, что картинка плавно перемещается внутри окна. Благодаря установке position="absolute", она может даже выйти за пределы окна браузера. Для того, чтобы понять необходимость маскировки реакции браузера по умолчанию (e.preventDefault()), временно закомментируйте ее, уменьшите вертикальный размер окна браузера так, чтобы была видна лишь часть картинки (не забудьте обновить браузер), и проверьте работу страницы, нажимая стрелку вниз. Сравните поведение картинки с тем, что было ранее. Функция Move вложена внутрь функции Init. Можно ли вкладывать одну функцию внутрь другой? JavaScript это допускает. Такая возможность очень удобна и она существовала во многих старых языках (FORTRAN, PL/I, Pascal), но от нее отказались в С-подобных языках с целью повышения быстродействия. Ссылка pic определена вне функции Move, но она доступна внутри этой функции. Такая ситуация называется замыканием (closure). Переменная pic является свободной. Вложенная функция Move ее видит, то есть, замыкает вместе со своим окружением. Функция вспоминает состояние окружения (environment) в последующих вызовах. Понятие замыкание подробно описано в сети и мы скоро к нему вернемся снова. Эффект замыкания сильно упрощает код и процесс управления объектами, но он работает далеко не во всех языках программирования. Задание. Вновь добавьте в документ стилизованную панель div (картинка должна быть внутри этой панели). Добавьте в конец функции Move код, который ищет новую панель (с помощью селектора #ЗначениеID) и присваивает найденную ссылку локальной переменной div. Затем добавьте такой фрагмент: div.innerText = `x = ${pic.style.left}, y = ${pic.style.top}`; Здесь мы пытаемся вывести в панель div текущие координаты картинки. Синтаксис `текст ${местодержатель} текст` позволяет вставлять в текст произвольные объекты, которые будут приведены к типу string. Обратите внимание на символ ` (backquote, или backtick, это не апостроф, ищите его на клавише Ё). С помощью указанного синтаксиса также можно переносить длинный текст, не заботясь о сопряжении фрагментов текста (аналог символа @ в языке C#). Запуск этого варианта показывает, что при попытке изменить позицию картинки она внезапно исчезает, так как в этот момент JavaScript вдруг обнаруживает, что картинка все-таки вложена в панель div, но не может изменить размеры панели должным образом. Для исправления ситуации вынесите картинку из панели (в коде HTML), а саму панель оставьте. Вновь проверьте работу страницы. Вы должны видеть как перемещающуюся картинку, так и текст в панели div. Сама картинка теперь расположена не в панели, а прямо в теле документа (в контейнере document.body). Обработка событий Функции в JS являются объектами, их можно передавать в другие функции, возвращать из функций и присваивать их переменным. Эту особенность объектов во всех языках теперь принято обозначать термином first-class citizen. Функцию в JS часто называют first-class function (http://en.wikipedia.org/wiki/First-class_function). Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Современный JavaScript поддерживает полиморфизм, цепочный вызов серии методов, lambda-выражения, различает множество вариантов создания и использования функций: prototype functions, higher order functions, lambdas, anonymous functions, closures, privileged methods, function decorators, Immediately Invoked Function Expressions (IIFE), Stateless (Pure) Functions. Стиль программирования очень сильно изменился за последние 3-5 лет, потому, что сам язык бурно развивается (по существу, рождается заново после десятка лет неприятия). Мы применили один из вариантов создания функции для обработки события onkeydown. Он носит имя Functional Expression, так как ссылка на функцию присвоена объекту onkeydown, вложенному в объект document.body, а сама функция не имеет имени (anonymous). Повторим, что анонимная функция объявлена внутри функции Init, что допустимо в JavaScript и позволяет использовать эффект замыкания. Drag and Drop Мы рассмотрели способ перемещения визуального объекта с помощью клавиш, теперь рассмотрим, как перемещать объекты мышью с помощью технологии Drag and Drop. В полном объеме эта технология требует обрабатывать множество разных событий, возбуждаемых двумя объектами: перемещаемого источника — source, контейнера, который станет приемником перемещаемого объекта — destination. В упрощенном варианте достаточно обработать лишь три события: ondragstart — для источника, ondrop и ondragover для контейнера-приемника. Но роль приемника не может выполнять такой контейнер, как document.body. Поэтому нам придется увеличить размеры панели div, которая будет выполнять роль приемника. Добавим два компонента стиля width и height для нашей панели div. Это можно сделать двумя разными способами: В коде HTML, добавив width:1000px;height:700px; в значение атрибута style. В коде JavaScript, добавив такой код: div.style.width = "1000px"; div.style.height = "700px"; Сделайте это тем или иным способом, но не двумя сразу (сработают оба, но победит последний — код JavaScript). Для объекта-источника следует установить атрибут draggable в значение true. Это также можно выполнить двумя способами. Однако, для изображений (<img>) и гиперccылок (<a>) этот атрибут уже установлен по умолчанию. Мы собираемся перетаскивать объект img (source) и бросать ее в панели div (destinstion). Поэтому код обработки событий получит вид, показаный ниже. Вставьте код внутрь функции Init, но не любой другой функции, вложенной в Init. let x = 0, y = 0; pic.ondragstart = (e) => { x = e.x; y = e.y; } div.ondragover = (e) => { e.preventDefault(); } div.ondrop = (e) => { Move(e.x - x, e.y - y); } Для обработки событий мы используем лямбда-функции (этот способ появился в ES 6). Параметр e является ссылкой на объект, помогающий обработать событие (его тип зависит от браузера). С его помощью мы определяем текущие координаты указателя мыши. В обработчике ondrop также работает замыкание, так как переменные x и y определены вне лямбда-функции. Этот пример обнаруживает различия в обработке событий разными браузерами. Различия стараются убрать, но в данный момент (лето 2017 года) они присутствуют. Браузеры Edge и Chrome ведут себя почти одинаково, а браузер Firefox при отпускании картинки выполняет нечто, принятое им по умолчанию. Для устранения этого эффекта добавьте вызов e.preventDefault(); в начало функции обработки события ondrop. Для того, чтобы заметить различия в работе Edge и Chrome захватите картинку за ее нижний край и обратите внимание на сдвиг копии изображения, который выполняет Chrome. Этот сдвиг мешает точно указать желаемую позицию сброса (drop). Edge работает идеально. Другой, более точный, способ перемещения объектов (обработка событий мыши) мы рассмотрим ниже. Форма для ввода данных Для сбора данных из полей ввода и отправки их на сервер по умолчанию используется некая стратегия, называемая query string. Для нее обязательными являются следующие моменты: Все поля ввода должны быть вложены внутрь блока (тега) <form></form>. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Все поля ввода должны иметь атрибут name="имя эл-та" (например, name="email"). Заметьте, не id, а name. Форма должна иметь специальный элемент ввода типа submit (он имеет вид кнопки). При нажатии кнопки submit данные автоматически попадают в строку текста (query string), имеющую такой формат: ?name=Peter&email=peter%40ms.com&comment=My+comment&age=30 Текст будет добавлен в конец адресной строки браузера. Знак ? служит разделителем, он отделяет адрес от query string. Сама строка представляет собой коллекцию пар типа (ИмяЭлемента=ВведенноеЗначение). Пары разделяются символом &. Пробелы заменяются знаком +. Символ @ заменяется кодом %40. Для освоения этого способа передачи данных создадим новый файл. В папку html добавьте новую HTML-Page с именем Form.html. В тело документа (внутрь тега <body) вставьте такой код разметки. <h1>Data form</h1> <form id="f"> <label>Name: <input type="text" name="name"></label><br> <label for="email">Email: </label><input type="email" name="email" id="email"><br> <input type="submit" value="Submit" class="button"> <span id="msg" class="green"></span> </form> Запустите страницу (Ctrl+F5) и пощелкайте по меткам Name и Email. Обратите внимание на автоматический переход фокуса в связанные с метками поля ввода. Существует два разных способа связывания меток с полями ввода. Вложение тега input внутрь тега label (см. пару элементов для ввода Name). Привязка с помощью атрибутов — for (тега label) и id (тега input) (см. пару элементов для ввода Email). Первый способ проще, но он плохо стилизуется. Покажем это. Добавим перед кнопкой submit еще одно поле ввода. <label for="age">Age:</label><input type="text" name="age" id="age"><br> Запустите и убедитесь, что позиции полей ввода хочется выровнять. Попытаемся сделать это с помощью блока <style>. Вставьте внутрь блока <head> установку стилей. <style> body { font-family: Trebuchet MS; font-size: 80%; padding: 0px 10px; } label { display: inline-block; width: 70px; } .green { font-family: Comic Sans MS; color: green; } .button { border: 0; border-radius: 4px; width: 60px; color: white; opacity: 0.7; padding: 3px 5px; text-align: center; background-color: #282; transition-duration: 0.4s; box-shadow: 0 2px 6px 0 rgba(0,0,0,0.24),0 7px 10px 0 rgba(0,0,0,0.19); text-decoration: none; display: inline-block; font-size: 0.9em; cursor: pointer; } .button:hover { opacity: 1; transform: scale(1.04); } .button[disabled] { background-color: #ddd; } </style> Обратите внимание на стиль, применяемый ко всем элементам типа label. Установка ширины width работает для inline-элементов типа label и span только в сочетании с парой display: inline-block; и этот трюк работает для пары (label-input), если они связаны с помощью атрибутов for и id. Именованный стиль .green будет применен к любому элементу, помеченному атрибутом class="green". Мы пометили таким образом панель <span>, которая будет использована для вывода сообщений об ошибках ввода. Класс .button используется для стилизации кнопки submit. Исправьте способ привязки первой метки к ее полю ввода, и добавьте еще один элемент <input> для ввода данных. <label for="cm">Comment:</label><textarea placeholder="Your comment..." name="cm" id="cm"></textarea><br> Добавьте в блок style такой текст: input, textarea { margin: 2px 0px; width: 200px; } Здесь установлены вертикальные и горизонтальные отступы между полями ввода. Скопируйте и вставьте стиль заголовка <h1> из первой страницы. После этого страница должна приобрести такой вид: Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Реакция на ввод данных на стороне клиента Покажем, как проверить вводимые данные до отправки их на сервер и вывести результат проверки в панель <span> с идентификатором msg. В блок <head> добавьте блок <script>, а внутрь него вставьте код: function Init() { let btn = document.querySelector("input[type=submit]"); btn.onclick = Validate; } function Validate() { msg.innerHTML = ""; let o = null; if (f.name.value.length < 3) // Добываем значение поля для ввода имени { o = f.name; msg.innerHTML += "<br>Name is required"; } if (o) // Если ссылка на ошибочное поле не пуста { o.focus(); o.select(); return false; // Не отправлять данные на сервер } return true; // Отправлять данные можно } onload = Init; Ссылка на кнопку submit добывается с помощью хитрого выражения, поданного на вход querySelector. Переменная f ссылается на форму потому, что ей был присвоен атрибут id="f". Эту ссылку можно было бы получить более сложным образом: let f = document.querySelector("form"). В таком случае форма могла бы и не иметь атрибут id. То же можно сказать про ссылку msg. В функции Validate проверяется текст только одного поля name. Если проверка не прошла, то выводится сообщение и данные не отправляются на сервер (см. return false). Если проверка прошла, то данные передаются на сервер методом get. Метод get по умолчанию установлен для нашей формы. Его суть в том, что данные передаются с помощью адресной строки. Введите какое-нибудь имя (например, Alex) и email (например, a@a.a), нажмите кнопку Submit и обратите внимание на адресную строку браузера. Информационная часть этой строки (?name=Alex&email=a%40a.a) называется query string. На нее ссылается свойство location.search объекта document. Чуть позже мы покажем, как добыть это свойство в коде другой страницы, а сейчас покажем логику проверки числовых данных на примере проверки данных поля возраста. Произведем три проверки: Поле не должно быть пустым. Введенное значение должно быть числом. Введенное число должно быть в диапазоне (10, 100). function TestAge() { let age = f.age.value; if (age == "") { msg.innerHTML += "<br>Please enter your age"; return false; } if (isNaN(age)) // NaN означает 'Not a Number' { msg.innerHTML += "<br>Age syntax is wrong"; return false; } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 age = Number(age); // Текст преобразуется в число (если получится) if (age < 10 || 100 < age) { msg.innerHTML += "<br>Age must be a number within (10, 100)"; return false; } return true; } Добавьте в тело Validate фрагмент, который вызывает функцию TestAge. if (!TestAge()) o = f.age; Добавьте код проверки данных других полей ввода. Например, проверку поля email можно осуществить с помощью встроенного объекта RegExp. function TestEmail() { return new RegExp("^[A-Za-z_]+(\\.[A-Z0-9a-z_]+){0,2}@([A-Za-z_]+\\.){1,3}[A-Za-z]+$", "i") .test(f.email.value); } Литерал регулярного выражения (имеющий вид: /шаблон/) подойдет и для проверки поля comment. Например, мы ставим условие: длина комментария не должна быть менее 10 символов и он не должен начинаться со слова Your. function TestComment() { let s = f.cm.value, big = s !== "" && s.match(/\w\D+/g)[0].length > 9, matches = /^(Your)\W+/.exec(s); return big && matches === null; } Выражения: /\w\D+/ и /^(Your)\W+/ являются литералами регулярных выражений. Задание. Добавьте указанные выше функции в блок script и вызовите их в коде функции Validate. Убедитесь, что скрипт не отправляет данные формы на сервер, пока не исправлены все ошибки ввода. Порядок проверки полей важен. Добейтесь того, чтобы исправления требовались по одному, в порядке следования полей ввода (сверху-вниз). Создание библиотеки инструментов Опытные программисты заводят арсенал своих собственных инструментов, ускоряющих процесс разработки кода приложений. Мы намеренно не будем пользоваться услугами библиотек (JQuery, и т. п.), так как наша цель — научиться самостоятельно разрабатывать полезные функции и объекты JS. Добавим глобальную функцию с именем $ и класс с именем O, которые упростят нашу жизнь. В папку js добавьте новый файл с именем Kit.js (здесь будет наша библиотека) и введите в него следующий код. var $ = function (name, s, p, t, n) { let msg = ""; if (!document.body) { msg = "Use 'Kit' after the document creation"; alert(msg); throw new Error(msg); } let e = new O(name, s, p, t, n).o; if (e.constructor.name == "HTMLUnknownElement") { msg = `"${name}" is a "HTMLUnknownElement"`; alert(msg); throw new Error(msg); } return e; }; class O { constructor(name, s, p, t, n=0) { t = t || document.body; if (t) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { if (n) t.insertAdjacentHTML("beforeEnd", "<br>".repeat(n)); this.o = document.createElement(name); if (p) this.add(this.o, p); if (s) this.add(this.o.style, s); t.appendChild(this.o); } } add(o, text) { let pp = text.split(";"); // name-value pairs for (let i = 0; i < pp.length; i++) { let pair = pp[i]; if (pair === "") continue; let p = pair.split("="), len = p.length; if (len > 0) { let n = p[0].trim(); o[n] = len == 2 ? p[1].trim() : n; } } } } Функция $ позволяет на лету создать и вставить произвольный HTML-элемент внутрь любого другого элемента контейнерного типа (такими являются: document.body, div, span, form). Внутрь означает — в конец коллекции childэлементов родительского элемента-контейнера (см. вызов t.appendChild(this.o)). Популярная библиотека JQuery тоже имеет функцию по имени $, в которой содержится большой объем кода JS. Если вы подключите JQuery произойдет конфликт имен. Параметрами нашей функции $ являются: name — имя HTML элемента, который следует создать и вставить в указанный контейнер. s — коллекция пар, определяющих стиль создаваемого объекта. p — коллекция пар, определяющих свойства этого объекта. t — ссылка на контейнер, внутрь которого следует вставить новый объект (target). n — количество переводов строки (тегов <br>), которые следует вставить перед новым объектом. Функция $ возвращает ссылку на public-поле o вновь созданного объекта класса O. Вновь генерируемый HTMLэлемент создается в конструкторе класса по имени O. Это действие выполняет оператор: this.o = document.createElement(name); Ссылка this говорит о том, что переменная o является не локальной переменной, действующей внутри конструктора, а public-полем вновь созданного объекта класса O. Метод add(o, text) класса O производит синтаксический анализ (parsing) текстовой строки, выуживает из нее пары вида (key, value) и применяет их в качестве свойств нового элемента или в качестве компонентов его стиля. Это возможно благодаря тому, что свойства HTML-элемента, а также компоненты его стиля, являются коллекциями вида хеш-таблица, т.е. множеством пар (key, value). Первый вызов метода задает свойства вновь созданному элементу, а второй вызов задает компоненты его стиля, так как параметр o содержит ссылку на this.o.style нового элемента. Далее, мы будем постепенно расширять функциональность класса O, добавляя в него статические методы. Для проверки функционирования нового инструментария добавьте в папку html копию файла Form.html, переименуйте ее в FormJS.html, удалите код разметки (все внутреннее содержимое тега <body>) документа и добавьте в блок <head> код JavaScript, как показано ниже. Перед существующим блоком <script> вставьте ссылку на файл с инструментами Kit. <script src="../js/Kit.js"></script> В начало существующего блока <script> вставьте объявление глобальных переменных: var f = null, msg = null; // Глобальная ссылка на форму и панель для вывода сообщений Полностью замените содержимое функции Init. function Init() Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { $("h1", 0, "innerText=Data form"); f = $("form", 0, "action=SplitQuery.html"); $("label", 0, "htmlFor=name;innerText=Name:", f); $("input", 0, "type=text;name=name;id=name;autofocus", f); $("label", 0, "htmlFor=age;innerText=Age:", f, 1); $("input", 0, "type=text;name=age;id=age", f); $("label", 0, "htmlFor=cm;innerText=Comment:", f, 1); $("textarea", 0, "placeholder=Your comment...;name=cm;id=cm", f); $("label", 0, "htmlFor=email;innerText=Email:", f, 1); $("input", 0, "type=email;name=email;id=email", f); $("label", 0, "htmlFor=url;innerText=URL:", f, 1); let url = $("input", 0, "type=url;name=url;id=url;value=http:/", f); url.onfocus = () => { url.selectionStart = url.value.length; } $("input", 0, "type=submit;className=button;", f, 1).onclick = Validate; msg = $("span", 0, "className=green"); } Убедитесь, что новая страница FormJS работает почти так же, как и старая — Form. Но, самым важным моментом является то, что произошел качественный скачок. Мы перестали быть дизайнерами. Тело документа при загрузке в браузер пусто (<body></body>). Теперь правит балом наш скрипт, который населяет документ элементами HTML в процессе выполнения функции Init. Заметьте, что добавлено новое поле ввода типа url, текст которого опекается браузером. Добавлен атрибут autofocus, который может и не иметь вид: key=value. Он может быть либо: autofocus, либо autofocus=autofocus; Наш Kit использует второй способ. Для отмены автофокуса не работает код вида o.autofocus=false;. Вместо этого придется использовать оператор: o.removeAtribute("autofocus");. The values "true" and "false" are not allowed on boolean attributes. To represent a false value, the attribute has to be omitted altogether. Приведем еще несколько boolean attributes, которые имеют сходное поведение. Это: selected, checked, disabled, autoplay, controls. Как обработать данные query string Для того, чтобы направить данные формы в другую страницу, надо установить атрибут action для самой формы. По умолчанию этот атрибут содержит URL своей же страницы. Поэтому при нажатии кнопки submit данные отправляются на сервер, который повторно посылает в браузер (round trip) тот же самый документ, не заполняя поля данными — сервер по умолчанию не желает помнить введенные данные. Отличие состоит в том, что теперь мы видим данные в адресной строке браузера. Мы хотим изменить алгоритм и передать данные странице, которая отобразит их в аккуратной таблице. Убедитесь, что в странице FormJS атрибут action уже добавлен (в коде JS) и он указывает на страницу: action=SplitQuery.html, которую предстоит разработать. Добавьте в папку html проекта новую страницу SplitQuery.html. В скрипте, запускаемом в момент загрузки страницы SplitQuery, мы собираемся произвести синтаксический анализ (parsing) адресной строки, вытащить из нее queryString, выудить из queryString все пары типа key=value и вывести значения этих пар в таблицу. Ниже приведен код страницы SplitQuery.html. <!DOCTYPE html> <html> <head> <title>Splitting Query String</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> body { font-family: Arial; font-size: 90%; padding: 0px 10px; } table { margin-bottom: 0.2em; border-collapse:collapse; } td { border: 1px solid #ccc; padding: 5px; } th { padding: 5px; background-color: #555; color: #6f6; text-align: center; font-weight: bold; } .button { border: 0; border-radius: 4px; width: 60px; color: white; opacity: 0.7; padding: 3px 5px; text-align: center; background-color: #282; transition-duration: 0.4s; box-shadow: 0 2px 6px 0 rgba(0,0,0,0.24),0 7px 10px 0 rgba(0,0,0,0.19); text-decoration: none; display: inline-block; font-size: 0.9em; cursor: pointer; } .button:hover { opacity: 1; transform: scale(1.04); } </style> <script src="../js/Kit.js"></script> <script> function Parse() { let form = $("form"), sp = $("span", 0, 0, form), qs = document.location.search, // Query String page = document.referrer; if (page) { let btn = $("input", 0, "type=submit;value=Go Back;className=button;"); btn.onclick = () => window.location = page; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 } if (qs.length == 0) { sp.innerHTML = "This page parses OueryString<br>OueryString is empty."; return; } let htm = "<h3>Query parameters</h3><table><tr><th><b>Key</b></th><th><b>Value</b></th></tr>", pp = qs.substr(1).split("&"); for (let i = 0; i < pp.length; i++) { let p = pp[i].split("="), key = unescape(p[0]), val = unescape(p[1].replace(/\+/g, " ")); htm += `<tr><td>${key}</td><td>${val}</td></tr>`; } htm += "</table>"; sp.innerHTML = htm; } onload = Parse; </script> </head> <body></body> </html> По завершении загрузки документа в браузер управление передается в функцию Parse ( onload=Parse). Выражение document.location.search; выделяет queryString (ту часть адресной строки, которая содержит переданные данные). Вызов метода substr(1) отрывает разделитель (знак вопроса), который присутствует в начале queryString. Вызов split("&") разбивает ее на пары вида key=value. Далее выделяются элементы key, value каждой пары (см. следующий вызов split("=")) и эти данные помещаются в ячейки таблицы, которая имеет вид текста (текстовой строки htm). Текст htm превращается в реальную таблицу (см. код: sp.innerHTML=htm;) В процессе сбора данных формы текст queryString претерпевает изменения. "Joe Doe" превращается в "Joe+Doe", служебные символы заменяются их кодами (например, '@' превратится в %40, а '#' в %23). На приемной странице (SplitQuery) необходимо произвести обратные преобразования. Их выполняют операторы вида: p[1].replace(/\+/g, " ") — заменяет символ '+' пробелом, unescape(val); — заменяет коды символов на сами символы. Важную роль в рассматриваемом сценарии играет текстовая строка document.referrer. Она содержит адрес (URI) той страницы (FormJS), которая сослалась на данную (SplitQuery). Строка document.referrer может быть пустой, если клиент обратился к странице SplitQuery напрямую. Но если переход к SplitQuery произошел из другой страницы, то мы имеем возможность вернуться к исходному документу (referrer), что мы и делаем в реакции на нажатие кнопки с надписью Go Back. К сожалению, при этом мы видим пустые поля ввода и отсутствие queryString, так как сервер ничего не желает помнить. Попутно заметим, что ссылка: <link rel="icon" type="image/png" href="../images/favicon.png" /> работает во всех браузерах, но жалуется на ее отсутствие в режиме отладки только Chrome. Имя favicon означает "любимая иконка", а саму ее вы должны видеть в закладке страницы. Добавьте такие же ссылки на favicon во все другие страницы. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 2. Примеры Эта глава содержит минимум пояснений. Вам следует подключить все примеры к проекту и поиграть с ними, пытаясь понять их работу на уровне интуиции. Понять детали происходящего, помогут лекции и материал главы 3. Расширение возможностей Kit Добавим в класс O статические методы, которые будут играть роль глобальных функций. static getHtml(o) { return o.innerHTML; } static html(text, to) { if (!to) to = document.body; if (to) to.insertAdjacentHTML("beforeEnd", text); } static query(s) { return document.querySelector(s); } static addText(text, to) { let o = document.createTextNode(text); if (!to) to = document.body; to.appendChild(o); return o; } static getAll(o) { let s = "["; for (let p in o) s += o[p] + ", "; s = s.substr(0, s.length - 2); s += "]"; return s; } При управлении структурой DOM очень часто возникает необходимость отыскать какой-нибудь элемент и получить на него ссылку. Для этого используют метод getElementById объекта document или более современный — querySelector. Чтобы упростить код, мы добавили библиотеку Kit метод query. Теперь вместо: document.getElementById("myImg") можно сделать вызов O.query("#myImg"). Чтобы получить ссылку на первый элемент типа input, можно вызвать O.query("input"), а отыскать элемент с атрибутом class="red" можно так: O.query(".red"). Методы addText(text, to) и html(text, to) позволяют вставить текст (или HTML-контент) внутрь произвольного элемента контейнерного типа, на который ссылается переменная to. Функция getAll(o) вытаскивает все свойства объекта, переданного параметром и возвращает их в виде строки текста. Calculator Этот пример состоит из двух html-файлов: Calculator и SplitQuery (уже существует). Первый файл демонстрирует технику вычислений на основе данных, введенных пользователем. Затем данные передаются странице SplitQuery. <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Calculator</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> body { font-family: Trebuchet MS; font-size: 16px; padding: 0; margin: 0; } h1 { color: #ffc; background-color: #29e; font-size: 1.5em; margin: 0; margin-bottom: 0.05em; padding: 0.25em; text-align: center; letter-spacing: 0.1em; border-bottom-style: solid; border-bottom-width: 0.1em; border-bottom-color: #28a; } td { padding: 5px; background-color: #fff; } .button { border: 0; border-radius: 8px; color: white; opacity: 0.7; padding: 5px 22px; text-align: center; background-color: #282; transition-duration: 0.4s; box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19); text-decoration: none; display: inline-block; font-size: 1.1em; margin: 4px 4px; cursor: pointer; } .button:hover { opacity: 1; transform: scale(1.04); } .button[disabled] { background-color: #ddd; } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 #profit { text-align: center; border: none; color: red; } </style> <script src="../js/Kit.js"></script> <script> class Calculator { constructor() { $("h1", 0, "innerText=Bank Interest Calculator;"); let f = $("form", 0, "action=SplitQuery.html;"); f.onsubmit = this.Calc; let table = $("table", 0, 0, f), tr = $("tr", 0, 0, table); $("td", 0, "innerText=Principal;", tr); $("input", 0, "type=text;name=principal;value=10000;", $("td", 0, 0, tr)); tr = $("tr", 0, 0, table); $("td", 0, "innerText=Interest Rate;", tr); $("input", 0, "type=text;name=rate;value=10%;", $("td", 0, 0, tr)); tr = $("tr", 0, 0, table); $("td", 0, "innerText=Months;", tr); $("input", 0, "type=text;name=months;value=12;", $("td", 0, 0, tr)); let btn = $("input", 0, "type=button;value=Calculate;name=calc;className=button;", f); btn.onclick = () => this.Calc.call(f); $("input", "background=#f56;", "type=submit;value=Submit;className=button;", f); $("input", 0, "type=text;readonly=readonly;size=20;name=profit;id=profit;", f); } Calc() { let p = parseFloat(this.principal.value), r = parseFloat(this.rate.value) / 1200, m = parseFloat(this.months.value), a = this.profit, profit = 0; if (p > 0 && r > 0 && m > 0) profit = p * (Math.pow(1 + r, m) - 1); profit = "Your profit: " + profit.toFixed(2); a.style.textDecoration = "underline"; a.value = profit; } } onload = () => new Calculator(); </script> </head> <body></body> </html> Класс Calculator содержит constructor, который создает структуру DOM, и метод Calc, который выбирает данные из элементов формы и производит вычисление сложного банковского процента. Первая кнопка (input type=button) выводит результат вычислений в текстовое поле, Вторая (input type=submit) запускает механизм передачи данных в другой документ. Важно понять, что внутри конструктора ссылка this указывает на объект Calculator, а внутри метода Calc ссылка this указывает на объект form, благодаря особой технике обработке события onclick. Метод Calc вызывается в ответ на два разных события: Событие onclick для кнопки btn (объект типа input type="button"), Событие onsubmit для формы f (объект типа form). Мы хотим, чтобы в обоих случаях ссылка this в методе Calc указывала на объект form, так как именно форма нужна для того, чтобы выудить данные, введенные пользователем в поля ввода (которые вложены в форму). Поэтому привязка к событию onsubmit для объекта f, ссылающегося на форму, выглядит просто: f.onsubmit = this.Calc; // Внутри Calc роль this будет играть форма f, так как именно для нее задан обработчик события. Привязка же к событию onclick кнопки btn выглядит более замысловато: btn.onclick = () => Calculator.Calc.call(f); Этот трюк позволяет подменить аргумент this. Если бы мы использовали обычный способ привязки события к функции (btn.onclick = this.Calc;), то внутри метода Calc ссылка this показывала бы на кнопку btn, которая возбудила событие. Если бы мы использовали для привязки события лямбда-функцию (btn.onclick = () => this.Calc()), то внутри Calc Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 ссылка this показывала бы на объект класса Calculator. Нам же удобно, чтобы внутри Calc ссылка this показывала бы на форму. Поэтому привязка имеет вид: btn.onclick = () => Calculator.Calc.call(f);. Метод call объекта Function позволяет в качестве первого параметра указать ссылку на произвольный объект. Внутри функции, вызываемой с помощью call, ссылка this будет указывать именно на этот объект. Обратите внимание также на то, что метод передачи данных формы (GET, а не POST) задан по умолчанию. Поэтому данные формы пакуются в коллекцию queryString. Второй способ передачи данных включается с помощью атрибута формы method="post". Он рассмотрен в документе Introduction to Razor Pages (при этом способе данные попадают в коллекцию Request.Form). Обсудим код SplitQuery.html. Свойство document.URL позволяет получить полный текст адресной строки. В этом тексте, кроме адреса страницы, содержатся данные полей той формы, которая передала управление в текущую страницу. Для того, чтобы их выудить, надо сначала выделить вторую часть адресной строки (queryString). Это можно сделать с помощью метода split, но проще воспользоваться свойством document.location.search. Оно содержит только queryString. В нашем случае queryString имеет вид: ?principal=10000&rate=10%25&months=12&profit=Your+profit%3A+1047.13 Процесс разбивки этого текста на пары key=value и последующее выделение элементов пар был описан ранее. Balls Пример Balls.html (Воздушные шары) нельзя назвать полезным, но он содержит все те сущности, которые хочется видеть в объектно-ориентированном подходе к разработке кода JavaScript. Класс Ball содержит методы управления объектами своего типа. Класс Game управляет коллекцией объектов типа Ball. В класс добавлены методы, реагирующие на события мыши и таймера (решена проблема this). Имеется DOM-элемент canvas и показаны способы управления его свойствами. Работает нетривиальная коллекция стилей CSS. Ниже будет показано, что на самом деле JavaScript превращает любой класс в глобальную функцию. Все методы класса выносятся в специальный объект, называемый прототипом функции. В нашем случае — Ball.prototype. <!DOCTYPE html> <html> <head> <title>Balls</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style type="text/css"> body { font-family: Trebuchet MS; padding: 1em; } span { color: red; font-weight: bold; font-size: large; margin-left: 4em; } .button { border: 0; border-radius: 8px; color: white; padding: 10px 22px; text-align: center; backgroundcolor: #f44; transition-duration: 0.4s; box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19); opacity: 0.7; text-decoration: none; display: inline-block; font-size: 1.1em; margin: 4px 4px; cursor: pointer; } .button:hover { opacity: 1; transform: scale(1.1); } </style> <script src="../js/Kit.js"></script> <script> class Ball { constructor(w, h) { this.rad = 6; this.min = this.rad; this.max = 32; this.maxX = w - this.rad; this.maxY = h - this.rad; this.grow = true; this.pi2 = 2 * Math.PI; this.x = this.Rand(32, w - 32); this.y = this.Rand(32, h - 32); } Rand(min, max) { return Math.round(min + Math.random() * (max - min)); } RandClr() { var c = '#'; for (let i = 0; i < 3; i++) c += "0123456789abcdef"[this.Rand(0, 15)]; return c; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 } Move() { this.x += (Math.random() >= 0.5 ? 1 : -1); this.y += (Math.random() >= 0.5 ? 1 : -1); if (this.x < 0) this.x += 2; else if (this.x > this.maxX) this.x -= 2; if (this.y < 0) this.y += 2; else if (this.y > this.maxY) this.y -= 2; this.rad += this.grow ? 0.75 : -0.75; if (this.rad < this.min || this.max < this.rad) this.grow = !this.grow; } Draw(dc) { this.Move(); dc.beginPath(); dc.arc(this.x, this.y, this.rad, 0, this.pi2); dc.closePath(); dc.fillStyle = this.RandClr(); dc.fill(); dc.lineWidth = this.rad * 0.1; dc.strokeStyle = "#030"; dc.stroke(); } } class Game { constructor() { this.timerMove = null; this.timerStop = null; this.ballsCount = 7; this.balls = new Array(this.ballsCount); this.dc = null; this.w = 800; this.h = 400; this.tick = 70; this.score = 0; this.msg = null; this.btnNew = null; this.SetHtml(); this.Start(); } SetHtml() { let canvas = $("canvas", "border=1px solid #aaa;", `width=${this.w};height=${this.h}`); $("br"); this.dc = canvas.getContext("2d"); this.dc.font = "25px Trebuchet MS"; this.SetBalls(); this.btnNew = $("input", null, "type=button;value=New Game;className=button;"); this.btnNew.onclick = () => this.Reset(); this.msg = $("span"); canvas.onmousedown = (e) => this.OnMouseDown(e); } Reset() { clearTimeout(this.timerStop); clearInterval(this.timerMove); this.msg.innerText = ""; this.score = 0; this.balls.length = 0; this.SetBalls(); this.Start(); } OnMouseDown(e) { if (this.balls.length === 0) this.Stop(); let x = e.x - e.target.offsetLeft, y = e.y - e.target.offsetTop; for (let i = 0; i < this.balls.length; i++) { Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 let b = this.balls[i]; if (Math.abs(x - b.x) < b.rad && Math.abs(y - b.y) < b.rad) { this.balls.splice(i, 1); this.score++; e.stopPropagation(); e.preventDefault(); return; } } } SetBalls() { for (let b, i = 0; i < this.ballsCount; i++) { this.balls[i] = b = new Ball(this.w, this.h); b.Draw(this.dc); } } Start() { this.timerMove = setInterval(() => { this.dc.clearRect(0, 0, this.w, this.h); this.dc.fillStyle = "#009"; this.dc.fillText("Test your agility", 300, 30); this.balls.forEach((b) => b.Draw.call(b, this.dc)); }, this.tick); this.timerStop = setTimeout(() => this.Stop(), this.ballsCount * 650); } Stop() { clearInterval(this.timerMove); this.msg.innerText = `Result: ${Math.round(this.score * 100 / this.ballsCount)}%`; } } onload = () => new Game(); </script> </head> <body></body> </html> Новые элементы HTML5 В этом примере мы не пользуемся библиотекой Kit.js, так как хотим показать возможности новых HTML-элементов, которые появились пару лет назад в связи с приходом стандарта HTML5 (он продолжает развиваться). <!DOCTYPE html> <html> <head> <title>FormElements</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> body { font-family: Trebuchet MS; font-size: 80%; padding: 0px 10px; } h1 { color: #ffc; background-color: #900; font-size: 1.6em; margin: 0; margin-bottom: 0.5em; padding: 0.25em; text-align: center; letter-spacing: 0.08em; border-bottom-style: solid; border-bottom-width: 0.2em; border-bottom-color: #c00; } fieldset { width: 22em; } label, meter, progress, fieldset, input { margin: 4px; } input[type=file] { display:none; } input[type=range], progress, meter { width: 75px; vertical-align: middle; } input:invalid { box-shadow: 0 0 5px 1px red; } input:focus:invalid { outline: none; } .w80 { display: inline-block; width: 80px; } .aside { display: inline; position: absolute; left: 30em; top: 54px; } .green { font-family: Comic Sans MS; color: green; } .red { background-color: #b00 !important; } .button { border: 0; border-radius: 4px; color: white; opacity: 0.7; padding: 3px 5px; text-align: center; background-color: #282; transition-duration: 0.4s; box-shadow: 0 2px 6px 0 rgba(0,0,0,0.24),0 7px 10px 0 rgba(0,0,0,0.19); textdecoration: none; display: inline-block; font-size: 0.9em; cursor: pointer; margin:4px;} .button:hover { opacity: 1; transform: scale(1.04); } .button[disabled] { background-color: #ddd; } .close { float: right; font-size: 10px; font-weight: bold; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; opacity:.6; position:relative; top:-8px; } .alert { padding: 8px 0px 8px 8px; margin-bottom: 20px; border: 2px solid #ecc; border-radius: 4px; width:20em; } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 #img { width: 3em; display: inline; vertical-align:middle; } </style> <script> function Init() { let state = 0, timer = null; sr.textContent = range.value; sm.textContent = meter.value; sp.textContent = progress.value; color.value = "#000000"; sc.textContent = color.value; range.oninput = () => { sr.textContent = range.value; meter.value = 10 + 9 * range.value; sm.textContent = meter.value; let v = parseInt(range.value * 1.5).toString(16); color.value = `#00${v}${v}00`; sc.textContent = color.value; if ((state = state == 0 ? 1 : 2) == 1) { timer = setInterval(() => { progress.value++; sp.textContent = progress.value; if (progress.value > 99) { clearInterval(timer); sp.textContent = (progress.value = 0); state = 0; } }, 30); } } file.onchange = () => { let f = file.files[0].name; sf.textContent = f; img.src = `../images/${f}`; } color.onchange = () => sc.textContent = color.value; reset.onclick = () => { sc.textContent = color.value = "#000000"; }; btnClose.onclick = () => divMsg.style.display = "none"; } onload = Init; </script> </head> <body> <h1>Form Elements</h1> <div id="divMsg" class="alert"> <button type="button" id="btnClose" class="close" value="Close">&times;</button> <span id="msg" class="green">Please, fill in this form</span> </div> <form id="f" action="SplitQuery.html"> <div> <span class="w80" title="This is a 'must' field">Language:*</span> <input list="lang" name="lang" required autofocus maxlength="15" placeholder="Start typing (c, p or c)..."> <datalist id="lang"> <option>C++</option> <option>C#</option> <option>Caml</option> <option>Pascal</option> <option>Python</option> <option>Perl</option> <option>Ruby</option> <option>R++</option> </datalist><br> <span class="w80">Experience:</span> <input type="number" name="ex" id="ex" min="2" max="30" requried step="2"><br> <span class="w80" title="You must set the time">Set Time:*</span><input type="time" name="time" id="time" required> <fieldset> <legend>What is your favorite language?</legend> <input type="radio" name="lg" value="C++" checked> C++<br> <input type="radio" name="lg" value="Python"> Python<br> <input type="radio" name="lg" value="C#"> C# </fieldset> Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 <fieldset> <legend>Select server technologies?</legend> <input type="checkbox" checked name="ch" value="ASP.NET MVC">ASP.NET MVC<br> <input type="checkbox" name="ch" value="Django">Django (Python)<br> <input type="checkbox" name="ch" value="Rails">Ruby on Rails </fieldset> </div> <div class="aside"> <span class="w80">Set Date:</span><input type="datetime-local" name="dt" id="dt"><br> <span class="w80">Set Month:</span><input type="month" name="month" id="month"><br> <span class="w80">Set Week:</span><input type="week" name="week" id="week"><br> <fieldset> <legend>Advanced Widgets</legend> <span class="w80">Site Rating:</span><input type="range" name="range" id="range" min="0" max="10" step="1" value="0"> <span id="sr"></span><br> <span class="w80">Disk usage:</span><meter id="meter" min="0" max="100" value="10" low="60" high="80" optimum="50">0</meter> <span id="sm"></span><br> <span class="w80">Progress:</span><progress max="100" value="0" id="progress"></progress> <span id="sp"></span><br> <span class="w80">Color:</span><input type="color" name="color" id="color"> <span id="sc"></span><br> <label>Image: <mark id="sf">Browse</mark><input type="file" name="file" id="file" accept="image/*" multiple></label> <img id="img"><br> </fieldset> <fieldset> <legend>DIY<abbr title="Do-it-yourself">*</abbr></legend> <input type="radio" required name="title" id="r1" value="Mr"> Mr. <input type="radio" required name="title" id="r2" value="Ms"> Ms. </fieldset> </div> <button type="reset" class="red button" id="reset"> This a <br><strong>'Reset' button</strong> </button><br> <input type="submit" value="Submit" class="button"> </form> </body> </html> Советую обратить внимание на новые типа элемента <input>, такие как: range, color, number, date, month, week, а также на элементы: meter, progress, mark. Лучшую справку по этим элементам вы получите здесь. Внешний шаблон стилей Множество каскадных стилей, применяемых ко всем элементам Web-страницы, можно описать с помощью локального (embedded) блока <style></style> (в заголовке документа), но часто используется другой способ управления обликом документа — внешний шаблон стилей. Добавьте в проект папку css, а в нее вставьте новый файл с помощью команды: Add New Item-Style Sheet, выбранной в контекстном меню, задайте имя файла Styles.css и введите в файл желаемое множество стилей, например: body { font-family: Trebuchet MS, Verdana, sans-serif; font-size: 90%; margin: 20px; } input, select, textarea { margin: 4px; width: 27em; } label { display: inline-block; width: 80px; } h1 { color: #ffc; background-color: #900; font-size: 1.6em; margin: 0; margin-bottom: 0.5em; padding: 0.25em; text-align: center; letter-spacing: 0.08em; border-bottom-style: solid; border-bottom-width: 0.2em; borderbottom-color: #c00; } h2 { font-family:"Trebuchet MS"; text-align:center; } li { color: #27c; cursor: pointer; } img { margin: 2px; vertical-align:middle; } table { border: 1px solid; border-color: #aaa; } th { background-color: #555; color: #2f0; text-align: center; padding: 4px; } th a { background-color: #555; color: #cf0; padding: 3px; } td { padding: 4px 5px; } a { background-color: #389; color: white; padding: 4px; text-align: center; border-radius: 4px; margin: 2px; } a:hover { opacity: 0.8; } .link { background-color: white; color: #267; padding: 0px } .w6 { display: inline-block; width: 6em; } .w9 { display: inline-block; width: 9em; } .w34 { display: inline-block; width: 34em; } .leftShit { margin-left: 5em; } .center { text-align: center; } .red { font-style: oblique; color: red; } .green { font-style: oblique; color: green; } .redBk { background-color: #e22; } .greenBk { background-color: #389; } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 .hidden { display: none; } .inl { display: inline; } .disable { pointer-events: none; background-color: #bbb; user-select: none; } .biege { background-color: #fff9f5; border-style: solid; border-color: #bbb; margin: 8px 4px; padding: 2px 8px; width: 33em; } .button { padding: 4px; border: 0; color: white; display: inline-block; cursor: pointer; border-radius: 4px; } .button:hover { opacity: 0.8; } .footer { position: absolute; bottom: 0; width: 100%; white-space: nowrap; height: 60px; line-height: 60px; } .simple { border:1px solid black; width:160px; text-align:center; } Этот файл переопределяет стили стандартных элементов HTML (body, img. и т. д.) и создает новые именованные стили (.red, .simple). Аббревиатура CSS (Cascading-Style Sheets) обозначает каскадную модель образования таблицы стилей, благодаря которой отдельные атрибуты стиля можно добавлять в коллекцию, удалять их из нее и изменять независимо друг от друга. Накопив множество CSS-файлов, мы можем быстро и эффективно изменять облик любого документа сайта. Можно копировать шаблоны или CSS-файлы с шаблонами из понравившихся страниц Internet. Термин адаптивный Web-дизайн обозначает стратегию по выбору таблицы стилей в зависимости от размера экрана. Например, при просмотре страницы на смартфоне текст отображается в одной колонке с крупными гиперссылками. Если эта же страница просматривается на компьютере с обычным монитором, то текст принимает другой облик (несколько колонок с обычными гиперссылками). Секрет состоит в выборе другой таблицы стилей. Например, @media (min-height: 680px), screen and (orientation: portrait) { /* Здесь другая таблица стилей*/ } Zoo Zoo.html демонстрирует механизм наследования, описанный ниже (в главе 3). В примере также показано, как добавить аудио, как перемещать объекты с помощью мыши, как создавать аналоги всплывающих окон (по умолчанию они запрещены). Роль всплывающего окна выполняет панель (span). Добавьте в папку sounds mp3-файлы: Lion, Cougar, Elephant. <html> <head> <title>Zoo</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <link href="../css/Styles.css" rel="stylesheet" /> <script src="../js/Kit.js"></script> <script> class Panel { constructor() { this.player = $("audio", "visibility=hidden;", "id=player;"); this.div = $("div", "background-color=#ffe; border=1px solid #bbb;" + "width=400px;height=300px;left=20px;top=20px;"); this.msg = $("span", "font-size=1.2em;color=blue;position=absolute;top=278px;left=120px;" + "padding-left=10px;padding-top=10px;", 0, this.div); this.animal = null; this.pop = null; $("span", "font-size=18pt;color=red;padding-left=10px;padding-top=10px;", "innerText=My Zoo (click, drag, press space);", this.div); } Say(file) { this.player.src = file; this.player.play(); } RemovePopup() { if (this.pop) { this.pop.parentNode.removeChild(this.pop); this.pop = null; } } Popup(name, style) { this.RemovePopup(); let x = parseInt(style.left), y = parseInt(style.top) + 82, s = `padding=6px;border=4px solid #aa3;backgroundColor=#ffa;fontFamily=Trebuchet MS; textAlign=center;position=absolute; top=${y}px;left=${x}px;width=77px;`; this.pop = $("span", s, `innerText=I am ${name}`, this.div); } } class Animal { Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 constructor(name, x, y) { this.name = name; this.x = x; this.y = y; this.imgSrc = ""; this.sndSrc = ""; this.img = null; } Set(panel, name) { this.panel = panel; this.imgSrc = `../images/${name}.jpg`; this.sndSrc = `../sounds/${name}.mp3`; if (panel) this.Create(); } Speak() { this.panel.Say(this.sndSrc); this.panel.Popup(this.name, this.img.style); } Create() { let panel = this.panel, style = `position=absolute;width=90px;height=80px;left=${this.x}px;top=${this.y}px;`; this.img = $("img", style, `src=${this.imgSrc}`, panel.div); this.img.onmousedown = (e) => { let animal = panel.animal; if (animal && animal !== this) { panel.RemovePopup(); panel.player.pause(); animal.img.style.border = "none"; } e.preventDefault(); e.target.xOld = Math.round(e.x); e.target.yOld = Math.round(e.y); e.target.style.border = "solid red 2px"; panel.animal = this; panel.msg.innerText = `My name is ${this.name}`; if (e.ctrlKey) this.Speak(); } window.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { panel.RemovePopup(); panel.animal.Speak(); } } this.img.onmousemove = (e) => { if (!e.target.xOld) return; panel.RemovePopup(); let o = e.target, s = o.style, x = Math.round(e.x), y = Math.round(e.y); s.left = (parseInt(s.left) + x - o.xOld) + "px"; s.top = (parseInt(s.top) + y - o.yOld) + "px"; o.xOld = x; o.yOld = y; } this.img.onmouseup = (e) => e.target.xOld = null; this.img.onmouseout = (e) => e.target.xOld = null; } } class Lion extends Animal { constructor(name, x, y, panel) { super(name, x, y); super.Set(panel, "Lion"); } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 } class Cougar extends Animal { constructor(name, x, y, panel) { super(name, x, y); super.Set(panel, "Cougar"); } } class Elephant extends Animal { constructor(name, x, y, panel) { super(name, x, y); super.Set(panel, "Elephant"); } } class Game { constructor() { let panel = new Panel(); new Lion("Leo", 20, 120, panel); new Cougar("Megera", 120, 120, panel); new Elephant("Dolly", 220, 120, panel); } } onload = () => new Game(); </script> </head> <body></body> </html> ToolbarDyn Пример ToolbarDyn.html иллюстрирует возможности метода defineProperties прототипа Object, создание объектов с помощью конструктора, а также, создание и удаление элементов DOM. <html> <head> <title>ToolbarDyn</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> body { font-family: Trebuchet MS, Arial,Helvetica,sans-serif; font-size: 80%; padding: 0px 10px; } img { margin: 4px; border: 1px solid #afa; border-radius: 6px; } .toolbar { height:3.2em; border:2px solid #999; border-radius:10px; box-shadow:inset -2px -2px 3px #000; margin:3px; overflow:hidden; padding-left:8px; } .button { border: 0; border-radius:8px; color: white; padding:8px 16px; text-align:center; background:#3a3; transition-duration:0.4s; box-shadow:0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19); text-decoration:none; display: inline-block; font-size: 1.1em; margin: 4px 4px; cursor: pointer; } .button:hover { opacity: 0.8; transform: scale(0.9); } .active { border: 1px solid #f44; box-shadow: none; transform: scale(0.9); margin: 5px 3px; } .normal { border: 1px solid #aaa; box-shadow: inset -2px -2px 3px #999; margin: 5px 3px; } .normal:hover { transform: scale(1.25); } .msg { color: green; margin: 4px; font-size: 1.2em; } .disabled { border: 1px solid #999; box-shadow: none; opacity: 0.4; } .disabled:hover { transform: scale(1); } </style> <script src="../js/Kit.js"></script> <script> class Button { constructor(title, toolbar) { this.title = title; this.toolbar = toolbar; this.img = $("img", 0, `src=../images/${title}.gif;title=${title};className=normal;`, toolbar.div); this.img.onclick = () => this.Toggle(); this.img.oncontextmenu = (e) => this.Enable(e); } Toggle() { if (this.enabled) { this.active = !this.active; this.toolbar.OnClicked(); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 } } Enable(e) { e.preventDefault(); this.enabled = !this.enabled; } } Object.defineProperties(Button.prototype, { enabled: { get: function () { return !this.img.classList.contains("disabled"); }, set: function (v) { v ? this.img.classList.remove("disabled") : this.img.classList.add("disabled"); } }, active: { get: function () { return this.img.classList.contains("active"); }, set: function (v) { v ? this.img.className = "active" : this.img.className = "normal"; } }, }); class Toolbar { constructor() { this.items = []; this.curId = -1; this.div = $("div", 0, "className=toolbar;"); var btnAdd = $("input", 0, "type=button;value=Add;className=button;"), btnRem = $("input", "background=#f33;", "type=button;value=Remove;className=button;"); this.msg = $("div"); btnAdd.onclick = () => this.Add(); btnRem.onclick = () => this.Remove(); } Add() { var titles = ["Cut", "Copy", "Paste", "Color", "Underline", "Bold", "VerticalText"]; this.curId = (this.curId + 1) % titles.length; this.items.push(new Button(titles[this.curId], this)); } Remove() { var last = this.items.length - 1; if (last >= 0) { var item = this.items[last]; this.items.splice(last, 1); this.div.removeChild(item.img); item = null; this.curId = last; this.OnClicked(); } } OnClicked() { let s = ""; for (let i = 0; i < this.items.length; i++) { let btn = this.items[i]; if (btn.active) s += btn.title + " "; } this.msg.innerText = s; } } onload = () => new Toolbar(); </script> </head> <body></body> </html> Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Drag and Drop Следующий пример DnD.html иллюстрирует более полную версию реализации технологии Drag and Drop. Здесь показано, как обработать события: dragstart, dragenter, dragleave, dragover, dragend, drop. Напомню, что при привязке функций к событиям принято к именам событий прибавлять префикс on. Например, событию dragstart соответствует ссылка ondragstart. Ей надо присвоить имя (адрес) реальной функции обработки. <html> <head> <title>DnD</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> [draggable] { user-select: none; } .column { height: 400px; width: 200px; float: left; border: 2px solid #666; background: #ccc; margin-right: 8px; text-align: center; cursor: move; border-radius: 10px; box-shadow: inset -2px -2px 3px #000; } .column header { padding: 8px; font-size: 8em; color: #fff; text-shadow: #99f 4px 4px; background: linear-gradient(90deg, #aaf 0%, #558 80%); } .column.over { border: 2px dashed #f00; } .moving { opacity: 0.4; transform: scale(0.95); } </style> <script src="../js/Kit.js"></script> <script> class DnD { constructor() { let div = $("div", 0, "id=columns;"); for (let i = 65; i < 68; i++) { let d = $("div", 0, "draggable=true;className=column;", div); $("header", 0, "innerText=" + String.fromCharCode(i), d); } this.SetHandlers(); this.dragged = null; } SetHandlers() { let cols = document.querySelectorAll("#columns .column"); for (let i = 0; i < cols.length; i++) { let c = cols[i]; c.ondragstart = (e) => { let d = e.target; d.classList.add("moving"); this.dragged = d; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text", d.innerHTML); } c.ondragenter = (e) => e.target.classList.add("over"); c.ondragleave = (e) => e.target.classList.remove("over"); c.ondragover = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = e.target.classList.contains("column") ? "move" : "none"; return false; } c.ondragend = () => { for (let i = 0; i < cols.length; i++) { cols[i].classList.remove("over"); cols[i].classList.remove("moving"); } } c.ondrop = (e) => { e.preventDefault(); e.stopPropagation(); let d = e.target; if (d.classList.contains("column") && this.dragged !== d) { this.dragged.innerHTML = d.innerHTML; d.innerHTML = e.dataTransfer.getData("text"); } } } } } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 onload = () => new DnD(); </script> </head> <body></body> </html> Snake Основной сложностью при реализации примера Snake.html является осуществление контроля над ссылкой this. Мы хотим, чтобы внутри всех методов класса Game был доступен объект класса Game. Иногда на него ссылается this, а иногда (в случае обработки событий) требуются некоторые усилия, чтобы передать ссылку внутрь метода. <html> <head> <title>Snake</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <script src="../js/Kit.js"></script> <script> class Cell { constructor(x, y, food = false) { this.x = x; this.y = y; this.isFood = food; } Draw(dc) { dc.fillStyle = this.isFood ? "#f00" : "#2c2"; let sz = Game.cellSize, lt = this.x * sz, tp = this.y * sz; dc.fillRect(lt, tp, sz, sz); dc.strokeStyle = "white"; dc.strokeRect(lt, tp, sz, sz); } } class Snake { constructor() { this.body = [new Cell(1, 0), new Cell(0, 0)]; } Draw(dc) { for (let i = 0; i < this.body.length; i++) this.body[i].Draw(dc); } } class Game { constructor() { window.onkeydown = (e) => this.Go(e); this.dir = "rt"; this.interval = 200; this.size = 25; this.saveMe = true; let sz = this.size * Game.cellSize; this.dc = $("canvas", "border=1px solid black;", `width=${sz};height=${sz}`).getContext("2d"); let cb = $("input", 0, "type=checkbox;checked=1;", 0, 1); cb.onchange = () => this.saveMe = !this.saveMe; O.addText("Save Me"); this.Reset(); } static get cellSize() { return 15; } Reset() { if (this.timer) clearInterval(this.timer); this.snake = new Snake(); this.SetFood(); this.dir = "rt"; this.timer = setInterval(this.Draw, this.interval, this); } SetFood() { Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 let x, y; for (let ok = false; !ok;) { let max = this.size - 1; x = Math.round(Math.random() * max); y = Math.round(Math.random() * max); ok = true; let b = this.snake.body; for (let i = 0; ok && i < b.length; i++) ok = b[i].x != x || b[i].y != y; } this.food = new Cell(x, y, true); } Draw(g) // g — Game object { let dc = g.dc, sz = g.size * Game.cellSize, body = g.snake.body, dir = g.dir; dc.fillStyle = "#222"; dc.fillRect(0, 0, sz, sz); let x = body[0].x, y = body[0].y; dir == "rt" ? x++ : dir == "lt" ? x-- : dir == "up" ? y-- : dir == "dn" ? y++ : 0; if (x == -1 || x == g.size || y == -1 || y == g.size || !g.saveMe && g.Self(x, y)) { g.Reset(); return; } let c = null; if (x == g.food.x && y == g.food.y) { c = new Cell(x, y); // New head cell will be inserted at the beginning g.SetFood(); } else { c = body.pop(); // Last cell will be inserted at the beginning c.x = x; c.y = y; } body.unshift(c); // Insert at the beginning g.snake.Draw(dc); g.food.Draw(dc); dc.font = "16px Trebushet MS"; dc.fillStyle = "#eee"; dc.fillText("Score: " + (body.length - 2), 5, sz - 10); } Go(e) { let key = e.which, dir = this.dir; this.dir = key == 37 && dir != "rt" ? "lt" : key == 38 && dir != "dn" ? "up" : key == 39 && dir != "lt" ? "rt" : key == 40 && dir != "up" ? "dn" : dir; } Self(x, y) { let b = this.snake.body; for (let i = 0; i < b.length; i++) { if (y == b[i].y && x == b[i].x) return b[i]; } return null; } } onload = () => new Game(); </script> </head> <body></body> </html> Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Рассмотрим метод Draw(g) класса Game, который вызывается при каждом прерывании таймера. Связывая метод Draw с событием таймера обычным способом, например: this.timer = setInterval(this.Draw, this.interval); мы получаем такую ситуацию, что внутри Draw ссылка this будет указывать на объект window (так как функция setInterval является свойством window). Однако, функция setInterval позволяет передать дополнительный параметр и мы, пользуясь этим, передаем в качестве третьего параметра ссылку this, которая в момент привязки указывает на объект Game. this.timer = setInterval(this.Draw, this.interval, this); Теперь внутри метода Draw(g) мы можем пользоваться ссылкой g (она указывает на объект типа Game) для управления свойствами и методами объекта Game. Рассмотрим обработку события нажатия клавиш. Если осуществить привязку к этому событию обычным способом: window.onkeydown = this.Go; то внутри функции Go мы не будем иметь доступ к объекту Game, так как this будет указывать на window. Однако, использование для привязки лямбда функции (благодаря поддержке стандарта ES6) window.onkeydown = (e) => this.Go(e); не изменяет ссылку this и она остается равной тому значению, которое имела в момент привязки. Более того, этот способ позволяет передать в метод ссылку e на объект типа KeyboardEvent, который помогает обработать событие. В примере также показано, как пользоваться функциями get, set (их принято называть аксессорами). В методе класса Cell мы добываем размер ячейки, который определен в другом классе — Game. Это делается с помощью статического свойства cellSize. Статический аксессор имеет вид: static get cellSize() { return 15; }. Важно запомнить как синтаксис объявления аксессоров, так и способ обращения к ним: Game.cellSize; (Game — имя класса, а не объекта). Так добывается значение статического свойства не только в методах других классов, но и в методах своего собственного класса. Обратите также внимание на методы pop и unshift, которые имеются в объекте Array. Они напоминают работу со стеком. Коллекция Array на самом деле является хеш-таблицей и она имеет множество удобных методов. EventPropagation До сих пор мы осуществляли привязку к событию простым способом, например: btn.onclick = f;. Здесь f — ссылка на функцию обработки события click. Такой способ имеет недостаток, заключающийся в том, что если подключить чужой скрипт, то он может найти ваш объект btn и переопределить обработчик события onclick заново. Ваш обработчик при этом перестанет работать. Существует другой способ привязки функции к событию, возбуждаемому элементом HTML. Он позволяет добавлять ссылки на функции обработки в коллекцию, поддерживаемую объектом событие (Event). В этом случае в ответ на одно событие может быть вызвано несколько функций или одна функция многократно. Следующий пример EventPropagation.html показывает, как управлять механизмом возбуждения и обработки событий. По умолчанию событие, возникшее в каком-то HTML-элементе, ищет функцию обработки, которая зарегистрирована именно для этого элемента (target), затем (независимо от результата) событие передается элементу, который является контейнером (parent) для данного элемента, но только в случае если этот процесс не остановлен путем вызова метода stopPropagation для объекта Event (или MouseEvent, или KeyboardEvent, и т. д.). Этот алгоритм повторяется, пока не будет достигнут конец цепочки вложенных элементов. Если имеются обработчики для всех элементов в цепочке вложенности, то событие обрабатывается несколько раз (по числу обработчиков). Если одна и та же функция зарегистрирована в качестве обработчика события для всех элементов в цепочке, то она будет вызвана многократно. Такой процесс поиска обработчиков называется bubbling (по аналогии с пузырьками газа в шампанском, которые всплывают вверх). Если в каком-то из обработчиков для объекта event вызвать метод stopPropagation(), то поиск прекратится. Метод addEventListener объектов DOM, поддерживающих некоторые HTML-элементы, позволяет (кроме того) включить или выключить обратный процесс поиска обработчиков события. Он называется capturing, но мне более точным кажется термин tunelling, который принят в технологии WPF. Суть этого процесса в том, что событие вначале ловится самым верхним элементом цепочки вложенности (parent of parent of parent...), затем оно передается вложенным элементам, которые, в свою очередь, передают эстафету своим детям, и т. д. Поиск производится вглубь тунелей (отсюда термин tunelling). Вот пример включения или выключения этого механизма. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 div.addEventListener("click", OnDivClick, false); // Tunelling выключен divs.addEventListener("click", OnDivClick, true); // Tunelling включен Здесь div — объект, поддерживающий какую-то панель в цепи вложенных друг в друга панелей. Первая строка кода включает bubbling, а вторая capturing (я называю его tunelling). Теперь событие может быть обработано трижды, в фазах: capturing, target и bubbling. Свойство eventPhase объекта Event позволяет определить фазу обработки события. Если значение этого свойства равно единице, то работает фаза capturing, если оно равно 2, то событие обрабатывается для самого источника события (target), если оно равно 3, то работает фаза bubbling. Для выключения bubbling следует после обработки события в фазе target вызвать stopPropagation(); Для выключения capturing надо удалить обработчик события и добавить его снова (с параметром false). Довольно сложная логика, но она работает. Убедитесь в этом, выполняя пример в режиме отладки и управляя флажками chBubble и chTunnel (элементами типа checkbox). <html> <head> <title>EventPropagation</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> body { font-family: Trebuchet MS; } h1 { color: #ffc; background: #900; font-size: 1.6em; margin: 0; margin-bottom: 0.5em; padding: 0.25em; text-align: center; letter-spacing: 0.08em; border-bottom-style: solid; border-bottom-width: 0.2em; border-bottom-color: #c00; } </style> <script src="../js/Kit.js"></script> <script> class EventPropagation { constructor() { $("h1", 0, "innerText=Event Bubbling and capturing;"); O.addText("Click any panel and analyse event route"); let div = $("div", "background=#eee;border=1px solid #aaa;margin=10px 0px;padding=20px;"); O.html("Bubbling: "); this.chBubble = $("input", 0, "type=checkbox;"); let bInfo = $("span", 0, "innerText=Off;"); O.html("<br/>Capturing:"); this.chTunnel = $("input", 0, "type=checkbox;"); let cInfo = $("span", 0, "innerText=Off;"); O.html("<br/>Route:"); this.divResult = $("span", "margin-left=30px;"); O.html("<pre>c: capturing; t: target; b: bubbling;</pre>", $("div", "margin=4px;")); this.divs = []; for (let i = 1, d = div, dn; i < 6; i++ , d = dn) { let s = "margin-bottom=15px;padding=4px;border=1px solid #000;background=#"; s += (i & 1) === 1 ? "ffc" : "cfc"; dn = $("div", s, `id=${i};innerText=${i};`, d); this.divs.push(dn); } this.AddListeners(); div.addEventListener('click', this.Reset, true); this.chBubble.onclick = () => bInfo.innerText = (this.chBubble.checked ? "On" : "Off"); this.chTunnel.onclick = () => { cInfo.innerText = (this.chTunnel.checked ? "On" : "Off"); this.RemoveListeners(); this.AddListeners(); } } AddListeners() { for (var i = 0; i < this.divs.length; i++) { this.divs[i].addEventListener("click", this.OnDivClick, false); if (this.chTunnel.checked) this.divs[i].addEventListener("click", this.OnDivClick, true); } } RemoveListeners() { for (var i = 0; i < this.divs.length; i++) { this.divs[i].removeEventListener("click", this.OnDivClick, false); this.divs[i].removeEventListener("click", this.OnDivClick, true); } } OnDivClick(e) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { if (e.eventPhase == 2) { e.currentTarget.style.backgroundColor = "red"; if (!glob.chBubble.checked) e.stopPropagation(); } let p = e.eventPhase, s = p == 1 ? "c" : p == 2 ? "t" : "b", txt = glob.divResult.innerHTML, t = txt !== "" ? "&#x21fe;" : ""; glob.divResult.innerHTML += t + s + e.currentTarget.id; } Reset(e) { for (var i = 0; i < glob.divs.length; i++) glob.divs[i].style.backgroundColor = (i & 1) === 0 ? "#ffc" : "#cfc"; glob.divResult.innerHTML = ""; } } var glob = null; onload = () => glob = new EventPropagation(); </script> </head> <body></body> </html> Forest Пример Forest основан на алгоритме рисования деревьев в контексте HTML-элемента canvas, который я подсмотрел на сайте GitHub. Каждое дерево рисуется с помощью метода Tree класса Forest. Дерево состоит из ветвей, для генерации которых используется рекурсивная процедура Recurse. Каждая ветвь представляет собой множество сегментов линий с постепенно уменьшающейся шириной и координатами, которые формируются с помощью генератора случайных чисел. Параметры алгоритма подобраны так, чтобы имитировать искривление ствола и ветвей, характерное для реальных деревьев и кустов. Пример использует два таймера: один для запуска процедуры генерации дерева, другой для постепенного осветления (или затемнения) фона (Night, Day). Секрет затухания деталей рисунка кроется в параметре opacity (степень непрозрачности цвета). Этот параметр в моей версии алгоритма задан жестко и равен fade=0.01. Влияние коэффициента на реалистичность происходящего весьма велика. Его следует задавать разным для разных браузеров, так как скорость отрисовки дерева и скорость затухания деталей должны быть синхронизированы. Оказалось, что пример (а именно, код отрисовки дерева) позволяет оценить и сравнить браузеры по быстродействию. Меня удивил тот факт, что быстрее всех отрисовывает деревья браузер Edge, второе место следует отдать браузеру Firefox, на последнем месте (из трех браузеров, которые установлены на моем компьютере) стоит Chrome. Отставние Chrome было весьма заметно в 2017 году. В конце 2018 ситуация изменилась: быстродействие Chrome улучшилось, но оно медленно падает в роцессе работы скрипта. Проверьте это на вашем железе и сообщите мне результаты. <html> <head> <title>CanvasForest</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <style> .button { border: 0; border-radius: 8px; color: white; opacity: 0.7; padding: 5px 8px; text-align: center; background-color: #f44; transition-duration: 0.4s; box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19); text-decoration: none; display: inline-block; font-size: 1.1em; margin: 2px 6px; cursor: pointer; } .button:hover { opacity: 1; transform: scale(1.04); } </style> <script src="../js/Kit.js"></script> <script> class Forest { constructor() { this.width = window.innerWidth - 120; this.height = window.innerHeight; this.clr = "#420"; let fade = 0.01; this.white = `rgba(255,255,255,${fade})`; this.black = `rgba(0,0,0,${fade})`; this.bkClr = this.white; this.delay = 60; this.delta = 10; this.steady = true; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 this.busy = false; let w = this.width, x = this.width + 10, canvas = $("canvas", 0, `width=${w};height=${this.height}`), bStart = $("button", `position=absolute;left=${x}`, "id=start;innerText=Start;className=button;"), bDay = $("button", `position=absolute;left=${x};top=45px;background=#6a6`, "innerText=Night;className=button;"), bSteady = $("button", `position=absolute;left=${x};top=85px;background=#6ad`, "id=steady;innerText=Steady;className=button;"), bMore = $("button", `position=absolute;left=${x};top=125px;background=#6ad`, "innerText=More;className=button;"), bLess = $("button", `position=absolute;left=${x};top=165px;background=#6ad`, "innerText=Less;className=button;"), sDelay = $("span", `position=absolute;left=${x};top=205px;margin=10px;`, `innerText=${this.delay} ms`); this.dc = canvas.getContext("2d"); this.Init(); bStart.onclick = () => this.OnStart(); bDay.onclick = (e) => this.OnToggleDay(e); bMore.onclick = () => this.OnMore(); bLess.onclick = () => this.OnLess(); bSteady.onclick = () => this.OnSteady(); } Init() { this.dc.fillStyle = "#222"; this.dc.fillRect(0, 0, this.width, this.height); this.dc.fill(); } Tree(x, y, dx, dy, w, rate, t) { // This algorithm belongs to Alejandro U. Alvarez https://github.com/aurbano/TreeGenerator/tree/master/src // I have slightly modified it let dc = this.dc, widthLoss = 0.03,// Width loss branchLoss = 0.8,// % width maintained for branches speed = 0.3,// Movement speed newBranch = 0.8,// Chance of not starting a new branch h = this.height, clr = this.clr, steady = this.steady;// Steady growth or fast Recurse(x, y, dx, dy, w, rate, t); function Recurse(x, y, dx, dy, w, rate, t) // Recursive tree generation { dc.lineWidth = w - t * widthLoss; dc.beginPath(); dc.moveTo(x, y); if (steady) rate *= 0.5; x += dx; y += dy; dx += speed * Math.sin(Math.random() + t); dy += speed * Math.cos(Math.random() + t); if (w < 6 && y > h - Math.random() * 0.3 * h) // Check if branches are getting too low w *= 0.8; dc.strokeStyle = clr; // Draw the next segment of the branch dc.lineTo(x, y); dc.stroke(); // Generate new branches. Thet should spawn after a certain time, depending on the width if (t > 5 * w + Math.random() * 100 && Math.random() > newBranch) { setTimeout(() => { Recurse(x, y, 2 * Math.sin(Math.random() + t), 2 * Math.cos(Math.random() + t), (w - t * widthLoss) * branchLoss, rate + Math.random() * 100, 0); w *= 0.8; // When branching, loose a bit of width }, 10 + 2 * rate * Math.random()); } if (w - t * widthLoss >= 1) // Continue the branch setTimeout(() => Recurse(x, y, dx, dy, w, rate, ++t), rate); } } OnToggleDay(e) { if (this.bkClr === this.white) { e.target.innerText = "Day"; this.clr = "#eef"; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 this.dc.fillStyle = "#aaa"; this.bkClr = this.black; } else { e.target.innerText = "Night"; this.clr = "#420"; this.dc.fillStyle = "#222"; this.bkClr = this.white; } this.ChangeFading(); } Start() { this.timerTree = setInterval(() => this.Tree(rnd(this.width), this.height, 0, -rnd(3), rnd(10), 30, 0), this.delay); this.ChangeFading(); O.query("#start").innerText = "Stop"; function rnd(x) { return Math.random() * x; } } OnStart() { clearInterval(this.timerTree); clearInterval(this.timerFade); let btn = O.query("#start"); this.busy = !this.busy; if (!this.busy) btn.innerText = "Start"; else { btn.innerText = "Stop"; this.Start(); } } ChangeFading() { clearInterval(this.timerFade); this.timerFade = setInterval(() => { this.dc.fillStyle = this.bkClr; this.dc.fillRect(0, 0, this.width, this.height); this.dc.fill(); }, 100); } OnSteady() { this.steady = !this.steady; O.query("#steady").innerText = this.steady ? "Steady" : "Quick"; } OnMore() { if (this.delay > 20) this.delay -= 10; clearInterval(this.timerTree); this.Start(); O.query("span").innerText = `${this.delay} ms` } OnLess() { if (this.delay < 200) this.delay += 10; clearInterval(this.timerTree); this.Start(); O.query("span").innerText = `${this.delay} ms` } } onload = () => new Forest(); </script> </head> <body></body> </html> Chess Последний пример, Chess.html, иллюстрирует такой механизм ООП, как наследование свойств и методов. Класс Piece (шахматная фигура) является базовым для всех типов шахматных фигур. Он содержит методы, общие для всех фигур: CanMoveTo(x, y), CanBeat (x, y), AddCell(moves, cell), Moves8(p), Draw(x, y). Классы, реализующие функциональность других фигур, различаются способом реализации метода GetMoves(cell). Этот метод возвращает массив объектов Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 класса Cell, который содержит множество тех клеток шахматной доски, на которые данная фигура может пойти (может быть перемещена) при условии, что в данный момент она расположена в клетке cell. Обратите внимание на то, как реализован метод GetMoves для ферзя (класс Queen). Множество его ходов состоит из слияния множества ходов ладьи (Rook) и множества ходов слона (Bishop). В методе GetMoves класса Queen мы используем понятие prototype, которое давно реализовано в языке JavaScript и оно скрыто в новом синтаксисе классов. В действительности любой класс реализован в виде конструктора (функции, у которой есть прототип — специальный объект, в который помещены все методы класса). Используя классы, мы видим синтаксический сахар (Syntactic sugar), который нам приятно видеть, так как мы привыкли к способу реализации технологии ООП в таких языках, как С++, C# и Java. На самом деле JavaScript реализует наследование и много других черт ООП, а также функционального программирования, с помощью механизма прототипов. Разработчики языка JavaScript считают, что возможности этого механизма значительно превышают возможности традиционного ООП. В примере также использована функция типа Immediately-invoked function expression (IIFE), которая позволяет спрятать внутрь тела анонимной функции переменные: board и game (объекты соответствующих классаов). Если бы мы не использовали механизм самозапускаемой функции (IIFE), то переменные были бы определены в глобальном пространстве имен. Для нашей страницы этот факт не имеет никакого значения, но современные способы создания страниц (с подключением js-файлов из сети) выдвигают повышенные требования к изоляции кода во избежание конфликта имен. Цель применения IIFE — не засорять глобальное пространство имен. <html> <head> <title>Chess</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <link href="../css/Styles.css" rel="stylesheet" /> <script src="../js/Kit.js"></script> <script> (function () { class Cell { constructor(x, y, size, clr) { this.x = x; this.y = y; this.size = size; this.clr = clr; this.figure = null; this.marked = false; this.selected = false; } Clear(dc) { this.figure = null; this.marked = false; this.selected = false; } Mark(dc) { this.marked = true; this.Draw(dc); } Select(dc) { this.selected = true; this.Draw(dc); } Draw(dc) { let x = this.x * this.size + board.pad, y = this.y * this.size; dc.fillStyle = this.selected ? board.clrS : this.marked ? board.clrM : this.clr; dc.fillRect(x, y, this.size, this.size); dc.strokeStyle = "#444"; dc.strokeRect(x, y, this.size, this.size); if (this.figure) this.figure.Draw(x, y); } } class Piece { Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 constructor(name) { this.name = name; this.clr = this.name.slice(-1); this.img = new Image(); this.img.src = "../images/" + name + ".gif"; this.wasMoved = false; } CanMoveTo(x, y) { if (x >= 0 && x < 8 && y >= 0 && y < 8) { let c = board.cells[x][y]; if (c.figure) return c.figure.clr != this.clr; else return true; } return false; } CanBeat(x, y) { if (x >= 0 && x < 8 && y >= 0 && y < 8) { let c = board.cells[x][y]; if (c.figure) return c.figure.clr != this.clr; } return false; } AddCell(moves, cell) { if (cell.figure) { if (cell.figure.clr != this.clr) moves.push(cell); return false; } moves.push(cell); return true; } Moves8(p) { let moves = [], cells = board.cells; for (let i = 0; i < p.length; i += 2) { if (this.CanMoveTo(p[i], p[i + 1])) moves.push(cells[p[i]][p[i + 1]]); } return moves; } Draw(x, y) { if (!this.img) return; let sz = board.size / 8; game.dc.drawImage(this.img, x, y, sz, sz); } } class Pawn extends Piece { GetMoves(cell) { let cells = board.cells, moves = [], x = cell.x, y = cell.y, d = this.clr == 'B' ? 1 : -1; // advance delta if (!this.wasMoved) { let y1 = y + d, y2 = y1 + d; if (!cells[x][y1].figure && !cells[x][y2].figure) moves.push(cells[x][y2]); } let j = y + d, onBoard = j >= 0 && j < 8; if (onBoard) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { let c = cells[x][j]; if (!c.figure) moves.push(c); } let i = x - 1; if (x - 1 >= 0 && onBoard && this.CanBeat(i, j)) moves.push(cells[i][j]); i = x + 1; if (x + 1 < 8 && onBoard && this.CanBeat(i, j)) moves.push(cells[i][j]); return moves; } } class Knight extends Piece { constructor(clr) { super("N" + clr); } GetMoves(cell) { let x = cell.x, y = cell.y, x1 = x - 1, x2 = x - 2, x3 = x + 1, x4 = x + 2, y1 = y - 1, y2 = y - 2, y3 = y + 1, y4 = y + 2; return this.Moves8([x4, y3, x4, y1, x2, y3, x2, y1, x3, y4, x3, y2, x1, y4, x1, y2]); } } class Bishop extends Piece { constructor(clr) { super("B" + clr); } GetMoves(cell) { let moves = [], cells = board.cells, x = cell.x, y = cell.y, i, j; for (let d = 1; (i = x + d) < 8 && (j = y + d) < 8; d++) { if (!this.AddCell(moves, cells[i][j])) break; } for (let d = 1; (i = x + d) < 8 && (j = y - d) >= 0; d++) { if (!this.AddCell(moves, cells[i][j])) break; } for (let d = 1; (i = x - d) >= 0 && (j = y + d) < 8; d++) { if (!this.AddCell(moves, cells[i][j])) break; } for (let d = 1; (i = x - d) >= 0 && (j = y - d) >= 0; d++) { if (!this.AddCell(moves, cells[i][j])) break; } return moves; } } class Rook extends Piece { constructor(clr) { super("R" + clr); } GetMoves(cell) { let moves = [], cells = board.cells, x = cell.x, y = cell.y, i, j; for (j = y + 1; j < 8; j++) { if (!this.AddCell(moves, cells[x][j])) break; } for (j = y - 1; j >= 0; j--) { if (!this.AddCell(moves, cells[x][j])) break; } for (i = x + 1; i < 8; i++) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { if (!this.AddCell(moves, cells[i][y])) break; } for (i = x - 1; i >= 0; i--) { if (!this.AddCell(moves, cells[i][y])) break; } return moves; } } class Queen extends Piece { constructor(clr) { super("Q" + clr); } GetMoves(cell) { return [].concat(Rook.prototype.GetMoves.call(this, cell), Bishop.prototype.GetMoves.call(this, cell)); } } class King extends Piece { constructor(clr) { super("K" + clr); } GetMoves(cell) { let x = cell.x, y = cell.y, xm = x - 1, xp = x + 1, ym = y - 1, yp = y + 1; return this.Moves8([xp, ym, xp, y, xp, yp, x, yp, xm, yp, xm, y, xm, ym, x, ym]); } } class Board { constructor() { this.size = 400; this.pad = 20; // for board markup this.clrW = "#ffe"; this.clrB = "#da6"; this.clrS = "#f99"; this.clrM = "#cfc"; this.font = '18px Arial'; this.cells = null; this.cellActive = null; this.moves = []; this.lastTurn = 'B'; } CreateCells() { this.cells = []; for (let i = 0; i < 8; i++) { this.cells[i] = []; for (let j = 0; j < 8; j++) { let clr = ((i + j) % 2 == 0 ? this.clrB : this.clrW); this.cells[i][j] = new Cell(i, j, this.size / 8, clr, this); } } } Draw(dc) { let a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], y = this.size + this.pad, dx = 2 * this.pad, dy = 1.5 * this.pad, d = this.size / 8, i; dc.font = this.font; dc.fillStyle = "#000"; for (i = 0; i < 8; i++) dc.fillText(a[i], i * d + dx, y); for (i = 0; i < 8; i++) dc.fillText(8 - i, 0, i * d + dy); for (i = 0; i < 8; i++) { for (let j = 0; j < 8; j++) this.cells[i][j].Draw(dc); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 } } ClearSelection() { for (let i = 0; i < this.moves.length; i++) { let c = this.moves[i]; c.marked = false; c.Draw(game.dc); } this.cellActive.selected = false; this.cellActive.Draw(game.dc); this.cellActive = null; } Clear() { for (let x = 0; x < 8; x++) for (let y = 0; y < 8; y++) this.cells[x][y].Clear(); this.lastTurn = 'B'; } Select(cell) { if (this.cellActive) this.ClearSelection(); let f = cell.figure; if (f && f.clr != this.lastTurn) { this.moves = f.GetMoves(cell); for (let i = 0; i < this.moves.length; i++) { let m = this.moves[i]; this.cells[m.x][m.y].Mark(game.dc); } cell.Select(game.dc); this.cellActive = cell; } } TryMoveTo(cell) { let f = this.cellActive.figure; if (cell.marked && !cell.selected) { cell.figure = f; f.wasMoved = true; this.cellActive.figure = null; this.ClearSelection(); this.lastTurn = f.name.slice(-1); return true; } return false; } Add(f, x, y) { this.cells[x - 1][y - 1].figure = f; } } class Game { constructor(user) { let size = board.size + board.pad; this.sz = board.size / 8; this.canvas = $("canvas", 0, `width=${size};height=${size}`); this.dc = this.canvas.getContext("2d"); this.user = user; this.gameId = 0; this.state = 0; this.user1 = 0; this.user2 = 0; this.moves = []; this.games = []; this.moves_orig = null; this.mouseDown = false; this.posX = 0; this.posY = 0; board.CreateCells(); this.InitFigures('W', 'bottom'); this.InitFigures('B', 'top'); setTimeout(() => this.Draw(), 400); this.canvas.onmousedown = (e) => this.OnMouseDown(e); } InitFigures(clr, position) { Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 let y1 = (position == 'bottom' ? 8 : 1), y2 = (position == 'bottom' ? 7 : 2); for (let i = 1; i < 9; i++) board.Add(new Pawn(clr), i, y2); board.Add(new Rook(clr), 1, y1); board.Add(new Knight(clr), 2, y1); board.Add(new Bishop(clr), 3, y1); board.Add(new Queen(clr), 4, y1); board.Add(new King(clr), 5, y1); board.Add(new Bishop(clr), 6, y1); board.Add(new Knight(clr), 7, y1); board.Add(new Rook(clr), 8, y1); } NewGame(color) { this.gameId = 0; this.state = 0; this.moves = []; board.lastTurn = color == 'W' ? 'B' : 'W'; board.Clear(); board.ClearSelection(); if (color == 'W') { this.InitFigures('W', this.InitFigures('B', } else { this.InitFigures('W', this.InitFigures('B', } this.Draw(); 'bottom'); 'top'); 'top'); 'bottom'); } LoadGame(gameId, moves) { let g, i; for (i = 0; i < this.games.length; i++) { g = this.games[i]; if (g.Id == gameId) break; } let color = g.User1Id == this.user ? 1 : 2; this.newGame(color); this.gameId = gameId; this.state = g.State; this.user1 = g.User1Id; this.user2 = g.User2Id; for (i = 0; i < moves.length; i++) { let m = moves[i], data = m.Data.split(' '), x1 = parseInt(data[0]), y1 = parseInt(data[1]), x2 = parseInt(data[2]), y2 = parseInt(data[3]); board.Select(x1, y1); this.Move(x2, y2); } } Resign() { this.state = board.lastTurn; this.Draw(); } OnMouseDown(e) { e.preventDefault(); let i = Math.floor((e.x - e.target.offsetLeft - board.pad) / this.sz), j = Math.floor((e.y - e.target.offsetTop) / this.sz), cell = board.cells[i][j], a = board.cellActive; if (a) { let m = { figure: a.figure.name, src: a.x + ' ' + a.y, dst: cell.x + ' ' + cell.y }; if (board.TryMoveTo(cell)) { this.moves.push(m); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 return; } } board.Select(cell); } Draw() { let dc = this.dc; dc.clearRect(0, 0, this.canvas.width, this.canvas.height); board.Draw(dc); let c = board.cellActive; if (this.mouseDown && c) { if (c.figure) c.figure.Draw(dc); } } GoToStart() { while (this.MoveBack()); } GoToEnd() { while (this.MoveFwd()); } MoveBack() { if (!this.moves_orig) this.moves_orig = this.moves.slice(); let end = this.moves.length - 1; if (end < 0) return false; let tmp = []; for (let i = 0; i < end; i++) tmp.push(this.moves_orig[i]); this.LoadGame(this.gameId, tmp); return true; } MoveFwd() { let index = this.moves.length; if (this.moves_orig && this.moves_orig.length > index) { let gameId = this.gameId, tmp = []; for (let i = 0; i < index + 1; i++) tmp.push(this.moves_orig[i]); this.LoadGame(gameId, tmp); return true; } return false; } } let board = null, game = null; onload = () => { board = new Board(); game = new Game(0); } })(); </script> </head> <body></body> </html> В зимнем семестре 2016 года наш студент Иван Федоров создал модуль, который решает шахматные задачи вида: "Мат в три хода", или "Форсированная ничья". Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 3. Краткое описание языка Типы данных JavaScript — язык динамического программирования. Типы переменных не фиксированы, как это принято в языках C, C++, C#, Java. Во время выполнения кода любая переменная может изменить свой тип данных. Ho в каждый фиксированный момент времени она ссылается на объект какого-то определенного типа. Перечислим типы, встроенные в JS. Примитивныые типы: number, string, boolean. Они неизменяемы (immutable). Переменные этих типов можно перезаписать (создать заново), но нельзя изменить. Специальные типы: null (переменной присвоено пустое значение), undefined (переменная не инициализирована). Ссылочный (непримитивный) тип object, от которого происходит множество других типов. Существует также множество встроенных типов, построенных на основе object. Они создаются с помощью функций (конструкторов объектов): Array, Function, Date, RegExp и др. Если применить к переменным этого типа операцию typeof, то все они ответят строкой "object". Каждый объект в JavaScript является коллекцией (хеш-таблицей) пар типа key-value. Ключем всегда является строка текста, а значением — объект произвольного типа. Все перечисленные типы (кроме null, undefined) имеют конструкторы, они по сути являются объектами, имеющими свойства и методы, например, Date.parse("January 1, 1972 00:00:00") // Метод parse преобразует текст в дату Math.PI.toFixed(3) // Метод toFixed выдаст число в виде строки с тремя знаками после запятой Оператор typeof позволяет определить тип переменной. Например, результатом операции: typeof "Hi" будет "string". Результатом операции typeof null будет "object" и это признанная разработчиками ошибка, которую не будут исправлять, чтобы не нарушить существующий код. Рассмотрим фрагменты кода JS, которые помогут вам быстро (на уровне интуиции) войти в курс дела. Литералы, примитивы, и объекты (Immutable Primitive Values and Mutable Object References) Литералами называются фиксированные значения (values), которые обычно присваиваются переменным. Например: "Peter" — строковый литерал, [1, 2, 0.1, -7.4е2, 3e-5] — литерал массива. Следующий пример является литералом типа Object. Переменной p присвоена ссылка на объект, заданный литералом. var p = { name: "Alex", birth: Date.now() }; // Объект – это коллекция свойств Допустим, что есть оператор: var r = "Rang". Вы не можете изменить какой-либо символ в строке "Rang", но вы можете заменить всю строку: r = "Rank". Литералы типа string, number, boolean, date — являются примитивами, а литералы типа Object, Function, Array и RegExp — нет. При коррекции даты (Date) создается новая дата, но вы можете изменить отдельный элемент массива (Array) и для этого не надо заменять весь массив. Говорят, что массив mutable (изменяем), а объект типа Date — immutable. Объект в нотации JSON Создайте тестовую страницу HTML, добавьте ссылку на файл с нашей библиотекой: <script src="../js/Kit.js"> </script> добавьте еще один блок <script></script> и вcтавьте внутрь него объявление объекта в нотации JSON (Java Script Object Notation). Это — коллекция пар вида name: value. Первый элемент пары всегда имеет тип string. var man = // Свойства объекта (или поля) вложены в блок { } { name: "Peter Norton", // Свойству "name" соответствует строка текста. Его value (значение) — "Peter Norton" resident: true, // Свойству "resident" сопоставлено значение true типа Boolean // Свойству "Show" сопоставлена функция (всегда ссылочный тип). Такое свойство принято называть методом. Show: function() { return this.name + (this.resident ? " is " : " is not ") + "resident" } }; Этот код желательно выполнять в режиме отладки браузера Chrome. Откройте файл в брузере и нажмите F12. Для выполнения одной строки кода нажимайте F11. Для продолжения выполнения (без остановки) нажмите F8. В окне Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Watch удобно проверять значения переменных. Пользуйтесь также консолью браузера. Вводите выражение и оценивайте его результат. Добавьте в конец блока <script></script> фрагмент кода, который приведен ниже. Введите в окно Watch переменную o и наблюдайте за ее значением. Значение свойства объекта man можно добыть либо с помощью операции . (точка) либо с помощью операции []. var o = man.name; // "Peter Norton" o = man["name"]; // "Peter Norton". Это равносильный способ получения значения свойства. o = man.resident; // true o = man["resident"]; // true o = man.Show; // function () { return this.name + (this.resident ? " is " : " is not ") + "resident" }. Метод, как член объекта man o = man["Show"]; // Тот же результат. o = man.Show(); // "Peter Norton is resident". Строка текста — результат вызова метода. man.wife = "Gloria"; // Создаем и добавляем новое свойство в объект man. o = man.house; // undefined. Такого свойства нет. man.house = {}; // Добавили новое свойство. o = man.house; // house – это пустой объект {...}, не имеющий свойств С необычными свойствами можно работать только с помощью синтаксиса o["key"], но не o.key. o["bank account"] = 1005438; // Ключ свойства имеет пробел. Такое свойство нельзя задать в точечной нотации. var t = typeof o["bank account"]; // number o["0.45"] = "available"; // Свойство имеет необычный ключ "0.45". Такое свойство нельзя задать в точечной нотации. t = typeof o["0.45"]; // string o["typeof"] = Date(); // Свойство имеет необычный ключ "typeof". Такое свойство нельзя задать в точечной нотации. t = typeof o["typeof"]; // string. Результат вызова конструктора Date имеет тип string. var o = new Date(); t = typeof o; // "object". Операция new создает объект типа Date, а вызов конструктора (без операции new) создает объект типа String. JS различает Primitive String values и String objects. То же справедливо для типов Boolean and Numbers. o = typeof "Hi"; // "string". Primitive value (литерал) o = typeof new String("Hi"); // "object". Reference type. o = typeof String("Hi"); // "string". Primitive value (литерал). Эту трактовку собираются убрать в Strict-версии JavaScript. o = "Word".substr(-2); // "rd" – подстрока, выделенная, начиная со второго символа от конца строки o = "test"; // Литерал o.size = 4; // Устанавливаем свойство t = o.size; // undefined, так как погиб временный объект String o = new String("test"); o.size = 20; // Устанавливаем свойство t = o.size; // 20, так как объект String жив String object имеет множество методов (indexOf, concat,...). При попытке вызвать один из этих методов для литерала (например, "Word".substr(-2);) JS создает временный объект new String("Word") — wrapper object. Метод вызывается для него. Свойства, устанавливаемые литералам, не выживают, так как временный объект тут же погибает. Значение undefined генерируется, когда переменная не существует, значение null — когда ей не присвоено значение. if (typeof x === "undefined") // Убедись, что x не существует alert("x is undefined"); var x; if (!x) // Эквивалент if (x === null) alert("x is null"); if (o !== undefined && o !== null) // Убедись, что o существует, затем убедись, что у него есть значение alert(o); if (o) // Эквивалент предыдущему оператору, но более лаконичный alert(o); Операции === (Strict Equality) нет ни в С++, ни в C#. Она означает строгую проверку. Для всех объектов (ссылочных типов) операции == и === равносильны, для примитивов — нет. Закомментируйте условные операторы выше, чтобы избавиться от надоедливых сообщений. Coercion В документации вы можете встретить термин Coercion (implicit type conversion — неявное приведение типов). При проверке на строгое равенство (===) type coercion не производится. Следующие примеры иллюстрируют работу этого механизма и результаты могут вас удивить. o o o o = = = = 2 == '2'; // true. Сравниваются значения литералов разных типов. Приведение типов производится автоматически (Coercion). 2 === '2'; // false. Значение типа number сравнивается со значением типа string. (Strict Equality — проверка на строгое равенство). '\n\t123\r ' == 123; // true. Сначала строка преобразуется в число 123, затем происходит нестрогое (sloppy) сравнение. 5 + null; // 5 Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 o = 5 + true; // 6. Сначала true преобразуется в 1, затем происходит сложение чисел. o = 5 + undefined; // NaN (Not A Number) o = [1, 2] + [3]; // "1,23". Сначала [1, 2] преобразуется с помощью toString() в "1,2", затем происходит конкатенация строк текста. o = "Hot" && "Wet"; // "Wet". Так как "Hot" !== false o = "Hot" || "Wet"; // "Hot". "Wet" не анализируется. o = false && "Wet"; // false. "Wet" не анализируется. o = "" && "Wet"; // "". Так как "" == false. "Wet" не анализируется. o = "" || "Wet"; // "Wet". Так как "" == false. o = Boolean(undefined); // false o = Boolean(null); // false o = Boolean(new Boolean(false)); // true. Все объекты при приведении к Boolean дают true o = Boolean(Boolean(false)); // false. Так как преобразуется примитив, а не объект if (!foo) // Этому условию удовлетволят такие (falsy values): false, 0, "", null, undefined, NaN console.log("falsy values are: false, 0, "", null, undefined, NaN"); When an operator is applied to the “wrong” type of value, JavaScript will quietly convert that value to the type it needs, using a set of rules that often aren’t what you want or expect. This is called type coercion. o o o o o = = = = = 8 * null; "5" - 1); "5" + 1; "five" * 2; false == 0; // 0 // 4 // 51 // NaN // true The null in the first expression becomes 0, and the "5" in the second expression becomes 5 (from string to number). Yet in the third expression, + tries string concatenation before numeric addition, so the 1 is converted to "1" (from number to string). When something that doesn’t map to a number in an obvious way (such as "five" or undefined) is converted to a number, you get the value NaN. Further arithmetic operations on NaN keep producing NaN, so if you find yourself getting one of those in an unexpected place, look for accidental type conversions. When comparing values of the same type using ==, the outcome is easy to predict: you should get true when both values are the same, except in the case of NaN. But when the types differ, JavaScript uses a complicated and confusing set of rules to determine what to do. In most cases, it just tries to convert one of the values to the other value’s type. However, when null or undefined occurs on either side of the operator, it produces true only if both sides are one of null or undefined. O = null == undefined; // true O = null == 0; // false That behavior is often useful. When you want to test whether a value has a real value instead of null or undefined, you can compare it to null with the == (or !=) operator. But what if you want to test whether something refers to the precise value false? Expressions like 0 == false and "" == false are also true because of automatic type conversion. When you do not want any type conversions to happen, there are two additional operators: === and !==. The first tests whether a value is precisely equal to the other, and the second tests whether it is not precisely equal. So "" === false is false as expected. I recommend using the three-character comparison operators defensively to prevent unexpected type conversions from tripping you up. But when you’re certain the types on both sides will be the same, there is no problem with using the shorter operators. Short-circuiting of logical operators The logical operators && and || handle values of different types in a peculiar way. They will convert the value on their left side to Boolean type in order to decide what to do, but depending on the operator and the result of that conversion, they will return either the original left-hand value or the right-hand value. The || operator, for example, will return the value to its left when that can be converted to true and will return the value on its right otherwise. This has the expected effect when the values are Boolean and does something analogous for values of other types. o = null || "user"; // → user o = "Ann" || "user"; // → Ann We can use this functionality as a way to fall back on a default value. If you have a value that might be empty, you can put || after it with a replacement value. If the initial value can be converted to false, you’ll get the replacement instead. The rules for converting strings and numbers to Boolean values state that 0, NaN, and the empty string ("") count as false, while all the other values count as true. So 0 || -1 produces -1, and "" || "!?" yields "!?". Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 The && operator works similarly but the other way around. When the value to its left is something that converts to false, it returns that value, and otherwise it returns the value on its right. Another important property of these two operators is that the part to their right is evaluated only when necessary. In the case of true || X, no matter what X is—even if it’s a piece of program that does something terrible—the result will be true, and X is never evaluated. The same goes for false && X, which is false and will ignore X. This is called short-circuit evaluation. The conditional operator works in a similar way. Of the second and third values, only the one that is selected is evaluated. Вывод результатов вычислений в панели типа div или span Удалите все содержимое блока <script> (не комментируйте его, а удалите). Не жалейте этот код, впереди вас ждут более содержательные примеры. По опыту знаю, что некоторые студенты не хотят ничего удалять. Если вы из этой категории, то добавьте в проект копию текущего файла, переименуйте ее, а затем очистите блок <script>. Чаще всего JS используется в Web-приложениях для управления объектами того HTML-документа, который в данный момент воспроизводится браузером. Здесь наиболее ярко проявляются преимущества динамических черт языка. С помощью глобальных функций (то есть, методов объекта window), а также свойств и методов объекта document (точнее, window.document) мы можем реагировать на события, генерируемые действиями пользователя, и гибко управлять древовидной структурой HTML-документа на стороне клиента. Примеры использования JS в HTMLстраницах были приведены во второй главе этого документа. В последнее время JS все чаще используется для управления логикой вычислений или обработки данных, получаемых в асинхронном режиме от сервера (технология AJAX Asynchronous JavaScript and XML) или для обработыки данных на сервере (Node.js). В этих случаях он используется как полноценный язык объектно-ориентированного и функционального программирования. Часто и те, и другие черты языка используются одновременно. Вставьте в блок <script></script> тестового файла такой код. function Init() { let div = $("div", "background=#ffe;border=1px solid #aaa;padding=6px;margin=4px;"), v = "12" * "24", sp = $("span", 0, `innerHTML=${v}`, div); } onload = Init; Объясните самому себе тот результат, который появится в окне браузера. Что, и во что, вложено? Все ли стыкуется с вашими представлениями о типах данных? Просмотрите справку по свойству innerHTML. Рекомендую искать справки на сайте JavaScript.ru. Сравните innerHTML со свойством innerText. Запомните, что не все контейнеры имеют последнее свойство. При отладке часто помогает проверка свойств элемента nodeName и outerHTML. Прочтите о них. Функция Init вызывается после того, как HTML-документ будет загружен в браузер (построена DOM-модель). В нашем случае к этому моменту будет создано пустое тело документа. В теле функции мы создаем панель div и вставляем ее в тело документа (document.body). Отыщите код создания HTML-элемента в конструкторе класса O (файл Kit.js) и убедитесь, что по умолчанию элемент попадет в document.body. Для того, чтобы панель была видна, мы красим ее, задаем границу (border), и отступы. Затем в панель div мы вставляем другую (inline) панель типа span и выводим в нее текстовое значение переменной v. По умолчанию поведение span отличается от поведения div. Панель div растянута по ширине на все окно (работает компонент стиля display:block;). Панель span обжимает свое содержимое. Если оно пусто, то панель схлопывается и ее вовсе не видно на экране (компонент стиля display:inline;). Если вы в компонентах стиля укажете ширину и высоту, то обе панели ведут себя почти одинаково и размеры равны тем значениям, что указаны в стиле. Добавьте внутрь функции Init такой код. sp.innerHTML += "<br>N-ричные системы исчисления</br>"; for (let i = 1; i < 21; i++) { let s = String.fromCharCode(i + 96); sp.innerHTML += `${s}&#8594; ${parseInt(s, 29)},&nbsp;&nbsp;`; if (i % 5 == 0) sp.innerHTML += "<br>"; } sp.innerHTML += `<br>0xaf &#8594; ${parseInt('af', 16)},&nbsp;&nbsp;`; Запустите и просмотрите результат. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Разгадайте смысл вычислений и результатов. Последняя строка кода: sp.innerHTML += `<br>0xaf &#8594; ${parseInt('af', 16)},&nbsp;&nbsp;`; позволяет увидеть десятичное представление шестнадцатиричного числа: 0xaf. Удивительно, но JS умеет работать с числами в любой из N-ричных систем исчисления в диапазоне (2 – 36). Я не знаю других языков, которые это умеют. Что же выведено в предыдущих строках? Что означает результат: t →NaN ? Для заметки: Выражение &#8594; выводит символ с десятичным кодом 8594, который изображает стрелку вправо →. Выражение &nbsp; выводит пробел. Все браузеры убирают лишние символы wite space (обычные пробелы, переходы на новую строку и табуляции) но такие, именованные пробелы (&nbsp; — non breaking space) останутся. Откройте тестовую страницу, затем выделите в окне Solution Explorer строку Styles.css и перетащите ее внутрь тега <head>. Редактор вставит ссылку на таблицу каскадных стилей, как показано ниже. <link href="../Styles.css" rel="stylesheet" /> Запустите страницу и убедитесь, что стиль документа немного изменился. Большая часть из приведенных стилей в данный момент не задействована. Они понадобятся позже. В данный момент работает только стиль: body { font-family:"Trebuchet MS",Arial,Helvetica,sans-serif; font-size:70%; } With the 32 bits, the value is computed by this esoteric formula: e = 0111100 = 124 (in base 10) 1.25 / 8 = 0.15625 Объекты JavaScript Объекты можно создавать разными способами: Работать с объектом Object (понятие класс до недавних пор отсутствовало, поэтому тавтологии не избежать). Использовать нотацию JSON (JavaScript Object Notation). Использовать специальную функцию (аналог конструктора в знакомых вам языках). Не путайте нотацию JSON с языком JSON, открытый стандарт которого разрабатывает Douglas Crockford (см. фото). Одним из самых важных отличий между ними является то, что Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 язык JSON не может содержать функций, а JSON-нотация (JavaScript's object-literal syntax) может. Добавьте в папку html новый документ с именем Objects.html. Откройте файл с Kit.js и добавьте в нашу библиотеку (класс O) пару новых методов: static clear(o) { if (!o) o = document.body; while (o.childNodes.length > 0) o.removeChild(o.firstChild); } static setColor(c) { let res = "#000"; try { let p = c.indexOf('('), s = 0; for (let i = 0; i < 3; i++) { c = c.substr(p + 1); s += parseInt(c); p = c.indexOf(","); } res = s < 384 ? "#fff" : "#000"; } catch(e) { } return res; } Метод сlear проходит по дереву документа и удаляет все элементы, вложенные в DOM-объект o. Если параметр o не задан, то удаляются все элементы HTML-документа. Метод setColor определяет цвет текста в зависимости от поданного на вход цвета фона. Если цвет фона светлый (сумма компонентов red, green, blue больше 384), то цвет текста будет черным ("#000"), иначе он будет белым ("#fff"). Поиск символа '(' необходим, так как свойство backgroundColor объекта style имеет вид строки текста в формате: "rgb(128, 0, 32)", а нам надо выделить и сложить все три компоненты цвета. Блок try-catch обрабатывает ситуацию, когда входной параметр имеет falsy value. Добавьте в файл Kit.js глобальную функцию function MyDiv(header) { let body = document.body; if (!body) return null; let prop = (header ? "innerText=" + header : ""), clr = O.setColor(body.style.backgroundColor), style = `display=block;color=${clr};font-size=14pt;text-decoration=underline;margin=4px;`, a = $("a", style, prop); style = "display=none;margin=6px;padding=6px;background-color=whitesmoke;font-size=12pt;"; let sp = $("span", style); a.onclick = () => { sp.style.display = (sp.style.display == 'block' ? 'none' : 'block'); }; this.Add = (s, v) => { sp.innerHTML += (sp.innerHTML !== "" ? "<br>" : "") + s; if (v !== undefined) sp.innerHTML += `: ${v}`; }; } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Функция MyDiv становится свойством объекта window. Она является конструктором объектов, которые позволяют создавать схлопывающиеся панели типа Show-Hide с помощью выражения new MyDiv("текст"), В панель вставляется элемент типа <a> (гиперссылка с текстом "текст"). Нажатие на нее схлопывает или раскрывает панель типа span. Метод this.Add объекта, позволяет добавлять текст внутрь панели span. Цвет текста гиперссылки вычисляется в зависимости от цветв фона тела документа. Если последний не определен, то цвет текста будет черным (#000 ). Замените все содержимое файла Objects.html кодом, который демонстрирует первый способ создания объектов. <html> <head> <title>Objects</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <script src="../js/Kit.js"></script> <script> function Init() { document.body.style.backgroundColor = "#144"; var d = new MyDiv("1. Creating object with new Object()"); var o = new Object(); o.Name = "Alex Black"; o.Age = "30"; o.Phone = "555-12-34"; o["Favourite Hobby"] = "Jazz music"; o.Go = function () { return "Going"; } // Добавили метод (свойство, которое является функцией!) for (var p in o) // Проход по всем свойствам объекта d.Add(p, o[p]); // Добыть значение свойства можно с помощью вычисляемого ключа if ("Favourite Hobby" in o) d.Add(`${o.Name} has a hobby`, o["Favourite " + "Hobby"]); o["0.45"] = "OK"; d.Add("o[0.25 + 0.2]", o[0.25 + 0.2]); o["typeof"] = Date(); d.Add("typeof o['typeof']", typeof o["typeof"]); } onload = Init; </script> </head> <body style="background-color:Background"> </body> </html> Запустите проект и объясните цвет текста гиперссылки. Нажмите на нее (несколько раз) и попытайтесь объяснить то, что происходит. Запустите отладчик браузера (F12) и просмотрите код в режиме пошагового выполнения (F11). Периодически наводите курсор мыши на объекты JS и просматривайте их свойства. Вспомните, что коллекция свойств любого объекта является хеш-таблицей и запомните, что следующие три выражения эквивалентны: var o = {}; var o = new Object(); var o = Object.create(Object.prototype); Профессионалы обычно выбирают первый вариант. Упражнение Добавьте в конец функции Init вызов O.clear(); В студии установите тип браузера (Chrome) и запустите проект без отладчика (Ctrl+F5). В браузере Chrome запустите отладчик (F12). Найдите строку кода O.clear(); и установите на нее точку останова. Перезагрузите браузер (F5). После останова нажмите F11 и пройдите по шагам до оператора: o.removeChild(o.firstChild); Установите точку останова на этой строке. Наведите курсор на идентификатор firstChild. В окне его свойств (в заголовке вы увидите значение свойства: nodeName. Отыщите свойство nodeValue. Эти свойства указывают какой узел документа вы пытаетесь удалить и каково его значение (не все элементы имеют осмысленное nodeValue). Нажмите клавишу F8, которая заставляет браузер продолжить выполение кода до следующей остановки. Удалите вызов функции O.clear() и добавьте в конец функции Init такой фрагмент кода: var array = new Array(); for (var p in o) array.push(p); array.sort(); var s = ""; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 for (var i = 0; i < array.length; i++) s += array[i] + ", "; d.Add("<b>Sorted properties</b>", s); В этом фрагменте показано, как работать с множеством свойств объекта, поместить их в массив и отсортировать. Запомните, объект Array позволяет работать с элементами произвольных сущностей, динамическая коллекция Array по сути является не массивом, а хеш-таблицей. Прототип Array имеет множество полезных методов (sort, slice, splice, indexOf...). Рассмотрим, например, как работает итератор forEach. s = 0; [1, 2, 3, 4].forEach((n) => s += n); d.Add("[1, 2, 3, 4].forEach((n) => s += n)", s); Функция, переданная в метод forEach, как параметр, является анонимной лямбда-функцией. Она вызывается для каждого элемента массива. Переменная s на ходу изменила свой тип (вместо string стала number). Интересно то, что метод forEach можно использовать для любого объекта, который имеет индексированные свойства, а также свойство length. Вы сами можете создать такой объект. Функции, которые потребляют или возвращают другие функции как данные, называются функциями более высокого порядка (higher-order functions). Если вы создадите функцию, которая поглощает другую функцию, то такую функцию называют декоратором ( function decorator). Например, следующая функция является декоратором для анонимной лямбда-функции: function Sum(array) { var s = 0; array.forEach(function add(n) { s += n; }); return s; }; Удаление свойств и объектов В функцию Init файла Objects.html добавьте код, иллюстрирующий возможность удаления свойств объекта. delete o.Phone; delete o["Favourite Hobby"]; delete o["typeof"]; delete o["0.45"]; d.Add("After deleting some properties", O.getAll(o)); d.Add("delete o: " + delete o); xxx = { name: "A", born: new Date().toLocaleDateString() }; d.Add("xxx", O.getAll(xxx)); d.Add(`delete xxx: ${delete xxx}. Now xxx is undefined`); Объекты можно удалять только в случае, если они были объявлены глобально. Попытка удалить локальный объект (delete o) возвращает false. Это означает, что объект жив. Объект xxx является глобальным (так как он объявлен без описателя var или let или const) и мы можем его удалить. Прототипы Когда компилятор JS видит объявление функции, он создает прототип. Прототипом называется объект, который сопровождает каждую функцию. Он содержит ссылку __proto__ на прототип родительского объекта и коллекцию методов, в которой обязательно есть метод, называемый конструктором. Для ссылки __proto__ ввели обозначение dunder proto — аббревиатура от double underscore proto (впервые введена в языке Python). Создать объект можно с помощью функции-конструктора. Прототип функции-конструктора содержит свойства общие для всех объектов, создаваемых с помощью конструктора. Поэтому наиболее часто конструктор описывает и инициализирует только данные, а ее прототип содержит только методы и свойства, общие для всех объектов. В JavaScript отсутствует понятие класса (class — это иллюзия). Это другой мир (более сложный, свободный, мощный и более эффективный). Наиболее сложно выглядит иерархия функции Object (конструктора объектов JS). Отладчик Edge показывает ее так. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Итак, Object — это функция. Она имеет коллекцию вложенных функций ([Methods]) и ссылку __proto__ на объект Function. Последний также имеет коллекцию методов и ссылку __proto__ на объект типа Object.prototype. Последний имеет коллекцию методов и ссылку __proto__, которая никуда не указывает (null). Теперь советую терпеливо рассмотреть все три упомянутые коллекции методов. Коллекция методов, вложенных в функцию Object. Эти методы помогают работать со вновь создаваемыми объектами. Некоторыми мы будем пользоваться. Метод defineProperties уже работал в примере ToolbarDyn. Коллекция методов, вложенных в функцию Function. Они помогают создавать новые функции и справляться с проблемой this, с которой мы сталкивались во второй главе и столкнемся еще не раз. Коллекция методов объекта Object.prototype. Они помогают создавать новые свойства и управлять их атрибутами. Запомните, свойства имеют атрибуты, которые указывают: можно ли удалять свойства, можно ли изменять значения свойств, нужно ли их перечислять и каково их текущее значение. Перечисленные ниже методы помогают разбирать иерархии сложных объектов. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Функция, породившая прототип, является конструктором. Любая функция JS является конструктором своего прототипа. Эту фразу полезно вспоминать, когда становится трудно воспринимать происходящее. Добавьте фрагмент, который доказывает вышесказанное. d = MyDiv("Any function f is a constructor of its prototype"); function f() { return "Ok"; } d.Add("f.prototype.constructor === f", (f.prototype.constructor === f)); d.Add("new f().constructor", new f().constructor); d.Add("f.prototype.__proto__", f.prototype.__proto__); d.Add("f.prototype.__proto__.__proto__", f.prototype.__proto__.__proto__); d.Add("Object.prototype.__proto__", Object.prototype.__proto__); d.Add("Object.getPrototypeOf(f) === Function.prototype", (Object.getPrototypeOf(f) === Function.prototype)); Ссылка __proto__ в объекте Object.prototype равна null, что следует понимать, как отсутствие предка у объекта, созданого операцией new Object(). Объект, созданный конструктором Object, стоит в вершине иерархии прототипов и у него нет предка. Последняя строка кода подтверждает тот факт, что прототип нашей функции f() равен прототипу любой функции. Function является конструктором любой функции и она тоже имеет прототип. Конструкторы, как фабрики экземпляров Добавьте фрагмент, который иллюстрирует второй способ создания объектов (с помощью конструктора). d = MyDiv("2. Creating object using Constructor"); function Person(name, age) { this.name = name; this.age = age; this.toString = function () { return `${this.name}; Age: ${this.age}`; }; } o = new Person("Alex Black", 30); d.Add(o); // Здесь работает toString d.Add(`Values of the public fields: ${o.name}, ${o.age}`); var proto = Person.prototype; Функция Person, как и любая другая, является конструктором своего прототипа. Заметьте также, что ссылки this в данном контексте удалять нельзя. Здесь они не лишние, как часто бывает в примерах на языке C#. Наличие ссылок this говорит о том, что поля данных объекта, созданного с помощью конструктора (то есть, операции new Person(...)) отличаются от одноименных параметров (которые являются локальными переменными конструктора). Все this-поля доступны вне конструктора (public data-fields), как показано в фрагменте (см. o.name и o.age). Если поставить точку останова на строке, которая следует за созданием объекта типа Person и просмотреть результат ее выполнения в отладчике студии, то мы увидим следующую картину (слева-направо: Edge, Chrome, Firefox.) Мы видим, что наш объект содержит два поля данных (age, name), ссылку на свой прототип (__proto__) и коллекцию методов с единственным методом toString. Сдвинемся на одну ступень выше по иерархии наследования прототипов. Поставим точку останова на строке, следующей за строкой: var proto = Person.prototype; Мы увидим такую картину: Следовательно, прототип функции Person, содержит ссылку на свой прототип и единственный метод (constructor). Раскрыв constructor, мы увидим свойства конструктора, то есть, функции с именем "Person". Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Раскрыв узел __proto__, мы увидим методы, которые содержатся в прототипе функции Object. Вы видите, что функция toString уже имеется в объекте-обертке (среди методов прототипа Object). Можно считать, что метод toString унаследован объектом Person и переопределен в конструкторе. Вы видите его работу при выводе объекта o, например, при выполнении console.log(o);. Выводы Прототип — это объект, который автоматически создается при объявлении любой функции. Любую функцию можно вызывать двумя способами: var p = Person("Alex", 30); var p = new Person("Alex", 30); Следует понимать глубокие различия в том, что реально происходит, при использовании этих способов. При первом способе выполняется код функции Person, который создает глобальные объекты name, age и toString, объект типа Person не создается и функция возвращает значение undefined. При втором способе сначала создается объект типа Person, затем инициализируются его поля name и age, переопределяется метод toString, уже имеющийся в прототипе прототипа (то есть, в прототипе функции Object), и функция Person возвращает ссылку на вновь созданный объект. Вывод. Не забывайте оператор new, если вы вызываете функцию, выполняющую роль конструктора объектов. Вывод. Не вставляйте функции в тело функций, которые будут выполнять роль конструкторов. Представим себе, что вы создаете 100 объектов типа Person. Функция (метод) toString будет переопределен 100 раз. Для того, чтобы это произошло единожды, уберем переопределение toString из тела конструктора и поместим его в прототип. Person.prototype.toString = function () { return `${this.name}; Age: ${this.age}`; } Сделайте это и повторите опыт. Вы увидите такой результат. Функция toString переехала в прототип. Она будет переопределена лишь один раз. Не следует изменять способ переопределения функции toString. Если вы, преследуя цель сократить код, замените функцию на лямбда-выражение, то toString перестанет работать не только по умолчанию, но и при явном вызове. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Важно запомнить Конструктором Person является глобальная функция. Обратите внимание! Вызов var o = Person("A", 1); (здесь нет операции new). Результат o == undefined. Вызов конструктора с помощью операции new: var o = new Person("A", 1); рождает объект (typeof o == "object"). Как защитить конструктор Person от неправильного вызова (без операции new). function Person(name, age) { if (!(this instanceof Person)) throw new Error("You must use new with Person."); this.name = name; this.age = age; } Попытка удалить переопределенный метод Следующий фрагмент показывает, что JavaScript не позволяет удалять свойства, унаследованные из прототипа. var t = typeof o.toString; // "function". Метод toString заимствован из обертки Object delete o.toString; // Попытка удалить его t = typeof o.toString; // "function". Метод жив o.toString = undefined; // Обезвреживаем метод toString t = typeof o.toString; // "undefined". Метод toString недоступен d.Add("After disabling toString", O.getAll(o)); try { d.Add(o); } catch (e) { d.Add("Trying to output object o", e.message); } После того, как вы проверите этот фрагмент в режиме отладки, удалите его, так как нам понадобится метод toString. Наследование путем вложения (Inheritance through containment) Рассмотрим один из способов реализации механизма наследования в JS. Он называется наследованием путем вложения. Объект, который желает получить некоторые свойства другого, уже существующего объекта, просто вкладывает новый экземпляр этого объекта (созданный с помощью конструктора) в коллекцию своих свойств. d = new MyDiv("Inheritance through containment"); function Stud(name, age, course = 1) // 1 - default value { this.person = new Person(name, age); this.course = course; } Stud.prototype.toString = function () { return `Student: ${this.person}; Course: ${this.course}` }; var chick = new Stud("Chick Corea", 20, 3); d.Add(chick); miles = new Stud("Miles Davis", 18); // course = 1 (default value) miles.hobby = "Jazz"; d.Add(`${miles}; Hobby: ${miles.hobby}`); Конструктор Stud, как и любая другая функция, создает прототип, функциональность которого мы можем изменять. Добавьте код, иллюстрирующий эту возможность. d.Add("<b>Changing Prototype</b>"); Person.prototype.Test = function () { return "function Test was added to Person prototype"; }; Stud.prototype.maxAge = 90; Stud.prototype.SetAge = function (a) { if (0 <= a && a <= this.maxAge) this.age = a; return this.age; } d.Add("Change chick age. Now it is", chick.SetAge(21)); d.Add("chick instanceof Person", chick instanceof Person); d.Add("chick instanceof Stud", chick instanceof Stud); d.Add("Person.prototype.isPrototypeOf(chick)", Person.prototype.isPrototypeOf(chick)); d.Add("Stud.prototype.isPrototypeOf(chick)", Stud.prototype.isPrototypeOf(chick)); var hobby = chick.hobby; if (typeof hobby === "undefined") d.Add("Chick has no property, named 'hobby'"); Рассматривая выведенные результаты, мы видим, что этот тип наследования нельзя назвать настоящим, так как производный объект не является одновременно и типом Person и типом Stud. Напомню, что в JS отсутствует понятие Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 класса, поэтому наследование имеет несколько разных смыслов, которые не вполне соответствуют концепции, реализованной в C# или Java. Наследование с помощью конструкторов Рассмотрим другой способ реализации наследования, который в большей степени соответствует концепции наследования ООП. Методы с префиксом this, вложенные внутрь конструктора называются привилегированными и иногда они действительно необходимы, но в нашем случае их нет. У нас есть только метод toString, который мы перенесли в Person.prototype, и он автоматически создается при объявлении конструктора. В конструктор лучше не вставлять методы, которые одинаковы для всех объектов типа Person. Конструктор — всего лишь функция, вызываемая с помощью операции new, ее не следует путать с классом языка C#, несмотря на то, что логика использования конструктора напоминает логику создания объекта класса. Напомню, при создании 100 объектов типа Person свойство toString будет переопределяться 100 раз (для каждого создаваемого объекта). Мы же хотим, чтобы тело метода было установлено только один раз. Во избежание конфликта имен и разрушения работающего кода, добавим новый конструктор Student, в который не будем вкладывать объект Person и не будем вкладывать методы. Методы добавим в прототип функкции Student. Кроме того, рассмотрим способ реализации наследования, который в большей мере соответствует нашим ожиданиям. d = MyDiv("Inheritance between constructors"); function Student(name, age, course = 1) { Person.call(this, name, age); this.course = course; } Student.prototype = Object.create(Person.prototype); //Student.prototype.constructor = Student; Student.prototype.toString = function () { return `Student: ${Person.prototype.toString.call(this)}; Course: ${this.course}`; }; chick = new Student("Chick Corea", 20, 3); d.Add(chick); d.Add(O.getAll(chick)); d.Add("chick instanceof Person", chick instanceof Person); d.Add("chick instanceof Student", chick instanceof Student); d.Add("Person.prototype.isPrototypeOf(chick)", Person.prototype.isPrototypeOf(chick)); d.Add("Student.prototype.isPrototypeOf(chick)", Student.prototype.isPrototypeOf(chick)); var ed = new chick.constructor("Ed Sullivan", 24, 5); d.Add("new chick.constructor() instanceof Person", ed instanceof Person); d.Add("new chick.constructor() instanceof Student", ed instanceof Student); d.Add(O.getAll(ed)); Заставьте браузер выполнить страницу и убедитесь, что теперь объект chick является одновременно экземпляром типа Person и экземпляром типа Student. Секрет успеха кроется в коде, который имеется в нашем фрагменте: Person.call(this, name, age); // Вызов родительской версии конструктора Student.prototype = Object.create(Person.prototype); // Создание прототипа Student на основе прототипа Person Person.prototype.toString.call(this) // Вызов родительской версии toString Первая строка выполняет конструктор Person для объекта типа Student в момент создания этого объекта, а вторая — создает прототип Student на основе прототипа Person. Рассмотрим последние 6 строк результата: chick instanceof Person: true chick instanceof Student: true Person.prototype.isPrototypeOf(chick): true Student.prototype.isPrototypeOf(chick): true new chick.constructor() instanceof Person: true new chick.constructor() instanceof Student: false Первые строки результата соответствуют нашим ожиданиям: созданный объект одновременно является экземпляром двух типов. Однако последняя строка сообщает, что объект, созданный с помощью конструктора объекта chick не является экземпляром типа Student. Для того, чтобы исправить эту ситуацию, уберите комментарий в строке: Student.prototype.constructor = Student; Без этого уточнения конструктором объекта chick считается функция Person, несмотря на то, что ранее мы явно вызвали функцию Student при создании объекта chick = new Student("Chick Corea", 20, 3). Запустите страницу и убеди- Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 тесь, что теперь последний результат равен true. Вывод: в необычном мире JS реализация наследования требует особого внимания. Создание объектов с помощью нотации JSON Добавьте код, иллюстрирующий третий способ создания объектов (формат JSON). document.write("<b>3. Creating object using JSON (JavaScript Object Notation)</b>"); o = { name:"Peter Norton", age:23, toString:Show }; function Show() { return this.name + "; Age: " + this.age + "<br>"; } document.write("<br>" + o); Запустите и попытайтесь объяснить, что стало с панелью MyDiv. Ответ можно найти на сайте JavaScript.ru по теме document.write. Дело в том, что метод write заново загружает HTML-документ. В сети вы найдете сотни примеров, где вся функциональность построена на использовании document.write. Знатоки не рекомендуют пользоваться этим методом. Поэтому удалите последний фрагмент и замените его на: d = new MyDiv("3. Creating object using JSON (JavaScript Object Notation)"); o = { name: "Peter Norton", age: 23, toString: Show }; function Show() { return this.name + "; Age: " + this.age; } d.Add(o); d.Add(O.getAll(o)); Проблема this (Shadowing this) В начальной стадии изучения JS очень важно понять поведение ссылки this. Оно отличается от того, что наблюдается в языках С++ и C#. В JS ссылка this неявным образом всегда передается в любую функцию. Если функция глобальна, то внутри нее this всегда указывает на window, потому что глобальную функцию можно вызвать только для объекта window. Например, контекст вызова setInterval(f, 100) равносилен вызову window.setIntrval(f, 100), и внутри функции f — обработчика события — ссылка this указывает на window. Если функция (назовем ее метод M) является свойством объекта (назовем его Game), то тут возможны варианты. 1. 2. 3. 4. Если метод M вызывается явно внутри другого метода этого же объекта Game, то this указывает на объект Game. Если метод M вызывается в ответ на событие window, то this указывает на объект window. Если метод M вызывается в ответ на событие нажатия кнопки, то this указывает на кнопку (объект DOM). Если метод M вызывается в ответ на событие выбора из выпадающего списка, то this указывает на список. В этом сценарии объект, на который указывает this, зависит от источника события. Мы привыкли считать, что this всегда указывает на объект, которому принадлежит сам метод. Реальность не соответствует нашим ожиданиям и мы создаем неправильный код. Расмотрим реальный пример This.html. <html> <head> <title>Handle This</title> <link rel="icon" type="image/png" href="../images/favicon.png" /> <link href="../Styles.css" rel="stylesheet" /> <script src="../js/Kit.js"></script> <script> var Game = { cell: null, Init: function () { var style = "position=absolute;left=1px;top=1px;width=10px;height=10px;border=1px solid #000;"; Game.cell = $("div", style); setInterval(Game.Move, 500); }, Move() { Game.cell.style.left = (parseInt(Game.cell.style.left) + 20) + "px"; } }; onload = Game.Init; </script> </head> <body></body> </html> Запустите пример, и убедитесь, что div сдвигается вправо каждые полсекунды. Анализируя код, с точки зрения программиста, имеющего опыт разработки программ на языке C#, хочется заменить ссылку Game на this в обоих методах (Init и Move) объекта Game. Но внутри обоих методов ссылка this указывает на window, потому что они вызываются в Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 ответ на события объекта window (window.onload и событие таймера). Покажем традиционный (старый) метод преодоления этого препятствия. Привяжем метод Init к событию onload с помощью метода call. onload = function() { Game.Init.call(Game); } Параметр функции call определяет тот объект, который будет выполнять роль this внутри вызываемой функции Init. Теперь вы можете заменить Game на this внутри функции Init. Для того, чтобы изменить смысл ссылки this внутри функции Move, можно применить тот же прием (явная замена this): setInterval(function () { Game.Move.call(Game) }, 500); Возможен и другой способ: привязать обработчик к событию с помощью лямбда-выражения: setInterval(() => this.Move(), 500); Эта возможность появилась с приходом стандарта ES6. Привязка с помощью лямбда-выражения оставляет ссылку this без изменения. Если this указывала на Game в момент привязки к событию таймера, то эта ссылка будет указывать на тот же объект и в теле функции-обработчика (Move). Теперь можно заменить Game на this внутри функции Move: Move() { this.cell.style.left = (parseInt(this.cell.style.left) + 20) + "px"; } Мое чувство стиля требует также упростить процесс восприятия кода: Move() { var s = this.cell.style; s.left = (parseInt(s.left) + 20) + "px"; } Чтобы быть последовательным, заменим также и способ привязки к событию onload. onload = () => Game.Init(Game); Существуют и другие приемы решения проблемы поведения this. Мы, например, можем воспользоваться эффектом замыкания (closure). Он заключается в том, что обработчик события переносится внутрь функции Init. При этом сам обработчик становится анонимным. Ссылка this предварительно запоминается в другой переменной и она становится доступной внутри обработчика благодаря эффекту замыкания. Уберите метод Move и замените код функции Init. Init: function () { var style = "position=absolute;left=1px;top=1px;width=10px;height=10px;border=1px solid #000;"; this.cell = $("div", style); var me = this; // Запоминаем this setInterval(function () { var s = me.cell.style; s.left = (parseInt(s.left) + 20) + "px"; }, 500); } Приведем еще один способ решения проблемы this. Мы можем воспользоваться тем обстоятельством, что функция setInterval позволяет передавать в функцию обработки дополнительные параметры. Перададим this, примем его и будем работать с ним. В этом случае нет необходимости запоминать ссылку вне обработчика. Init: function () { var style = "position=absolute;left=1px;top=1px;width=10px;height=10px;border=1px solid #000;"; this.cell = $("div", style); setInterval(function (g) { var s = g.cell.style; s.left = (parseInt(s.left) + 20) + "px"; }, 500, this); } При вызове setInterval передано 3 параметра, последним из них является this. Внутри анонимной функции обработки события мы принимаем ссылку this в виде формального параметра g и работаем с ней, как со ссылкой на Game. Однако, самым кратким способом решения проблемы this, на мой взгляд, будет гибридный способ: использование премуществ вложенного обработчика и лямбда-функции. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Init: function () { var style = "position=absolute;left=1px;top=1px;width=10px;height=10px;border=1px solid #000;"; this.cell = $("div", style); setInterval(() => { var s = this.cell.style; s.left = (parseInt(s.left) + 20) + "px"; }, 500); } Рассмотренный случай является слишком простым, так как в обработчик события таймера система не передает параметр типа Event. При обработке других событий этот параметр существует и надо бы знать, как одновременно решать проблему this и не терять контроля над параметром Event. Замените весь код Game, как показано ниже. var Game = { cell: null, Init: function () { var style = "position=absolute;left=1px;top=1px;border=1px solid #000;"; this.cell = $("span", style); var b = $("button", 0, "innerText=Click;", 0, 1); b.onclick = this.Move; }, Move(e) { var s = Game.cell.style; s.left = (parseInt(s.left) + 20) + "px"; Game.cell.innerText = e.target.nodeName; } }; onload = () => Game.Init(Game); Запустите, нажмите на кнопку и убедитесь, что параметр типа MouseEvent приходит внутрь функции обработки события click. Он несет в себе информацию о событии. Мы выделяем из объекта e типа MouseEvent свойство nodeName и выводим его значение в панель. Попутно заметьте, что мы заменили тип панели и не стали указывать ее размеры для того, чтобы они изменялись автоматически. Теперь мы хотим решить проблему this, то есть, заменить внутри функции Move ссылку Game на ссылку this. Первый (сложный) способ — заменить способ привязки к событию и передать два параметра. Первый параметр заменяет смысл this, второй — передает ссылку на объект MouseEvent. b.onclick = function (e) { Game.Move.call(Game, e); } Теперь в теле функции Move можно заменить Game на this. Move(e) { var s = this.cell.style; s.left = (parseInt(s.left) + 20) + "px"; this.cell.innerText = e.target.nodeName; } Второй способ — запомнить this и использовать эффект замыкания. Init: function () { var style = "position=absolute;left=1px;top=1px;border=1px solid #000;"; this.cell = $("span", style); var b = $("button", 0, "innerText=Click;", 0, 1); var me = this; // Запоминаем this b.onclick = function (e) { var s = me.cell.style; s.left = (parseInt(s.left) + 20) + "px"; me.cell.innerText = e.target.nodeName; } } Третий способ — использовать лямбда-функцию, которая не портит this. Init: function () { var style = "position=absolute;left=1px;top=1px;border=1px solid #000;"; this.cell = $("span", style); var b = $("button", 0, "innerText=Click;", 0, 1); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 b.onclick = (e) => { var s = this.cell.style; s.left = (parseInt(s.left) + 20) + "px"; this.cell.innerText = e.target.nodeName; } } Метод apply прототипа Function Метод apply также позволяет задать ссылку this. Он отличается от call тем, что второй аргумент имеет тип Array (передаваемые параметры). Благодаря тому, что второй параметр массив, метод apply позволяет легко объединить два массива a и b, например: [].push.apply(a, b); Следующие трюки позволяют применить методы объекта Array к строке символов, которая не является массивом. Замените последнюю строку кода анонимного обработчика события onclick на: [].join.call('Sparse', ' '); var mm = [].map.call('Sparse', (с) => с.toUpperCase()); this.cell.innerText = mm; // e.target.nodeName; Метод bind прототипа Function Метод bind интересен тем, что он возвращает функцию, поэтому можно упростить первый способ привязки к событию и вместо: b.onclick = function (e) { Game.Move.call(Game, e); } задать: b.onclick = Game.Move.bind(Game); Свойство onclick объекта b должно ссылаться на функцию обработки события. Так как bind возвращает функцию, то мы можем убрать анонимную функцию function (e) { }, которая использована в первом способе привязки. Обратите внимание на то, что параметр e, несущий информацию о событии, будет выработан функцией bind. Наследование от объектов (Prototypal inheritance and dunder proto) В настоящее время (2016 год) понятие прототип в JS имеет двоякое толкование. Это обстоятельство сбивает с толку даже тех, кто имеет опыт разработки кода на JavaScript. Рассмотрим оба толкования. Ранее было показано, что каждый конструктор C имеет свойство prototype. Значением этого свойства является ссылка на объект, который становится шаблоном всех объектов, созданных с помощью операции new C(). Этот пример можно выполнить в окне Console браузера: function C() {} Object.getPrototypeOf(new C()) === C.prototype; // true Prototype Relationship. Связь двух объектов, один из которых является прототипом другого. Пример: var parent = {}; var child = Object.create(parent); Object.getPrototypeOf(child) === parent; // true Объект parent создан с помощью нотации JSON. Конструктор отсутствует. Объект child создается на основе существующего объекта parent, который становится прототипом объекта child. Для создания объекта, который наследует все свойства объекта-прототипа надо либо использовать метод create, который имеется в составе функции Object, либо установить свойство __proto__ (его называют dunder proto) для объекта-наследника. Вновь откройте документ Objects.html. Для иллюстрации описанных возможностей добавьте такой фрагмент. d = new MyDiv("Prototypal inheritance and dunder proto"); var Item = { id: "Item", name: "Unknown", show: function () { return "Item name: " + this.name; } }; var shirt = Object.create(Item, { name: { value: 'Shirt', writable: true } }); var hat = { __proto__: Item, name: 'Hat' }; d.Add(shirt.show()); d.Add(hat.show()); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 d.Add("Item.isPrototypeOf(shirt)", Item.isPrototypeOf(shirt)); d.Add("Item.isPrototypeOf(hat)", Item.isPrototypeOf(hat)); d.Add("shirt.isPrototypeOf(Item)", shirt.isPrototypeOf(Item)); d.Add("Object.getPrototypeOf(shirt)", Object.getPrototypeOf(shirt).id); d.Add("Object.getPrototypeOf(hat)", Object.getPrototypeOf(hat).id); d.Add("The rhs of instanceof operator must be a function"); try { shirt instanceof Item; } catch (e) { d.Add("shirt instanceof Item", e.message); }; Последние две строки иллюстрируют различие в подходе к реализации наследования. Item не является функцией, поэтому, несмотря на то, что объект hat является наследником объекта shirt, мы не можем применить оператор instanceof к объектам, созданным с помощью нотации JSON. Оператор instanceof работает только для объектов, созданных с помощью конструктора. Если бы Item был конструктором, а не объектом-прототипом, то исключение бы не возникло. Только объекты, созданные с помощью конструктора являются экземплярами (instances) прототипа . Изменить и удалить можно только собственное свойство. Удаление унаследованного свойства просто игнорируется, изменение значения свойства проходит, но значение этого же свойства у объекта-родителя останется прежним. Терминология: Двойной смысл прототипа К сожалению, термин прототип имеет два разных смысла в JavaScript: Prototype1: The prototype relationship (Связь по прототипу) Любой объект может быть прототипом другого объекта. Например, объект p является прототипом объекта o. var p = {}; var o = Object.create(p); Object.getPrototypeOf(o) === p // true Prototype2: The value of the property prototype (Значение свойства prototype) Каждый конструктор C имеет прототип (свойство с именем prototype), которое ссылается на объект. Этот объект становится прототипом всех других экземпляров (instances) конструктора. function C() {} Object.getPrototypeOf(new C()) === C.prototype // true Обратите внимание на присутствие операции new, которая создает объект. Разные термины Обычно контекст позволяет различить эти варианты. Для Prototype2 следует придумать другое имя, но это трудно сделать, так как имя prototype уже используется в стандартной библиотеке (например: getPrototypeOf, isPrototypeOf). Для Prototype2 можно было бы использовать термин прототип конструктора, но это проблематично, так как конструкторы тоже имеют прототип, например: function С() {} Object.getPrototypeOf(С) === Function.prototype // true Обратите внимание на отсутствие операции new, поэтому C — это функция. Видимо, термин instance prototype будет лучшим выбором. Итак, предлагается для Prototype1 использовать термин prototype или (prototype relationship), а для Prototype2 — термин instance prototype. Атрибуты свойств: enumerable, configurable, writable, value, getter, setter Добавьте в класс O (файл Kit.js) два новых инструмента: static getData(o) { var s = "<br>[<br>"; for (var p in o) { if (typeof o[p] !== "function") s += `&nbsp;&nbsp;${p}: ${o[p]}<br>`; } s += ']'; return s; } static getMethods(o) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 { var s = "<br>[<br>"; for (var p in o) { if (typeof o[p] === "function") s += `&nbsp;&nbsp;${p}: ${o[p]}<br>`; } s += ']'; return s; } При создании объекта-наследника с помощью метода Object.create существует возможность гибко управлять атрибутами как новых, так и наследуемых свойств. Добавьте фрагмент, иллюстрирующий эту возможность. d = new MyDiv("Inheritance and Defining Properties"); var shoes = Object.create(Item, { name: { value: "Shoes", configurable: true, writable: true }, size: { value: 18, enumerable: true, configurable: true, writable: true }, manufacturer: { value: "Nike", enumerable: false, writable: true } }); d.Add("Properties of shoes (object derived from Item)", O.getData(shoes)); d.Add("JSON.stringify(shoes)", JSON.stringify(shoes)); d.Add("Shoes have manufacturer", `${'manufacturer' in shoes}. It is: ${shoes.manufacturer}. Not shown because of enumerable flag.`); var descr = Object.getOwnPropertyDescriptor(shoes, "manufacturer"); d.Add("Object.getOwnPropertyDescriptor(shoes, 'manufacturer')", O.getData(descr)); В цепочке наследования нового объекта shoes обнаруживается три уровня: shoes-Item-Object. Свойство name объекта shoes переопределяет одноименное свойство, унаследованное от Item. Свойства size и manufacturer являются собственными (own) свойствами объекта shoes. Эти свойства имеют несколько разные атрибуты. Метод getData пользуется циклом for (var p in o), поэтому вы видите как собственные, так и унаследованные перечислимые свойства: size, id, name. Метод stringify объекта JSON отображает только собственные перечислимые свойства объекта, поэтому вы видите только одно свойство size. Если флаг enumerable какого-либо свойства установлен в false, то это свойство не отображается в перечислении, производимом с помощью цикла: for (var p in o) или с помощью метода Object.keys(). Если флаг writable какого-либо свойства установлен в false, то мы не можем изменять значение этого свойства. Если флаг configurable какого-либо свойства установлен в false, то мы не можем удалять это свойство или переопределять его атрибуты. Для того, чтобы разобраться в множестве свойств объекта (своих собственных и унаследованных), добавим в сумку с инструментами вспомогательную функцию, которая разделяет все свойства объекта по уровням иерархии? а также функцию, которая выводит имена ключей свойств, которые доступны с помощью свойства keys прототипа Object. static getAllPropertyNames(o) { var a = []; for (var i = 0; o; i++) { a.Level = i; var goods = []; [].push.apply(goods, Object.getOwnPropertyNames(o)); a.push(goods); o = Object.getPrototypeOf(o); } var s = ""; for (i = 0; i < a.length; i++) { var len = a[i].length; s += `<br>Level = ${i}. Count = ${len}`; for (var j = 0; j < len; j++) { if (j % 5 === 0) s += "<br>&nbsp;&nbsp;&nbsp;&nbsp;"; s += a[i][j] + ", " } } return s; } static showKeys(o) { var keys = Object.keys(o); var s = "Properties keys: "; Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 for (var i = 0; i < keys.length; i++) s += keys[i] + ", "; return s; } Добавьте в функцию Init фрагмент, который проводит анализ свойств объекта shoes. d = new MyDiv("Getting All Property Names and Defining Object"); d.Add("getAllPropertyNames(shoes)", O.getAllPropertyNames(shoes)); Вызов getAllPropertyNames производит такой результат: Level = 0. Count = 3 name, size, manufacturer, Level = 1. Count = 3 id, name, show, Level = 2. Count = 12 constructor, hasOwnProperty, propertyIsEnumerable, isPrototypeOf, toLocaleString, toString, valueOf, __proto__, __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__, Три свойства нулевого уровня являются собственными свойствами объекта shoes. Три свойства первого уровня унаследованы от объекта Item. Заметьте, что свойство name переопределено. Двенадцать свойств второго уровня унаследованы от объекта Object.prototype, а этот объект уже не имеет прототипа и его свойство __proto__ равно null. Управление свойствами d = new MyDiv("Defining properties"); Object.defineProperty(o, "Nick", { value: "Sasha", configurable: true, enumerable: true, writable: true }); Object.defineProperty(o, "Father", { value: "Anatoly", configurable: false, enumerable: true }); Object.defineProperty(o, "toString", { value: function () { return this.name + "; Age: " + this.age; } }); d.Add(o); d.Add("o.Nick (configurable: true)", o.Nick); d.Add("o.Father (configurable: false)", o.Father); d.Add(O.showKeys(o)); var descr = Object.getOwnPropertyDescriptor(o, "Father"); d.Add("Object.getOwnPropertyDescriptor(o, 'Father')", O.getData(descr)); o.Father = "Victor"; d.Add("After o.Father = 'Victor' the property is", `${o.Father}, because of writable flag`); Определение владельца свойства и копирование объектов Object имеет свойство constructor. Мы уже видели, как пользоваться этим свойством для создания объектов. При большой глубине дерева наследования возникает проблема: как узнать объект, от которого унаследовано то или иное свойство. Следующая утилита решает ее, поэтому добавьте ее в сумку с инструментами. static getDefiningObject(o, key) { o = Object(o); while (o && !{}.hasOwnProperty.call(o, key)) o = Object.getPrototypeOf(o); return o; } Свойство hasOwnProperty прототипа Object позволяет выделить из всех свойств только те, которые заданы непосредственно данному объекту, а не получены из прототипа. Добавьте в Init фрагмент, который проверяет работу функции getDefiningObject. d.Add("getDefiningObject(shoes,'id')", O.getDefiningObject(shoes, 'id').id); d.Add("getDefiningObject(shoes,'toString')", O.getDefiningObject(shoes, 'toString')); Теперь можно отыскать и удалить любое свойство, например: delete O.getDefiningObject(hat, 'id').id; Еще одна функция позволит нам копировать объекты с любой глубиной погружения в иерархическое дерево. Также добавьте ее в библиотеку полезных утилит Kit.js. static copyObject(src) { var copy = Object.create(Object.getPrototypeOf(src)); Object.getOwnPropertyNames(src).forEach(function (key) { Object.defineProperty(copy, key, Object.getOwnPropertyDescriptor(src, key)); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 }); return copy; } Возможность создания копии объекта проверьте с помощью фрагмента: d.Add("Properties of сopyObject(shoes)", O.getAllPropertyNames(O.copyObject(shoes))); Структура Object станет более понятной, если вы проведете анализ результатов, полученных с помощью кода: d = new MyDiv("Hierarchy of Object"); d.Add("getAllPropertyNames(Object)", O.getAllPropertyNames(Object)); d = new MyDiv("Hierarchy of new Object()"); d.Add("getAllPropertyNames(new Object())", O.getAllPropertyNames(new Object())); d = new MyDiv("Hierarchy of Object prototype"); d.Add("getAllPropertyNames(Object.prototype)", O.getAllPropertyNames(Object.prototype)); Вспомните, что Object — конструктор (то есть, функция), а new Object() — объект. Между Object и Object.prototype имеется посредник Function. Конструктор Object создает свойство __proto__ (ссылка на Function), оно, в свою очередь, имеет свойство __proto__, ссылающееся на Object.prototype. Объясните результаты следующего фрагмента: d = new MyDiv("Hierarchy of Function"); d.Add("getAllPropertyNames(Function)", O.getAllPropertyNames(Function)); d = new MyDiv("Hierarchy of new Function()"); d.Add("getAllPropertyNames(new Function())", O.getAllPropertyNames(new Function())); Обе сущности являются функциями, поэтому результаты идентичны. Объясните результат опроса Function.prototype. d = new MyDiv("Hierarchy of Function.prototype"); d.Add("getAllPropertyNames(Function.prototype)", O.getAllPropertyNames(Function.prototype)); Эксперимент показал, что иерархия рассмотренных сущностей одинакова в разных браузерах. Небольшое отличие состоит в том, что IE не имеет свойство name, которое упрощает анализ результатов. Исследование Array показывает, что отличий в структуре этой сущности больше, а именно: кроме свойства name, Chrome имеет методы observe и unobserve, которых нет в IE. Любопытно рассмотреть иерархию объекта window. d = MyDiv("Hierarchy of window object"); d.Add("getAllPropertyNames(window)", O.getAllPropertyNames(window)); Она различна в разных браузерах. В браузере IE: window - window.prototype - Object. В браузере Google Chrome: window - window.prototype – EventTarget - Object. One Page site Для разработки небольших сайтов весьма популярным подходом является технология One Page site. Проиллюстрируем идею этого подхода в рамках нашей страницы Objects.html. Страница уже перенасыщена информацией. В соответствии с концепцией One Page site мы создадим иллюзию перехода к другой странице, на самом деле оставаясь в рамках той же самой страницы. В функцию Init нашего документа добавьте фрагмент, который добавляет кнопку перехода. $("input", 0, "type=button;value='Click me to go on...;'").onclick = GoOn; Ее нажатие вызовет функцию GoOn, которая создаст иллюзию перехода к новому документу. На самом деле мы просто уничтожим все содержимое документа (все наши панели типа MyDiv) и создадим новые. Также добавим кнопку, которая позволит создать иллюзию перехода назад. Добавьте глобальную функцию GoOn. function GoOn() { O.clear(); $("input",0,"type=button;value=Go back;").onclick= () => { O.clear(); Init(); } } В теле функции мы уничтожаем все текущее (старое) содержимое и вместо него вставляем кнопку возврата, при нажатии которой удаляется новое содержимое и вновь генерируется старое. Это делает функция Init. Новый код будем добавлять в функцию GoOn между вызовами O.clear и $. Проверьте работу кнопок и продолжим исследование свойств объектов JS. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Изменяем прототип String Как изменить прототип встроенного в JS конструктора String? Добавьте в начало файла Kit.js следующий код. String.prototype.Clean = function() { return this.trim().replace(/\s+/g, " "); } String.prototype.Contains = function(s, b) { return (b) ? (b + this + b).indexOf(b + s + b) > -1 : this.indexOf(s) > -1; } String.prototype.Enclose = function(c) { return c + this + c; } String.prototype.ToHtml = function (c) { return '<' + c + '>' + this + '</' + c + '>'; } В прототип String добавлено 4 метода. Заметьте, что метод Clean использует цепочный вызов функций. Следующий код служит для проверки новых возможностей объекта String. Вставьте его в функцию GoOn после вызова O.clear(), но перед созданием копки возврата. d = new MyDiv("Clean, Contains and Enclose methods added to String.prototype"); var ss = " Test text ", st = ss.trim(), sc = ss.Clean(); d.Add(`<pre>'${ss}' : ${ss.length} chars '${st}' : ${st.length} chars '${sc}' : ${sc.length} chars</pre>`); var s = "Attributes attract attorneys"; d.Add(s.Enclose("'") + " contains " + "at".Enclose("'") + ": " + s.Contains("at")); d.Add(s.Enclose("'") + " contains " + "at".Enclose("'") + ": " + s.Contains("at", " ")); s += " at "; d.Add(s.Enclose("'") + " contains " + "at".Enclose("'") + ": " + s.Contains("at", " ")); function ShowFact(n) { var s = ""; for (var i = 1, p = 1; i < n; i++, p *= i) s += i + "*"; s += i + "=" + p; return s; } d.Add("ShowFact(5).ToHtml('span')", ShowFact(5).ToHtml("span")); d.Add("ShowFact(5).ToHtml('h1')", ShowFact(5).ToHtml("h1")); Accessors (getters and setters) Сравнительно новая конструкция JavaScript, похожая на properties языка C#, называется Accessors (getters and setters). Вставьте следующий фрагмент в нужное место функции GoOn. d = new MyDiv("Accessors (Getters and Setters)"); var alex = { name: "", birth: Date(), get Name() { return this.name; }, set Name(value) { value = value.Clean().replace(/d+/, " "); var matches = /([A-Za-zА-Яа-я]+) ([A-Za-zА-Яа-я]+) ([A-Za-zА-Яа-я]+)/i.exec(value); if (matches === null) value = "undefined"; else value = matches[0]; this.name = value; }, get Birth() { return this.birth; }, set Birth(value) { value = new Date(value); if (`${value}` === "Invalid Date" || Date.parse(`${value}`) !== value.valueOf()) value = new Date(); this.birth = value; } }; alex.Name = "*&*& Alex Black Jr55 ?:% "; alex.Birth = "2000-02-26"; d.Add(`Name = ${alex.Name}, Birth = ${alex.Birth.toDateString()}`); d.Add("LocaleDateString(['ru-RU'])", alex.Birth.toLocaleDateString(["ru-RU"])); alex.Birth = new Date(1947, 01, 26); d.Add("new Date(1947, 01, 26)", alex.Birth.toDateString()); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 d.Add("LocaleDateString(['ru-RU'])", alex.Birth.toLocaleDateString(["ru-RU"])); alex.Birth = "wrong"; d.Add("After assigning 'wrong' Date, alex.Birth", alex.Birth.toDateString()); alex.Birth = null; d.Add("After assigning null Date, alex.Birth", alex.Birth.toDateString()); Обратите внимание на особенности синтаксиса аксессоров get и set, а также на приемы работы с типом данных Date и RegExp. Выражение: /([A-Za-zА-Яа-я]+) ([A-Za-zА-Яа-я]+) ([A-Za-zА-Яа-я]+)/i является литералом, который создает объект типа RegExp (регулярное выражение). Мы намеренно задали искаженное имя и дату с тем, чтобы показать, как можно их скорректировать с помощью аксессоров set. Внутри тела аксессора set Name мы пользуемся методом exec, который возвращает коллекцию совпадений с шаблоном объекта RegExp. Аксессоры, как и другие свойства объекта наследуются. Обработка исключений При обработке исключительных ситуаций вы можете пользоваться свойством stack объекта Error. Следующий фрагмент показывает, как выбросить и обработать исключение. d = new MyDiv("Exception handling"); function TestExceptionStack() { try { ImitateError(); } catch (e) { d.Add(e.stack.replace(/\n/g, "<br>&nbsp;&nbsp;")); } finally { d.Add("Block finally is executed in any case..."); } } function ImitateError() { throw new Error("Imitating Bad Error..."); } TestExceptionStack(); d.Add("<b>Exception types</b>:"); var exceptions = [ function () { throw Error("My Imitated Error"); }, function () { new Array(-3); }, // RangeError: Array length must be a finite positive integer function () { xxxx; }, // ReferenceError: 'xxxx' is undefined function () { undefined.n; }, // TypeError: Unable to get property 'n' of undefined or null reference function () { eval('3 +'); } // SyntaxError: Syntax error ]; for (var i = 0; i < exceptions.length; i++) { try { exceptions[i](); } catch (e) { var s = e.stack; d.Add(e.constructor) d.Add(s.slice(0, s.indexOf(':') + 2) + e.message); } } Этот пример показывает, как извлечь информацию из свойства constructor объекта e. Кроме того, здесь работает массив ссылок на функции (см. определение exceptions). Преобразование объекта в строку формата JSON и обратно Сейчас мы добавим несколько функций, которые преобразуют произвольный объект в текстовую строку формата JSON. JSON-формат весьма успешно конкурирует с XML и, скорее всего, заменит его в недалеком будущем. Строку текста в формате JSON можно передать в любой другой HTML-документ и с помощью JS возродить из нее оригинальный объект. Это иллюстрирует замечательную особенность языков динамического программирования (каковым JavaScript и является) — возможность на лету создавать объекты с иерархической структурой из простого текста, и, наоборот, преобразовывать произвольный объект в обычный текст. Откройте сумку с любимыми инструментами (файл Kit.js) и добавьте в нее две функции function ObjectToString(o) { var val, res = ""; if (typeof (o) == "undefined") return res; res += "{"; for (var i in o) { res += i + ":"; val = o[i]; if (val instanceof Date) Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 res += "new Date('" + val.toString() + "')"; else { switch (typeof val) { case "object": res += (val[0] ? ArrayToString(val) : ObjectToString(val)); break; case "string": res += "'" + escape(val) + "'"; break; default: res += val; } } res += ","; } res = res.substring(0, res.length - 1) + "}"; return res; } Внимательно рассмотрите код, убедитесь в том, что здесь есть прямая и косвенная рекурсии. Мысленно выполните преобразование (пройдите по коду) для простого объекта типа Person. Попробуйте написать на листе бумаги текст, в который будет преобразован объект. Добавьте в Kit.js две небольшие функции, которые производят обратное преобразование (возрождение объекта из текстовой строки). function StringToObject(text) { eval("var res = " + unescape(text)); return res; } function StringToArray(text) { eval("var res = " + text); return res; } Не сразу ясно, откуда берется объект res. Прочтите справку по функциям eval, escape и unescape. Откройте файл Objects.html и добавьте в нужное место функции GoOn такой код. var d = new MyDiv("Converting Object To String and back"); d.Add("<b>String image of an object:</b><br>"); var text = ObjectToString(alex); d.Add(text); var o = StringToObject(text); d.Add("<b>Resurrected Object's properties:</b>"); d.Add(O.getData(o)); В этот момент мне пришлось вручную стирать кэш браузера, так как он не видел новых функций в Kit.js. Для стирания кэша надо перегрузить страницу браузера с помощью комбинации Ctrl+F5. Запустите проект и произведите тщательный анализ результата. Попытайтесь упростить ту ветвь обработки свойств объекта, которая работает с датой. Кроме текущего варианта: res += "new Date('" + val.toString() + "')"; возможен другой (более простой?!): res += "new Date('" + val.toJSON() + "')"; Мне кажется, что существуют еще более простые варианты. FP approach (callback): [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => n*n); There’s no need to think about the low-level details of a sorting algorithm (or even which sorting algorithm to choose). We provide a callback that the JavaScript engine will call every time it needs to compare two items. var values = [0, 3, 2, 5, 7, 4, 8, 1]; values.sort((a,b)=>b-a); The functional approach allows us to create a function as a standalone entity, just as we can any other object type, and to pass it as an argument to a method, just like any other object type, which can accept it as a parameter, just like any other object type. Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 Пример сортироки строк таблицы <html> <head> <title>BarCodes</title> <link rel="icon" type="image/png" href="../Images/favicon.png" /> <style> body { font-family: Trebuchet MS, Arial,Helvetica,sans-serif; font-size: 80%; padding: 0px 10px; } table { border: 1px solid #aaa; border-collapse: collapse; } thead { background-color: #555; } th { padding: 1px 20px; color: #6f6; text-align: center; font-weight: bold; min-width: 60px; cursor: pointer; } tr.normal td { background: #fff; border-bottom: none; border-left: none; border-right: 1px solid #ccc; border-top: 1px solid #ddd; padding: 1px 20px; min-width: 60px; } tr.alt td { background: #eee; border-bottom: none; border-left: none; border-right: 1px solid #ccc; border-top: 1px solid #ddd; padding: 1px 20px; min-width: 60px; } h1 { color: #ffc; background-color: #900; font-size: 1.6em; margin: 0; margin-bottom: 0.5em; padding: 0.25em; text-align: center; letter-spacing: 0.08em; border-bottom-style: solid; border-bottom-width: 0.2em; border-bottom-color: #c00; } div.container { clear: both; height: 285px; overflow: hidden; width: 340px; } div.container table { overflow: hidden; float: left; } .fix { display: block; } tbody.scrol { display: block; height: 262px; overflow: auto; width: 100% } </style> <script src="../JS/Kit.js"></script> <script> class BarCodes { constructor() { this.rows = []; this.tb = null; this.SetHTML(); this.CodeCountry(); this.SetTable(); this.order = "asc"; } CodeCountry() { let data = `100–139,United States; 300–379,France; 300–379,Monaco; 380,Bulgaria; 383,Slovenia; 385,Croatia; 387,Bosnia; 389,Montenegro; 390,Kosovo; 400–440,Germany; 450–459,Japan; 460–469,Russia; 470,Kyrgyzstan; 471,Taiwan; 474,Estonia; 475,Latvia; 476,Azerbaijan; 477,Lithuania; 478, Uzbekistan; 479,Sri Lanka; 480,Philippines; 481,Belarus; 482,Ukraine; 483,Turkmenistan; 484,Moldova; 485,Armenia; 486,Georgia; 487,Kazakhstan; 488,Tajikistan; 489,Hong Kong; 490–499,Japan; 500–509,United Kingdom; 520–521,Greece; 528,Lebanon; 529,Cyprus; 530,Albania; 531,Macedonia; 535,Malta; 539,Ireland; 540–549,Belgium; 540–549,Luxembourg; 560,Portugal; 569,Iceland; 570–579,Denmark; 570–579,Faroe Islands; 570–579,Greenland; 590,Poland; 594,Romania; 599,Hungary; 600–601,South Africa; 603,Ghana; 604,Senegal; 608,Bahrain; 609,Mauritius; 611,Morocco; 613,Algeria; 615,Nigeria; 616,Kenya; 618, Cote d'Ivoire; 619,Tunisia; 620,Tanzania; 621,Syria; 622,Egypt; 623,Brunei; 624, Libya; 625,Jordan; 626,Iran; 627,Kuwait; 628,Saudi Arabia; 629,United Arab Emirates; 640–649,Finland; 690–699,China; 700–709,Norway; 729,Israel; 730–739,Sweden; 740,Guatemala; 741,Salvador; 742,Honduras; 743,Nicaragua; 744,Costa Rica; 745,Panama; 746,Dominican Republic; 750,Mexico; 754–755,Canada; 759,Venezuela; 760–769,Switzerland; 760–769,Liechtenstein; 770–771,Colombia; 773,Uruguay; 775,Peru; 777,Bolivia; 778–779,Argentina; 780,Chile; 784,Paraguay; 786,Ecuador; 789–790,Brazil; 800–839,Italy; 800–839,San Marino; 800–839,Vatican City; 840–849,Spain; 840–849,Andorra; 850,Cuba; 858,Slovakia; 859,Czech Republic; 860,Serbia; 865,Mongolia; 867,North Korea; 868–869,Turkey; 870–879,Netherlands; 880,South Korea; 884,Cambodia; 885,Thailand; 888,Singapore; 890,India; 893,Vietnam; 894,Bangladesh; 896,Pakistan; 899,Indonesia; 900–919,Austria; 930–939,Australia; 940–949,New Zealand; 955,Malaysia; 958,Macau`; let d = data.split(';'); for (var i = 0; i < d.length; i++) { let c = d[i].split(','); this.rows.push({ code: c[0].trim(), country: c[1].trim() }); } } SetTable() { this.tb.innerHTML = ""; for (var i = 0; i < this.rows.length; i++) { let row = this.rows[i], cn = (i & 1) === 0 ? "normal" : "alt", r = $("tr", 0, `className=${cn}`, this.tb); Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 $("td", 0, `innerText=${row.code}`, r); $("td", 0, `innerText=${row.country}`, r); } } SortColumn(col) { this.order = this.order === "asc" ? "desc" : "asc"; this.rows.sort((x, y) => { let a = col === 0 ? x.code : x.country, b = col === 0 ? y.code : y.country, v = this.order === "asc" ? 1 : -1; return a > b ? v : a < b ? -v : 0; }); this.SetTable(); } SetHTML() { $("h1", 0, "innerText=BarCodes of 121 countries"); let d = $("div", 0, "className=container;"), table = $("table", 0, 0, d), th = $("thead", 0, "className=fix;", table), r = $("tr", 0, 0, th), c = $("th", 0, "innerText=Code;", r); c.onclick = () => this.SortColumn(0); c = $("th", 0, "innerText=Country;", r); c.onclick = () => this.SortColumn(1); this.tb = $("tbody", 0, "className=scrol;", table); } } onload = () => new BarCodes(); </script> </head> <body></body> </html> Обратите внимание на способ заполнения коллекции объектов: this.rows.push({ code: c[0].trim(), country: c[1].trim() }); Также попытайтесь запомнить прием, с помощью которого можно передать внутрь метода sort адрес функции сравнения двух объектов сортируемой коллекции. Вместо адреса функции мы передаем лямбда выражение с двумя параметрами (x, y), которые будут выполнять роль ссылок на сравниваемые объекты. SortColumn(col) { this.order = this.order === "asc" ? "desc" : "asc"; this.rows.sort((x, y) => { let a = col === 0 ? x.code : x.country, b = col === 0 ? y.code : y.country, v = this.order === "asc" ? 1 : -1; return a > b ? v : a < b ? -v : 0; }); this.SetTable(); } Демонстрация приемов работы с канвой <!DOCTYPE html> <html> <head> <title>CanvasTest</title> <link rel="icon" type="image/png" href="../images/favicon.png"> <style> body { font-family: Trebuchet MS, Verdana, sans-serif; font-size: 90%; margin: 20px; } h1 { color: #ffc; background-color: #900; font-size: 1.6em; margin: 0; padding: 0.25em; text-align: center; letter-spacing: 0.08em; border-bottom-style: solid; border-bottom-width: 0.2em; border-bottom-color: #c00; } #dv:hover .move { transform: translate(365px,-297px) rotate(360deg) scale(0.5); transition: all 2s ease; } .pos { position: absolute; top: 67%; left: 0; } #sp { transform: translate(50px, 60px) scale(.75, .75) rotate(35deg); transform-origin: 60% 100%; } input[type=button] { opacity: 0.9; padding: 4px; border: 0; color: white; background: #e22; width: 8em; display: inline-block; cursor: pointer; border-radius: 4px; } input[type=button]:hover { opacity: 1; transform: scale(1.1); } Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 </style> <script src="../js/Kit.js"></script> <script> class ImageRect { constructor(canvas, size, path) { this.canvas = canvas; this.w = canvas.width; this.h = canvas.height; this.img = new Image(size, size); this.img.src = path; this.img.onload = () => this.Draw(); this.angle = 0; } Draw() { let dc = this.canvas.getContext("2d"); dc.fillStyle = "#fed"; dc.fillRect(0, 0, this.w, this.h); dc.save(); // Draw 200 rectangles dc.strokeRect(0, 0, 400, 400); var a = Math.PI / 20, c = Math.PI / 4; dc.translate(200, 200); for (var i = 1; i < 200; i++) { dc.rotate(a); var coeff = Math.sin(c) / Math.sin(3 * c - a); dc.scale(coeff, coeff); dc.strokeRect(-200, -200, 400, 400); } dc.restore(); dc.save(); // Rotate and draw Image let shift = 50, sz = this.img.width, rad = shift + sz / 2; dc.translate(rad, rad); dc.rotate(this.angle); dc.translate(-rad, -rad); dc.drawImage(this.img, shift, shift, sz, sz); dc.restore(); } } class ImageMouse { constructor(canvas) { this.canvas = canvas; let dc = this.canvas.getContext("2d"); this.img = new Image(); this.img.src = "../images/PumpkinBlackCat.png"; this.img.onload = () => this.Draw(dc, 190, 200); this.canvas.onmousemove = (e) => { let t = e.target; this.Draw(dc, e.x - t.offsetLeft, e.y - t.offsetTop); } } Draw(dc, x, y) { dc.clearRect(0, 0, dc.canvas.width, dc.canvas.height); dc.font = "24px Arial"; dc.fillText("Rotate image with the mouse", 20, 40); dc.save(); dc.translate(190, 200); dc.rotate(Math.atan2(y - 190, x - 200)); dc.drawImage(this.img, 0, 0, 160, 140); dc.restore(); } } class App { constructor() { Введение в JavaScript. Черносвитов А.В. (Alexcher@Avalon.ru) © 2005-2017 $("h1", 0, "innerText=Canvas & CSS Demo"); let canvas = $("canvas", "border=1px solid #aaa;", "width=400;height=400;"), btn = $("input", "position=absolute;left=100px;top=430px;", "type=button;value=Rotate;"), rect = new ImageRect(canvas, 150, "../images/Boots.gif"), div = $("div", "background=#eee;position=absolute;left=425px;top=65px;width=400px;height=355px;border=1px solid #aaa; font-size=20pt;padding=20px;text-align=center;", "id=dv;innerText=Hover the mouse here"), img = $("img", "width=100px;", "src=../images/Absolut.jpg;className=pos move;", div), sp = $("span", "background=#abf;position=absolute;left=100px;top=20px;width=80px;height=25px;padding=10px;fontsize=1.5em;", "id=sp;"), timerID = -1, toDeg = 180 / Math.PI, canv = $("canvas", "border=1px solid #aaa;position=absolute;left=865px;top=65px;", "width=350;height=395;"), imgMouse = new ImageMouse(canv), rotating = false; btn.onclick = () => { rotating = !rotating; if (rotating) { timerID = setInterval(() => { rect.angle += 0.01; rect.Draw(); sp.innerHTML = `${(rect.angle * toDeg).toPrecision(4)}&deg;`; // &#186; // you may use code }, 10); btn.value = "Stop"; } else { clearTimeout(timerID); btn.value = "Rotate"; } } setTimeout(() => btn.click(), 1000); } } onload = () => new App(); </script> </head> <body></body> </html>