Язык Javascript
Язык Javascript
Язык JavaScript
Илья Кантор
Сборка от 8 ноября 2024 г.
Последняя версия учебника находится на сайте [Link]
Мы постоянно работаем над улучшением учебника. При обнаружении ошибок пишите о них на нашем баг-трекере.
●
Введение
● Введение в JavaScript
● Справочники и спецификации
●
Редакторы кода
● Консоль разработчика
● Основы JavaScript
●
Привет, мир!
● Структура кода
●
Строгий режим — "use strict"
● Переменные
● Типы данных
● Взаимодействие: alert, prompt, confirm
● Преобразование типов
●
Базовые операторы, математика
●
Операторы сравнения
● Условное ветвление: if, '?'
● Логические операторы
● Операторы нулевого слияния и присваивания: '??', '??='
● Циклы while и for
● Конструкция "switch"
● Функции
● Function Expression
● Стрелочные функции, основы
● Особенности JavaScript
●
Качество кода
● Отладка в браузере
●
Советы по стилю кода
● Комментарии
● Ниндзя-код
● Автоматическое тестирование c использованием фреймворка Mocha
● Полифилы
● Объекты: основы
● Объекты
● Копирование объектов и ссылки
● Сборка мусора
● Методы объекта, "this"
● Конструктор, оператор "new"
● Опциональная цепочка '?.'
● Тип данных Symbol
● Преобразование объектов в примитивы
● Типы данных
●
Методы примитивов
● Числа
● Строки
● Массивы
● Методы массивов
● Перебираемые объекты
● Map и Set
● WeakMap и WeakSet
1/597
●
[Link], values, entries
● Деструктурирующее присваивание
● Дата и время
● Формат JSON, метод toJSON
● Продвинутая работа с функциями
● Рекурсия и стек
● Остаточные параметры и оператор расширения
● Область видимости переменных, замыкание
● Устаревшее ключевое слово "var"
● Глобальный объект
● Объект функции, NFE
● Синтаксис "new Function"
●
Планирование: setTimeout и setInterval
● Декораторы и переадресация вызова, call/apply
● Привязка контекста к функции
● Повторяем стрелочные функции
● Свойства объекта, их конфигурация
● Флаги и дескрипторы свойств
● Свойства - геттеры и сеттеры
●
Прототипы, наследование
● Прототипное наследование
●
[Link]
●
Встроенные прототипы
● Методы прототипов, объекты без свойства __proto__
●
Классы
● Класс: базовый синтаксис
● Наследование классов
●
Статические свойства и методы
● Приватные и защищённые методы и свойства
●
Расширение встроенных классов
●
Проверка класса: "instanceof"
● Примеси
●
Обработка ошибок
● Обработка ошибок, "try..catch"
● Пользовательские ошибки, расширение Error
●
Промисы, async/await
● Введение: колбэки
●
Промисы
●
Цепочка промисов
● Промисы: обработка ошибок
●
Promise API
● Промисификация
● Микрозадачи
●
Async/await
● Генераторы, продвинутая итерация
●
Генераторы
●
Асинхронные итераторы и генераторы
● Модули
●
Модули, введение
●
Экспорт и импорт
● Динамические импорты
●
Разное
2/597
●
Proxy и Reflect
● Eval: выполнение строки кода
●
Каррирование
●
Ссылочный тип
● Побитовые операторы
●
BigInt
● Юникод, внутреннее устройство строк
●
Intl: интернационализация в JavaScript
●
WeakRef и FinalizationRegistry
3/597
Здесь вы можете изучить JavaScript, начиная с нуля и заканчивая продвинутыми концепциями вроде ООП.
Введение
Про язык JavaScript и окружение для разработки на нём.
Введение в JavaScript
Давайте посмотрим, что такого особенного в JavaScript, чего можно достичь с его помощью и какие другие технологии
хорошо с ним работают.
Программы на этом языке называются скриптами. Они могут встраиваться в HTML и выполняться автоматически при
загрузке веб-страницы.
Скрипты распространяются и выполняются, как простой текст. Им не нужна специальная подготовка или компиляция
для запуска.
Почему JavaScript?
Когда JavaScript создавался, у него было другое имя – «LiveScript». Однако, язык Java был очень популярен в то
время, и было решено, что позиционирование JavaScript как «младшего брата» Java будет полезно.
Сегодня JavaScript может выполняться не только в браузере, но и на сервере или на любом другом устройстве,
которое имеет специальную программу, называющуюся «движком» JavaScript .
У браузера есть собственный движок, который иногда называют «виртуальная машина JavaScript».
Эти названия полезно знать, так как они часто используются в статьях для разработчиков. Мы тоже будем их
использовать. Например, если «функциональность X поддерживается V8», тогда «Х», скорее всего, работает в
Chrome, Opera и Edge.
Движок применяет оптимизации на каждом этапе. Он даже просматривает скомпилированный скрипт во время его
работы, анализируя проходящие через него данные, и применяет оптимизации к машинному коду, полагаясь на
полученные знания. В результате скрипты работают очень быстро.
Возможности JavaScript сильно зависят от окружения, в котором он работает. Например, [Link] поддерживает
функции чтения/записи произвольных файлов, выполнения сетевых запросов и т.д.
4/597
В браузере для JavaScript доступно всё, что связано с манипулированием веб-страницами, взаимодействием с
пользователем и веб-сервером.
Современные браузеры позволяют ему работать с файлами, но с ограниченным доступом, и предоставляют его,
только если пользователь выполняет определённые действия, такие как «перетаскивание» файла в окно браузера
или его выбор с помощью тега <input> .
Это называется «Политика одинакового источника» (Same Origin Policy). Чтобы обойти это ограничение, обе
страницы должны согласиться с этим и содержать JavaScript-код, который специальным образом обменивается
данными.
Это ограничение необходимо, опять же, для безопасности пользователя. Страница [Link] ,
которую открыл пользователь, не должна иметь доступ к другой вкладке браузера с URL [Link] и
воровать информацию оттуда.
● JavaScript может легко взаимодействовать с сервером, с которого пришла текущая страница. Но его способность
получать данные с других сайтов/доменов ограничена. Хотя это возможно в принципе, для чего требуется явное
согласие (выраженное в заголовках HTTP) с удалённой стороной. Опять же, это ограничение безопасности.
[Link]
[Link] [Link]
<script>
...
</script>
5/597
Подобные ограничения не действуют, если JavaScript используется вне браузера, например — на сервере.
Современные браузеры предоставляют плагины/расширения, с помощью которых можно запрашивать
дополнительные разрешения.
●
Полная интеграция с HTML/CSS.
●
Простые вещи делаются просто.
● Поддерживается всеми основными браузерами и включён по умолчанию.
JavaScript – это единственная браузерная технология, сочетающая в себе все эти три вещи.
Вот что делает JavaScript особенным. Вот почему это самый распространённый инструмент для создания
интерфейсов в браузере.
Хотя, конечно, JavaScript позволяет делать приложения не только в браузерах, но и на сервере, на мобильных
устройствах и т.п.
Синтаксис JavaScript подходит не под все нужды. Разные люди хотят иметь разные возможности.
Это естественно, потому что проекты разные и требования к ним тоже разные.
Так, в последнее время появилось много новых языков, которые транспилируются (конвертируются) в JavaScript,
прежде чем запустятся в браузере.
Современные инструменты делают транспиляцию очень быстрой и прозрачной, фактически позволяя разработчикам
писать код на другом языке, автоматически преобразуя его в JavaScript «под капотом».
Есть и другие. Но даже если мы используем один из этих языков, мы должны знать JavaScript, чтобы действительно
понимать, что мы делаем.
Итого
●
JavaScript изначально создавался только для браузера, но сейчас используется на многих других платформах.
●
Сегодня JavaScript занимает уникальную позицию в качестве самого распространённого языка для браузера,
обладающего полной интеграцией с HTML/CSS.
●
Многие языки могут быть «транспилированы» в JavaScript для предоставления дополнительных функций.
Рекомендуется хотя бы кратко рассмотреть их после освоения JavaScript.
Справочники и спецификации
Эта книга является учебником и нацелена на то, чтобы помочь вам постепенно освоить язык. Но когда вы хорошо
изучите основы, вам понадобятся дополнительные источники информации.
6/597
Спецификация
Вначале спецификация может показаться тяжеловатой для понимания из-за слишком формального стиля изложения.
Если вы ищете источник самой достоверной информации, то это правильное место, но она не для ежедневного
использования.
Новая версия спецификации появляется каждый год. А пока она не вышла официально, все желающие могут
ознакомиться с текущим черновиком на [Link] .
Чтобы почитать о самых последних возможностях, включая те, которые «почти в стандарте» (так называемые «stage
3 proposals»), посетите [Link] .
Если вы разрабатываете под браузеры, то существуют и другие спецификации, о которых рассказывается во второй
части этого учебника.
Справочники
●
MDN (Mozilla) JavaScript Reference – это справочник с примерами и другой информацией. Хороший источник для
получения подробных сведений о функциях языка, методах встроенных объектов и так далее.
Хотя зачастую вместо их сайта удобнее использовать какой-нибудь интернет-поисковик, вводя там запрос «MDN
[что вы хотите найти]», например [Link] для поиска информации о функции
parseInt .
Таблицы совместимости
Посмотреть, какие возможности поддерживаются в разных браузерах и других движках, можно в следующих
источниках:
●
[Link] – таблицы с информацией о поддержке по каждой возможности языка. Например, чтобы
узнать, какие движки поддерживают современные криптографические функции, посетите:
[Link] .
●
[Link] – таблица с возможностями языка и движками, которые их поддерживают и
не поддерживают.
Все эти ресурсы полезны в ежедневной работе программиста, так как они содержат ценную информацию о
возможностях использования языка, их поддержке и так далее.
Пожалуйста, запомните эти ссылки (или ссылку на эту страницу) на случай, когда вам понадобится подробная
информация о какой-нибудь конкретной возможности JavaScript.
Редакторы кода
Есть два основных типа редакторов: IDE и «лёгкие» редакторы. Многие используют по одному инструменту каждого
типа.
IDE
Термином IDE (Integrated Development Environment, «интегрированная среда разработки») называют мощные
редакторы с множеством функций, которые работают в рамках целого проекта. Как видно из названия, это не просто
редактор, а нечто большее.
IDE загружает проект (который может состоять из множества файлов), позволяет переключаться между файлами,
предлагает автодополнение по коду всего проекта (а не только открытого файла), также она интегрирована с
системой контроля версий (например, такой как git ), средой для тестирования и другими инструментами на уровне
всего проекта.
7/597
● Visual Studio Code (кросс-платформенная, бесплатная).
●
WebStorm (кросс-платформенная, бесплатная для некоммерческого использования).
Для Windows есть ещё Visual Studio (не путать с Visual Studio Code). Visual Studio – это платная мощная среда
разработки, которая работает только на Windows. Она хорошо подходит для .NET платформы. У неё есть бесплатная
версия, которая называется Visual Studio Community .
Многие IDE платные, но у них есть пробный период. Их цена обычно незначительна по сравнению с зарплатой
квалифицированного разработчика, так что пробуйте и выбирайте ту, что вам подходит лучше других.
«Лёгкие» редакторы
«Лёгкие» редакторы менее мощные, чем IDE, но они отличаются скоростью, удобным интерфейсом и простотой.
В основном их используют для того, чтобы быстро открыть и отредактировать нужный файл.
Главное отличие между «лёгким» редактором и IDE состоит в том, что IDE работает на уровне целого проекта,
поэтому она загружает больше данных при запуске, анализирует структуру проекта, если это необходимо, и так
далее. Если вы работаете только с одним файлом, то гораздо быстрее открыть его в «лёгком» редакторе.
На практике «лёгкие» редакторы могут иметь множество плагинов, включая автодополнение и анализаторы
синтаксиса на уровне директории, поэтому границы между IDE и «лёгкими» редакторами размыты.
Не будем ссориться
Редакторы, перечисленные выше, известны автору давно и заслужили много хороших отзывов от коллег.
Конечно же, есть много других отличных редакторов. Выбирайте тот, который вам больше нравится.
Выбор редактора, как и любого другого инструмента, индивидуален и зависит от ваших проектов, привычек и личных
предпочтений.
Консоль разработчика
Код уязвим для ошибок. И вы, скорее всего, будете делать ошибки в коде… Впрочем, давайте будем откровенны: вы
точно будете совершать ошибки в коде. В конце концов, вы человек, а не робот .
Но по умолчанию в браузере ошибки не видны. То есть, если что-то пойдёт не так, мы не увидим, что именно
сломалось, и не сможем это починить.
Для решения задач такого рода в браузер встроены так называемые «Инструменты разработки» (Developer tools или
сокращённо — devtools).
Chrome и Firefox снискали любовь подавляющего большинства программистов во многом благодаря своим отменным
инструментам разработчика. Остальные браузеры, хотя и оснащены подобными инструментами, но всё же зачастую
находятся в роли догоняющих и по качеству, и по количеству свойств и особенностей. В общем, почти у всех
программистов есть свой «любимый» браузер. Другие используются только для отлова и исправления специфичных
«браузерозависимых» ошибок.
Для начала знакомства с этими мощными инструментами давайте выясним, как их открывать, смотреть ошибки и
запускать команды JavaScript.
Google Chrome
В её JavaScript-коде закралась ошибка. Она не видна обычному посетителю, поэтому давайте найдём её при помощи
инструментов разработки.
8/597
Точный внешний вид инструментов разработки зависит от используемой версии Chrome. Время от времени
некоторые детали изменяются, но в целом внешний вид остаётся примерно похожим на предыдущие версии.
●
В консоли мы можем увидеть сообщение об ошибке, отрисованное красным цветом. В нашем случае скрипт
содержит неизвестную команду «lalala».
●
Справа присутствует ссылка на исходный код [Link] с номером строки кода, в которой эта ошибка и
произошла.
Под сообщением об ошибке находится синий символ > . Он обозначает командную строку, в ней мы можем
редактировать и запускать JavaScript-команды. Для их запуска нажмите Enter .
Многострочный ввод
Обычно при нажатии Enter введённая строка кода сразу выполняется.
Чтобы перенести строку, нажмите Shift+Enter . Так можно вводить более длинный JS-код.
Теперь мы явно видим ошибки, для начала этого вполне достаточно. Мы ещё вернёмся к инструментам разработчика
позже и более подробно рассмотрим отладку кода в главе Отладка в браузере.
Их внешний вид и принципы работы мало чем отличаются. Разобравшись с инструментами в одном браузере, вы без
труда сможете работать с ними и в другом.
Safari
Safari (браузер для Mac, не поддерживается в системах Windows/Linux) всё же имеет небольшое отличие. Для начала
работы нам нужно включить «Меню разработки» («Developer menu»).
Откройте Настройки (Preferences) и перейдите к панели «Продвинутые» (Advanced). В самом низу вы найдёте
чекбокс:
9/597
Теперь консоль можно активировать нажатием клавиш Cmd+Opt+C . Также обратите внимание на новый элемент
меню «Разработка» («Develop»). В нем содержится большое количество команд и настроек.
Итого
●
Инструменты разработчика позволяют нам смотреть ошибки, выполнять команды, проверять значение переменных
и ещё много всего полезного.
●
В большинстве браузеров, работающих под Windows, инструменты разработчика можно открыть, нажав F12 . В
Chrome для Mac используйте комбинацию Cmd+Opt+J , Safari: Cmd+Opt+C (необходимо предварительное
включение «Меню разработчика»).
Теперь наше окружение полностью настроено. В следующем разделе мы перейдём непосредственно к JavaScript.
Основы JavaScript
Давайте изучим основы создания скриптов.
Привет, мир!
Но нам нужна рабочая среда для запуска наших скриптов, и, поскольку это онлайн-книга, то браузер будет хорошим
выбором. В этой главе мы сократим количество специфичных для браузера команд (например, alert ) до минимума,
чтобы вы не тратили на них время, если планируете сосредоточиться на другой среде (например, [Link]). А на
использовании JavaScript в браузере мы сосредоточимся в следующей части учебника.
Итак, сначала давайте посмотрим, как выполнить скрипт на странице. Для серверных сред (например, [Link]), вы
можете выполнить скрипт с помощью команды типа "node [Link]" . Для браузера всё немного иначе.
Тег «script»
Программы на JavaScript могут быть вставлены в любое место HTML-документа с помощью тега <script> .
Для примера:
<!DOCTYPE HTML>
<html>
<body>
<p>Перед скриптом...</p>
<script>
alert( 'Привет, мир!' );
</script>
<p>...После скрипта.</p>
</body>
</html>
Тег <script> содержит JavaScript-код, который автоматически выполнится, когда браузер его обработает.
Современная разметка
Тег <script> имеет несколько атрибутов, которые редко используются, но всё ещё могут встретиться в старом коде:
10/597
Этот атрибут должен был задавать язык, на котором написан скрипт. Но так как JavaScript является языком по
умолчанию, в этом атрибуте уже нет необходимости.
<script type="text/javascript"><!--
...
//--></script>
Этот комментарий скрывал код JavaScript в старых браузерах, которые не знали, как обрабатывать тег <script> .
Поскольку все браузеры, выпущенные за последние 15 лет, не содержат данной проблемы, такие комментарии уже
не нужны. Если они есть, то это признак, что перед нами очень древний код.
Внешние скрипты
<script src="/path/to/[Link]"></script>
Здесь /path/to/[Link] – это абсолютный путь от корневой папки до необходимого файла. Корневой папкой
может быть корень диска или корень сайта, в зависимости от условий работы сайта. Также можно указать
относительный путь от текущей страницы. Например, src="[Link]" или src="./[Link]" будет означать,
что файл "[Link]" находится в текущей папке.
<script src="[Link]
<script src="/js/[Link]"></script>
<script src="/js/[Link]"></script>
…
На заметку:
Как правило, только простейшие скрипты помещаются в HTML. Более сложные выделяются в отдельные файлы.
Польза отдельных файлов в том, что браузер загрузит скрипт отдельно и сможет хранить его в кеше .
Другие страницы, которые подключают тот же скрипт, смогут брать его из кеша вместо повторной загрузки из сети.
И таким образом файл будет загружаться с сервера только один раз.
11/597
Если атрибут src установлен, содержимое тега script будет игнорироваться.
В одном теге <script> нельзя использовать одновременно атрибут src и код внутри.
<script src="[Link]">
alert(1); // содержимое игнорируется, так как есть атрибут src
</script>
Нужно выбрать: либо внешний скрипт <script src="…"> , либо обычный код внутри тега <script> .
Вышеприведённый пример можно разделить на два скрипта:
<script src="[Link]"></script>
<script>
alert(1);
</script>
Итого
●
Для добавления кода JavaScript на страницу используется тег <script>
●
Атрибуты type и language необязательны.
●
Скрипт во внешнем файле можно вставить с помощью <script src="path/to/[Link]"></script> .
Нам ещё многое предстоит изучить про браузерные скрипты и их взаимодействие со страницей. Но, как уже было
сказано, эта часть учебника посвящена именно языку JavaScript, поэтому здесь мы постараемся не отвлекаться на
детали реализации в браузере. Мы воспользуемся браузером для запуска JavaScript, это удобно для онлайн-
демонстраций, но это только одна из платформ, на которых работает этот язык.
Задачи
Вызвать alert
важность: 5
Выполните это задание в песочнице, либо на вашем жёстком диске, где – неважно, главное – проверьте, что она
работает.
К решению
Возьмите решение предыдущей задачи Вызвать alert, и измените его. Извлеките содержимое скрипта во внешний
файл [Link] , лежащий в той же папке.
К решению
Структура кода
12/597
Инструкции
Мы уже видели инструкцию alert('Привет, мир!') , которая отображает сообщение «Привет, мир!».
В нашем коде может быть столько инструкций, сколько мы захотим. Инструкции могут отделяться точкой с запятой.
alert('Привет'); alert('Мир');
Обычно каждую инструкцию пишут на новой строке, чтобы код было легче читать:
alert('Привет');
alert('Мир');
Точка с запятой
В большинстве случаев точку с запятой можно не ставить, если есть переход на новую строку.
alert('Привет')
alert('Мир')
В этом случае JavaScript интерпретирует перенос строки как «неявную» точку с запятой. Это называется
автоматическая вставка точки с запятой .
В большинстве случаев новая строка подразумевает точку с запятой. Но «в большинстве случаев» не значит
«всегда»!
alert(3 +
1
+ 2);
Код выведет 6 , потому что JavaScript не вставляет здесь точку с запятой. Интуитивно очевидно, что, если строка
заканчивается знаком "+" , значит, это «незавершённое выражение», поэтому точка с запятой не требуется. И в этом
случае всё работает, как задумано.
Но есть ситуации, где JavaScript «забывает» вставить точку с запятой там, где она нужна.
13/597
Пример ошибки
Если вы хотите увидеть конкретный пример такой ошибки, обратите внимание на этот код:
alert('Hello');
[1, 2].forEach(alert);
Пока нет необходимости знать значение скобок [] и forEach . Мы изучим их позже. Пока что просто запомните
результат выполнения этого кода: выводится Hello , затем 1 , затем 2 .
А теперь давайте уберем точку с запятой после alert :
alert('Hello')
[1, 2].forEach(alert);
Этот код отличается от кода, приведенного выше, только в одном: пропала точка с запятой в конце первой строки.
Если мы запустим этот код, выведется только первый alert , а затем мы получим ошибку (вам может
потребоваться открыть консоль, чтобы увидеть её)!
Это потому что JavaScript не вставляет точку с запятой перед квадратными скобками [...] . И поэтому код в
последнем примере выполняется, как одна инструкция.
Вот как движок видит его:
alert('Hello')[1, 2].forEach(alert);
Выглядит странно, правда? Такое слияние в данном случае неправильное. Мы должны поставить точку с запятой
после alert , чтобы код работал правильно.
Мы рекомендуем ставить точку с запятой между инструкциями, даже если они отделены переносами строк. Это
правило широко используется в сообществе разработчиков. Стоит отметить ещё раз – в большинстве случаев можно
не ставить точку с запятой. Но безопаснее, особенно для новичка, ставить её.
Комментарии
Со временем программы становятся всё сложнее и сложнее. Возникает необходимость добавлять комментарии,
которые бы описывали, что делает код и почему.
Комментарии могут находиться в любом месте скрипта. Они не влияют на его выполнение, поскольку движок просто
игнорирует их.
Однострочные комментарии начинаются с двойной косой черты // .
Часть строки после // считается комментарием. Такой комментарий может как занимать строку целиком, так и
находиться после инструкции.
Как здесь:
14/597
*/
alert('Привет');
alert('Мир');
Содержимое комментария игнорируется, поэтому, если мы поместим код внутри /* … */ , он не будет исполняться.
/* Закомментировали код
alert('Привет');
*/
alert('Мир');
/*
/* вложенный комментарий ?!? */
*/
alert( 'Мир' );
Комментарии увеличивают размер кода, но это не проблема. Есть множество инструментов, которые минифицируют
код перед публикацией на рабочий сервер. Они убирают комментарии, так что они не содержатся в рабочих скриптах.
Таким образом, комментарии никоим образом не вредят рабочему коду.
Позже в учебнике будет глава Качество кода, которая объяснит, как лучше писать комментарии.
На протяжении долгого времени JavaScript развивался без проблем с обратной совместимостью. Новые функции
добавлялись в язык, в то время как старая функциональность не менялась.
Преимуществом данного подхода было то, что существующий код продолжал работать. А недостатком – что любая
ошибка или несовершенное решение, принятое создателями JavaScript, застревали в языке навсегда.
Так было до 2009 года, когда появился ECMAScript 5 (ES5). Он добавил новые возможности в язык и изменил
некоторые из существующих. Чтобы устаревший код работал, как и раньше, по умолчанию подобные изменения не
применяются. Поэтому нам нужно явно их активировать с помощью специальной директивы: "use strict" .
«use strict»
Директива выглядит как строка: "use strict" или 'use strict' . Когда она находится в начале скрипта, весь
сценарий работает в «современном» режиме.
Например:
"use strict";
Совсем скоро мы начнём изучать функции (способ группировки команд), поэтому заранее отметим, что в начале
большинства видов функций можно поставить "use strict" . Это позволяет включить строгий режим только в
15/597
конкретной функции. Но обычно люди используют его для всего файла.
alert("some code");
// "use strict" ниже игнорируется - он должен быть в первой строке
"use strict";
Консоль браузера
В дальнейшем, когда вы будете использовать консоль браузера для тестирования функций, обратите внимание, что
use strict по умолчанию в ней выключен.
Иногда, когда use strict имеет значение, вы можете получить неправильные результаты.
Итак, как можно включить use strict в консоли?
Можно использовать Shift+Enter для ввода нескольких строк и написать в верхней строке use strict :
Если этого не происходит, например, в старом браузере, есть некрасивый, но надежный способ обеспечить use
strict . Поместите его в следующую обёртку:
(function() {
'use strict';
// ...ваш код...
})()
Современный JavaScript поддерживает «классы» и «модули» — продвинутые структуры языка (и мы, конечно, до них
доберёмся), которые автоматически включают строгий режим. Поэтому в них нет нужды добавлять директиву "use
strict" .
Подытожим: пока очень желательно добавлять "use strict"; в начале ваших скриптов. Позже, когда весь
ваш код будет состоять из классов и модулей, директиву можно будет опускать.
16/597
Все примеры в этом учебнике подразумевают исполнение в строгом режиме, за исключением случаев (очень редких),
когда оговорено иное.
Переменные
Переменная
Переменная – это «именованное хранилище» для данных. Мы можем использовать переменные для хранения
товаров, посетителей и других данных.
let message;
Теперь можно поместить в неё данные (другими словами, определить переменную), используя оператор
присваивания = :
let message;
Строка сохраняется в области памяти, связанной с переменной. Мы можем получить к ней доступ, используя имя
переменной:
let message;
message = 'Hello!';
Для краткости можно совместить объявление переменной и запись данных в одну строку:
alert(message); // Hello!
Такой способ может показаться короче, но мы не рекомендуем его. Для лучшей читаемости объявляйте каждую
переменную на новой строке.
Некоторые люди также определяют несколько переменных в таком вот многострочном стиле:
17/597
let user = 'John',
age = 25,
message = 'Hello';
В принципе, все эти варианты работают одинаково. Так что это вопрос личного вкуса и эстетики.
Ключевое слово var – почти то же самое, что и let . Оно объявляет переменную, но немного по-другому,
«устаревшим» способом.
Есть тонкие различия между let и var , но они пока не имеют для нас значения. Мы подробно рассмотрим их в
главе Устаревшее ключевое слово "var".
Аналогия из жизни
Мы легко поймём концепцию «переменной», если представим её в виде «коробки» для данных с уникальным
названием на ней.
Например, переменную message можно представить как коробку с названием "message" и значением "Hello!"
внутри:
"H
ell
o!
"
message
let message;
message = 'Hello!';
alert(message);
llo!"
"He
"W
or
ld
!"
message
18/597
let hello = 'Hello world!';
let message;
Поэтому следует объявлять переменную только один раз и затем использовать её уже без let .
Имена переменных
let userName;
let test123;
Если имя содержит несколько слов, обычно используется верблюжья нотация , то есть, слова следуют одно за
другим, где каждое следующее слово начинается с заглавной буквы: myVeryLongName .
Самое интересное – знак доллара '$' и подчёркивание '_' также можно использовать в названиях. Это обычные
символы, как и буквы, без какого-либо особого значения.
Эти имена являются допустимыми:
alert($ + _); // 3
19/597
let 1a; // не может начинаться с цифры
Технически здесь нет ошибки, такие имена разрешены, но есть международная традиция использовать
английский язык в именах переменных. Даже если мы пишем небольшой скрипт, у него может быть долгая жизнь
впереди. Людям из других стран, возможно, придётся прочесть его не один раз.
Зарезервированные имена
Существует список зарезервированных слов , которые нельзя использовать в качестве имён переменных,
потому что они используются самим языком.
Например: let , class , return и function зарезервированы.
alert(num); // 5
"use strict";
Константы
Чтобы объявить константную, то есть, неизменяемую переменную, используйте const вместо let :
Переменные, объявленные с помощью const , называются «константами». Их нельзя изменить. Попытка сделать
это приведёт к ошибке:
20/597
const myBirthday = '18.04.1982';
Если программист уверен, что переменная никогда не будет меняться, он может гарантировать это и наглядно
донести до каждого, объявив её через const .
Преимущества:
●
COLOR_ORANGE гораздо легче запомнить, чем "#FF7F00" .
●
Гораздо легче допустить ошибку при вводе "#FF7F00" , чем при вводе COLOR_ORANGE .
●
При чтении кода COLOR_ORANGE намного понятнее, чем #FF7F00 .
Когда мы должны использовать для констант заглавные буквы, а когда называть их нормально? Давайте разберёмся
и с этим.
Название «константа» просто означает, что значение переменной никогда не меняется. Но есть константы, которые
известны до выполнения (например, шестнадцатеричное значение для красного цвета), а есть константы, которые
вычисляются во время выполнения сценария, но не изменяются после их первоначального назначения.
Например:
Значение pageLoadTime неизвестно до загрузки страницы, поэтому её имя записано обычными, а не прописными
буквами. Но это всё ещё константа, потому что она не изменяется после назначения.
Другими словами, константы с именами, записанными заглавными буквами, используются только как псевдонимы для
«жёстко закодированных» значений.
В разговоре о переменных необходимо упомянуть, что есть ещё одна чрезвычайно важная вещь.
Название переменной должно иметь ясный и понятный смысл, говорить о том, какие данные в ней хранятся.
Именование переменных – это один из самых важных и сложных навыков в программировании. Быстрый взгляд на
имена переменных может показать, какой код был написан новичком, а какой – опытным разработчиком.
В реальном проекте большая часть времени тратится на изменение и расширение существующей кодовой базы, а не
на написание чего-то совершенно нового с нуля. Когда мы возвращаемся к коду после какого-то промежутка времени,
гораздо легче найти информацию, которая хорошо размечена. Или, другими словами, когда переменные имеют
хорошие имена.
Пожалуйста, потратьте время на обдумывание правильного имени переменной перед её объявлением. Делайте так, и
будете вознаграждены.
Несколько хороших правил:
●
Используйте легко читаемые имена, такие как userName или shoppingCart .
21/597
●
Избегайте использования аббревиатур или коротких имён, таких как a , b , c , за исключением тех случаев, когда
вы точно знаете, что так нужно.
●
Делайте имена максимально описательными и лаконичными. Примеры плохих имён: data и value . Такие имена
ничего не говорят. Их можно использовать только в том случае, если из контекста кода очевидно, какие данные
хранит переменная.
●
Договоритесь с вашей командой об используемых терминах. Если посетитель сайта называется «user», тогда мы
должны называть связанные с ним переменные currentUser или newUser , а не, к примеру, currentVisitor
или newManInTown .
Звучит просто? Действительно, это так, но на практике для создания описательных и кратких имён переменных
зачастую требуется подумать. Действуйте.
В результате их переменные похожи на коробки, в которые люди бросают разные предметы, не меняя на них
этикетки. Что сейчас находится внутри коробки? Кто знает? Нам необходимо подойти поближе и проверить.
Такие программисты немного экономят на объявлении переменных, но теряют в десять раз больше при отладке.
Итого
Мы можем объявить переменные для хранения данных с помощью ключевых слов var , let или const .
●
let – это современный способ объявления.
●
var – это устаревший способ объявления. Обычно мы вообще не используем его, но мы рассмотрим тонкие
отличия от let в главе Устаревшее ключевое слово "var" на случай, если это всё-таки вам понадобится.
●
const – похоже на let , но значение переменной не может изменяться.
Переменные должны быть названы таким образом, чтобы мы могли легко понять, что у них внутри.
Задачи
Работа с переменными
важность: 2
К решению
К решению
22/597
Рассмотрим следующий код:
У нас есть константа birthday , а также age , которая вычисляется при помощи некоторого кода, используя
значение из birthday (в данном случае детали не имеют значения, поэтому код не рассматривается).
Можно ли использовать заглавные буквы для имени birthday ? А для age ? Или одновременно для обеих
переменных?
К решению
Типы данных
Значение в JavaScript всегда относится к данным определённого типа. Например, это может быть строка или число.
Есть восемь основных типов данных в JavaScript. В этой главе мы рассмотрим их в общем, а в следующих главах
поговорим подробнее о каждом.
Переменная в JavaScript может содержать любые данные. В один момент там может быть строка, а в другой – число:
// Не будет ошибкой
let message = "hello";
message = 123456;
Языки программирования, в которых такое возможно, называются «динамически типизированными». Это значит, что
типы данных есть, но переменные не привязаны ни к одному из них.
Число
let n = 123;
n = 12.345;
Числовой тип данных ( number ) представляет как целочисленные значения, так и числа с плавающей точкой.
Существует множество операций для чисел, например, умножение * , деление / , сложение + , вычитание - и так
далее.
Кроме обычных чисел, существуют так называемые «специальные числовые значения», которые относятся к этому
типу данных: Infinity , -Infinity и NaN .
●
Infinity представляет собой математическую бесконечность ∞. Это особое значение, которое больше
любого числа.
alert( 1 / 0 ); // Infinity
●
NaN означает вычислительную ошибку. Это результат неправильной или неопределённой математической
операции, например:
23/597
alert( "не число" / 2 ); // NaN, такое деление является ошибкой
Если где-то в математическом выражении есть NaN , то оно распространяется на весь результат (есть только одно
исключение: NaN ** 0 равно 1 ).
Специальные числовые значения относятся к типу «число». Конечно, это не числа в привычном значении этого слова.
BigInt
В JavaScript тип number не может безопасно работать с числами, большими, чем (253-1) (т. е.
9007199254740991 ) или меньшими, чем -(253-1) для отрицательных чисел.
Если говорить совсем точно, то, технически, тип number может хранить большие целые числа (до
1.7976931348623157 * 10308 ), но за пределами безопасного диапазона целых чисел ±(253-1) будет ошибка
точности, так как не все цифры помещаются в фиксированную 64-битную память. Поэтому можно хранить
«приблизительное» значение.
То есть все нечетные целые числа, большие чем (253-1) , вообще не могут храниться в типе number .
В большинстве случаев безопасного диапазона чисел от -(253-1) до (253-1) вполне достаточно, но иногда нам
требуется весь диапазон действительно гигантских целых чисел без каких-либо ограничений или пропущенных
значений внутри него. Например, в криптографии или при использовании метки времени («timestamp») с
микросекундами.
Тип BigInt был добавлен в JavaScript, чтобы дать возможность работать с целыми числами произвольной длины.
Чтобы создать значение типа BigInt , необходимо добавить n в конец числового литерала:
Так как необходимость в использовании BigInt –чисел появляется достаточно редко, мы рассмотрим их в отдельной
главе BigInt. Ознакомьтесь с ней, когда вам понадобятся настолько большие числа.
Поддержка
В данный момент BigInt поддерживается только в браузерах Firefox, Chrome, Edge и Safari, но не
поддерживается в IE.
24/597
Строка
Двойные или одинарные кавычки являются «простыми», между ними нет разницы в JavaScript.
Обратные же кавычки имеют расширенную функциональность. Они позволяют нам встраивать выражения в строку,
заключая их в ${…} . Например:
// Вставим переменную
alert( `Привет, ${ name}!` ); // Привет, Иван!
// Вставим выражение
alert( `результат: ${ 1 + 2}` ); // результат: 3
Выражение внутри ${…} вычисляется, и его результат становится частью строки. Мы можем положить туда всё, что
угодно: переменную name , или выражение 1 + 2 , или что-то более сложное.
Обратите внимание, что это можно делать только в обратных кавычках. Другие кавычки не имеют такой
функциональности встраивания!
alert( "результат: ${1 + 2}" ); // результат: ${1 + 2} (двойные кавычки ничего не делают)
В JavaScript подобного типа нет, есть только тип string . Строка может содержать ноль символов (быть пустой),
один символ или множество.
Булевый тип ( boolean ) может принимать только два значения: true (истина) и false (ложь).
Такой тип, как правило, используется для хранения значений да/нет: true значит «да, правильно», а false значит
«нет, не правильно».
Например:
25/597
Мы рассмотрим булевые значения более подробно в главе Логические операторы.
Значение «null»
В JavaScript null не является «ссылкой на несуществующий объект» или «нулевым указателем», как в некоторых
других языках.
Это просто специальное значение, которое представляет собой «ничего», «пусто» или «значение неизвестно».
Значение «undefined»
Специальное значение undefined также стоит особняком. Оно формирует тип из самого себя так же, как и null .
let age;
alert(age); // "undefined"
…Но так делать не рекомендуется. Обычно null используется для присвоения переменной «пустого» или
«неизвестного» значения, а undefined – для проверок, была ли переменная назначена.
Объекты и символы
Объекты занимают важное место в языке и требуют особого внимания. Мы разберёмся с ними в главе Объекты после
того, как узнаем больше о примитивах.
Тип symbol (символ) используется для создания уникальных идентификаторов в объектах. Мы упоминаем здесь о
нём для полноты картины, изучим этот тип после объектов.
Оператор typeof
Оператор typeof возвращает тип аргумента. Это полезно, когда мы хотим обрабатывать значения различных типов
по-разному или просто хотим сделать проверку.
// Обычный синтаксис
typeof 5 // Выведет "number"
// Синтаксис, напоминающий вызов функции (встречается реже)
typeof(5) // Также выведет "number"
26/597
Если передается выражение, то нужно заключать его в скобки, т.к. typeof имеет более высокий приоритет, чем
бинарные операторы:
Другими словами, скобки необходимы для определения типа значения, которое получилось в результате выполнения
выражения в них.
typeof 0 // "number"
1. Math — это встроенный объект, который предоставляет математические операции и константы. Мы рассмотрим
его подробнее в главе Числа. Здесь он служит лишь примером объекта.
2. Результатом вызова typeof null является "object" . Это официально признанная ошибка в typeof , ведущая
начало с времён создания JavaScript и сохранённая для совместимости. Конечно, null не является объектом.
Это специальное значение с отдельным типом.
3. Вызов typeof alert возвращает "function" , потому что alert является функцией. Мы изучим функции в
следующих главах, где заодно увидим, что в JavaScript нет специального типа «функция». Функции относятся к
объектному типу. Но typeof обрабатывает их особым образом, возвращая "function" . Так тоже повелось от
создания JavaScript. Формально это неверно, но может быть удобным на практике.
Итого
Оператор typeof позволяет нам увидеть, какой тип данных сохранён в переменной.
●
Имеет две формы: typeof x или typeof(x) .
●
Возвращает строку с именем типа. Например, "string" .
●
Для null возвращается "object" – это ошибка в языке, на самом деле это не объект.
27/597
В следующих главах мы сконцентрируемся на примитивных значениях, а когда познакомимся с ними, перейдём к
объектам.
Задачи
Шаблонные строки
важность: 5
К решению
Так как мы будем использовать браузер как демо-среду, нам нужно познакомиться с несколькими функциями его
интерфейса, а именно: alert , prompt и confirm .
alert
С этой функцией мы уже знакомы. Она показывает сообщение и ждёт, пока пользователь нажмёт кнопку «ОК».
Например:
alert("Hello");
Это небольшое окно с сообщением называется модальным окном. Понятие модальное означает, что пользователь не
может взаимодействовать с интерфейсом остальной части страницы, нажимать на другие кнопки и т.д. до тех пор,
пока взаимодействует с окном. В данном случае – пока не будет нажата кнопка «OK».
prompt
Этот код отобразит модальное окно с текстом, полем для ввода текста и кнопками OK/Отмена.
title
Текст для отображения в окне.
default
Необязательный второй параметр, который устанавливает начальное значение в поле для текста в окне.
Пользователь может напечатать что-либо в поле ввода и нажать OK. Введённый текст будет присвоен переменной
result . Пользователь также может отменить ввод нажатием на кнопку «Отмена» или нажав на клавишу Esc . В
этом случае значением result станет null .
28/597
Вызов prompt возвращает текст, указанный в поле для ввода, или null , если ввод отменён пользователем.
Например:
Чтобы prompt хорошо выглядел в IE, рекомендуется всегда указывать второй параметр:
confirm
Синтаксис:
result = confirm(question);
Функция confirm отображает модальное окно с текстом вопроса question и двумя кнопками: OK и Отмена.
Например:
Итого
alert
показывает сообщение.
prompt
показывает сообщение и запрашивает ввод текста от пользователя. Возвращает напечатанный в поле ввода текст
или null , если была нажата кнопка «Отмена» или Esc с клавиатуры.
confirm
показывает сообщение и ждёт, пока пользователь нажмёт OK или Отмена. Возвращает true , если нажата OK, и
false , если нажата кнопка «Отмена» или Esc с клавиатуры.
Все эти методы являются модальными: останавливают выполнение скриптов и не позволяют пользователю
взаимодействовать с остальной частью страницы до тех пор, пока окно не будет закрыто.
29/597
Такова цена простоты. Есть другие способы показать более приятные глазу окна с богатой функциональностью для
взаимодействия с пользователем, но если «навороты» не имеют значения, то данные методы работают отлично.
Задачи
Простая страница
важность: 4
Запустить демо
К решению
Преобразование типов
Чаще всего операторы и функции автоматически приводят переданные им значения к нужному типу.
Например, alert автоматически преобразует любое значение к строке. Математические операторы преобразуют
значения к числам.
Есть также случаи, когда нам нужно явно преобразовать значение в ожидаемый тип.
Строковое преобразование
Преобразование происходит очевидным образом. false становится "false" , null становится "null" и т.п.
Численное преобразование
30/597
Явное преобразование часто применяется, когда мы ожидаем получить число из строкового контекста, например из
текстовых полей форм.
Если строка не может быть явно приведена к числу, то результатом преобразования будет NaN . Например:
Значение Преобразуется в…
undefined NaN
null 0
true / false 1 / 0
Пробельные символы (пробелы, знаки табуляции \t , знаки новой строки \n и т. п.) по краям обрезаются. Далее, если остаётся пустая
string
строка, то получаем 0 , иначе из непустой строки «считывается» число. При ошибке результат NaN .
Примеры:
Учтите, что null и undefined ведут себя по-разному. Так, null становится нулём, тогда как undefined
приводится к NaN .
Большинство математических операторов также производит данное преобразование, как мы увидим в следующей
главе.
Логическое преобразование
Например:
31/597
Итого
Строковое – Происходит, когда нам нужно что-то вывести. Может быть вызвано с помощью String(value) . Для
примитивных значений работает очевидным образом.
Значение Становится…
undefined NaN
null 0
true / false 1 / 0
Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то получаем 0 , иначе из непустой строки «считывается»
string
число. При ошибке результат NaN .
Подчиняется правилам:
Значение Становится…
Большую часть из этих правил легко понять и запомнить. Особые случаи, в которых часто допускаются ошибки:
●
undefined при численном преобразовании становится NaN , не 0 .
●
"0" и строки из одних пробелов типа " " при логическом преобразовании всегда true .
В этой главе мы не говорили об объектах. Мы вернёмся к ним позже, в главе Преобразование объектов в примитивы,
посвящённой только объектам, сразу после того, как узнаем больше про основы JavaScript.
Многие операторы знакомы нам ещё со школы: сложение + , умножение * , вычитание - и так далее.
В этой главе мы начнём с простых операторов, а потом сконцентрируемся на специфических для JavaScript аспектах,
которые не проходят в школьном курсе арифметики.
let x = 1;
x = -x;
alert( x ); // -1, применили унарный минус
●
Бинарным называется оператор, который применяется к двум операндам. Тот же минус существует и в бинарной
форме:
let x = 1, y = 3;
alert( y - x ); // 2, бинарный минус вычитает значения
32/597
Формально, в последних примерах мы говорим о двух разных операторах, использующих один символ: оператор
отрицания (унарный оператор, который обращает знак) и оператор вычитания (бинарный оператор, который
вычитает одно число из другого).
Математика
Взятие остатка %
Оператор взятия остатка % , несмотря на обозначение, никакого отношения к процентам не имеет.
Например:
Возведение в степень **
Оператор возведения в степень a ** b возводит a в степень b .
alert( 2 ** 2 ); // 2² = 4
alert( 2 ** 3 ); // 2³ = 8
alert( 2 ** 4 ); // 2⁴ = 16
Математически, оператор работает и для нецелых чисел. Например, квадратный корень является возведением в
степень ½:
Давайте рассмотрим специальные возможности операторов JavaScript, которые выходят за рамки школьной
арифметики.
Обычно при помощи плюса '+' складывают числа.
Обратите внимание, если хотя бы один операнд является строкой, то второй будет также преобразован в строку.
Например:
33/597
Как видите, не важно, первый или второй операнд является строкой.
Здесь операторы работают один за другим. Первый + складывает два числа и возвращает 4 , затем следующий +
объединяет результат со строкой, производя действие 4 + '1' = '41' .
Сложение и преобразование строк — это особенность бинарного плюса + . Другие арифметические операторы
работают только с числами и всегда преобразуют операнды в числа.
Унарный, то есть применённый к одному значению, плюс + ничего не делает с числами. Но если операнд не число,
унарный плюс преобразует его в число.
Например:
// Не влияет на числа
let x = 1;
alert( +x ); // 1
let y = -2;
alert( +y ); // -2
alert( apples + oranges ); // "23", так как бинарный плюс объединяет строки
С точки зрения математики, такое изобилие плюсов выглядит странным. Но с точки зрения программиста тут нет
ничего особенного: сначала выполнятся унарные плюсы, которые приведут строки к числам, а затем бинарный '+'
их сложит.
Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете.
34/597
Приоритет операторов
В том случае, если в выражении есть несколько операторов – порядок их выполнения определяется приоритетом,
или, другими словами, существует определённый порядок выполнения операторов.
Из школы мы знаем, что умножение в выражении 1 + 2 * 2 выполнится раньше сложения. Это как раз и есть
«приоритет». Говорят, что умножение имеет более высокий приоритет, чем сложение.
Скобки важнее, чем приоритет, так что, если мы не удовлетворены порядком по умолчанию, мы можем использовать
их, чтобы изменить приоритет. Например, написать (1 + 2) * 2 .
В JavaScript много операторов. Каждый оператор имеет соответствующий номер приоритета. Тот, у кого это число
больше, – выполнится раньше. Если приоритет одинаковый, то порядок выполнения – слева направо.
Отрывок из таблицы приоритетов (нет необходимости всё запоминать, обратите внимание, что приоритет унарных
операторов выше, чем соответствующих бинарных):
… … …
15 унарный плюс +
15 унарный минус -
14 возведение в степень **
13 умножение *
13 деление /
12 сложение +
12 вычитание -
… … …
2 присваивание =
… … …
Так как «унарный плюс» имеет приоритет 15 , который выше, чем 12 у «сложения» (бинарный плюс), то в
выражении "+apples + +oranges" сначала выполнятся унарные плюсы, а затем сложение.
Присваивание
Давайте отметим, что в таблице приоритетов также есть оператор присваивания = . У него один из самых низких
приоритетов: 2 .
let x = 2 * 2 + 1;
alert( x ); // 5
Благодаря этому присваивание можно использовать как часть более сложного выражения:
let a = 1;
let b = 2;
let c = 3 - (a = b + 1);
alert( a ); // 3
alert( c ); // 0
35/597
В примере выше результатом (a = b + 1) будет значение, которое присваивается переменной a (то есть 3 ).
Потом оно используется для дальнейших вычислений.
Забавное применение присваивания, не так ли? Нам нужно понимать, как это работает, потому что иногда это можно
увидеть в JavaScript-библиотеках.
Однако писать самим в таком стиле не рекомендуется. Такие трюки не сделают ваш код более понятным или
читабельным.
Присваивание по цепочке
Рассмотрим ещё одну интересную возможность: цепочку присваиваний.
let a, b, c;
a = b = c = 2 + 2;
alert( a ); // 4
alert( b ); // 4
alert( c ); // 4
Такое присваивание работает справа налево. Сначала вычисляется самое правое выражение 2 + 2 , и затем
результат присваивается переменным слева: c , b и a . В конце у всех переменных будет одно значение.
Опять-таки, чтобы код читался легче, лучше разделять подобные конструкции на несколько строчек:
c = 2 + 2;
b = c;
a = c;
Например:
let n = 2;
n = n + 5;
n = n * 2;
let n = 2;
n += 5; // теперь n = 7 (работает как n = n + 5)
n *= 2; // теперь n = 14 (работает как n = n * 2)
alert( n ); // 14
Подобные краткие формы записи существуют для всех арифметических и побитовых операторов: /= , -= , **= и
так далее.
Вызов с присваиванием имеет в точности такой же приоритет, как обычное присваивание, то есть выполнится после
большинства других операций:
let n = 2;
n *= 3 + 5;
Инкремент/декремент
Одной из наиболее частых числовых операций является увеличение или уменьшение на единицу.
36/597
Для этого существуют даже специальные операторы:
●
Инкремент ++ увеличивает переменную на 1:
let counter = 2;
counter++; // работает как counter = counter + 1, просто запись короче
alert( counter ); // 3
●
Декремент -- уменьшает переменную на 1:
let counter = 2;
counter--; // работает как counter = counter - 1, просто запись короче
alert( counter ); // 1
Важно:
Инкремент/декремент можно применить только к переменной. Попытка использовать его на значении, типа 5++,
приведёт к ошибке.
Есть ли разница между ними? Да, но увидеть её мы сможем, только если будем использовать значение, которое
возвращают ++/-- .
Давайте проясним этот момент. Как мы знаем, все операторы возвращают значение. Операторы инкремента/
декремента не исключение. Префиксная форма возвращает новое значение, в то время как постфиксная форма
возвращает старое (до увеличения/уменьшения числа).
let counter = 1;
let a = ++counter; // (*)
alert(a); // 2
В строке (*) префиксная форма ++counter увеличивает counter и возвращает новое значение 2 . Так что
alert покажет 2 .
let counter = 1;
let a = counter++; // (*) меняем ++counter на counter++
alert(a); // 1
В строке (*) постфиксная форма counter++ также увеличивает counter , но возвращает старое значение
(которое было до увеличения). Так что alert покажет 1 .
Подведём итоги:
●
Если результат оператора не используется, а нужно только увеличить/уменьшить переменную, тогда без разницы,
какую форму использовать:
let counter = 0;
counter++;
++counter;
alert( counter ); // 2, обе строки сделали одно и то же
37/597
let counter = 0;
alert( ++counter ); // 1
●
Если нужно увеличить и при этом получить значение переменной до увеличения – нужна постфиксная форма:
let counter = 0;
alert( counter++ ); // 0
let counter = 1;
alert( 2 * ++counter ); // 4
Сравните с:
let counter = 1;
alert( 2 * counter++ ); // 2, потому что counter++ возвращает "старое" значение
Хотя технически здесь всё в порядке, такая запись обычно делает код менее читабельным. Одна строка
выполняет множество действий – нехорошо.
При беглом чтении кода можно с лёгкостью пропустить такой counter++ , и будет неочевидно, что переменная
увеличивается.
Лучше использовать стиль «одна строка – одно действие»:
let counter = 1;
alert( 2 * counter );
counter++;
Побитовые операторы
Побитовые операторы работают с 32-разрядными целыми числами (при необходимости приводят к ним), на уровне их
внутреннего двоичного представления.
Эти операторы не являются чем-то специфичным для JavaScript, они поддерживаются в большинстве языков
программирования.
Они используются редко, когда возникает необходимость оперировать с числами на очень низком (побитовом)
уровне. В ближайшем времени они нам не понадобятся, так как веб-разработчики редко к ним прибегают, хотя в
некоторых сферах (например, в криптографии) они полезны.
Вы можете прочитать о них в главе Побитовые операторы, когда возникнет реальная необходимость.
38/597
Оператор «запятая»
Оператор «запятая» ( , ) редко применяется и является одним из самых необычных. Иногда он используется для
написания более короткого кода, поэтому нам нужно знать его, чтобы понимать, что при этом происходит.
Оператор «запятая» предоставляет нам возможность вычислять несколько выражений, разделяя их запятой , .
Каждое выражение выполняется, но возвращается результат только последнего.
Например:
let a = (1 + 2, 3 + 4);
Попробуйте запустить следующий код (строгий режим "use strict" в примере ниже не используется,
иначе мы бы получили ошибку):
a = 1 + 2, 3 + 4;
alert(a); // 3
Необычный результат, правда? Особенно учитывая то, что оператор , должен «выполнять каждое выражение, но
возвращать результат только последнего».
Без скобок в a = 1 + 2, 3 + 4 сначала выполнится + , суммируя числа в a = 3, 7 , затем оператор
присваивания = присвоит a = 3 , а то, что идёт дальше, будет проигнорировано. Всё так же, как в (a = 1 +
2), 3 + 4 .
Иногда его используют в составе более сложных конструкций, чтобы сделать несколько действий в одной строке.
Например:
Такие трюки используются во многих JavaScript-фреймворках. Вот почему мы упоминаем их. Но обычно они не
улучшают читабельность кода, поэтому стоит хорошо подумать, прежде чем их использовать.
Задачи
let a = 1, b = 1;
let c = ++a; // ?
let d = b++; // ?
К решению
39/597
Результат присваивания
важность: 3
let a = 2;
let x = 1 + (a *= 2);
К решению
Преобразование типов
важность: 5
"" + 1 + 0
"" - 1 + 0
true + false
6 / "3"
"2" * "3"
4 + 5 + "px"
"$" + 4 + 5
"4" - 2
"4px" - 2
" -9 " + 5
" -9 " - 5
null + 1
undefined + 1
" \t \n" - 2
К решению
Исправьте сложение
важность: 5
Ниже приведён код, который запрашивает у пользователя два числа и показывает их сумму.
alert(a + b); // 12
К решению
Операторы сравнения
40/597
В этом разделе мы больше узнаем про то, какие бывают сравнения, как язык с ними работает и к каким
неожиданностям мы должны быть готовы.
В конце вы найдёте хороший рецепт того, как избегать «причуд» сравнения в JavaScript.
Например:
Сравнение строк
Чтобы определить, что одна строка больше другой, JavaScript использует «алфавитный» или «лексикографический»
порядок.
Другими словами, строки сравниваются посимвольно.
Например:
В примерах выше сравнение 'Я' > 'А' завершится на первом шаге, тогда как строки 'Коты' и 'Кода' будут
сравниваться посимвольно:
1. К равна К .
2. о равна о .
3. т больше, чем д . На этом сравнение заканчивается. Первая строка больше.
Например, в JavaScript имеет значение регистр символов. Заглавная буква "A" не равна строчной "a" . Какая
же из них больше? Строчная "a" . Почему? Потому что строчные буквы имеют больший код во внутренней
таблице кодирования, которую использует JavaScript (Unicode). Мы ещё поговорим о внутреннем представлении
строк и его влиянии в главе Строки.
41/597
Сравнение разных типов
При сравнении значений разных типов JavaScript приводит каждое из них к числу.
Например:
Например:
Забавное следствие
Возможна следующая ситуация:
●
Два значения равны.
●
Одно из них true как логическое значение, другое – false .
Например:
let a = 0;
alert( Boolean(a) ); // false
let b = "0";
alert( Boolean(b) ); // true
С точки зрения JavaScript, результат ожидаем. Равенство преобразует значения, используя числовое
преобразование, поэтому "0" становится 0 . В то время как явное преобразование с помощью Boolean
использует другой набор правил.
Строгое сравнение
Использование обычного сравнения == может вызывать проблемы. Например, оно не отличает 0 от false :
Это происходит из-за того, что операнды разных типов преобразуются оператором == к числу. В итоге, и пустая
строка, и false становятся нулём.
Давайте проверим:
42/597
Оператор строгого равенства дольше писать, но он делает код более очевидным и оставляет меньше места для
ошибок.
При использовании математических операторов и других операторов сравнения < > <= >=
Значения null/undefined преобразуются к числам: null становится 0 , а undefined – NaN .
Посмотрим, какие забавные вещи случаются, когда мы применяем эти правила. И, что более важно, как избежать
ошибок при их использовании.
С точки зрения математики это странно. Результат последнего сравнения говорит о том, что « null больше или
равно нулю», тогда результат одного из сравнений выше должен быть true , но они оба ложны.
Причина в том, что нестрогое равенство и сравнения > < >= <= работают по-разному. Сравнения преобразуют
null в число, рассматривая его как 0 . Поэтому выражение (3) null >= 0 истинно, а null > 0 ложно.
С другой стороны, для нестрогого равенства == значений undefined и null действует особое правило: эти
значения ни к чему не приводятся, они равны друг другу и не равны ничему другому. Поэтому (2) null == 0 ложно.
43/597
●
Не используйте сравнения >= > < <= с переменными, которые могут принимать значения null/undefined ,
разве что вы полностью уверены в том, что делаете. Если переменная может принимать эти значения, то добавьте
для них отдельные проверки.
Итого
●
Операторы сравнения возвращают значения логического типа.
●
Строки сравниваются посимвольно в лексикографическом порядке.
●
Значения разных типов при сравнении приводятся к числу. Исключением является сравнение с помощью
операторов строгого равенства/неравенства.
●
Значения null и undefined равны == друг другу и не равны любому другому значению.
●
Будьте осторожны при использовании операторов сравнений вроде > и < с переменными, которые могут
принимать значения null/undefined . Хорошей идеей будет сделать отдельную проверку на null/undefined .
Задачи
Операторы сравнения
важность: 5
5 > 4
"ананас" > "яблоко"
"2" > "12"
undefined == null
undefined === null
null == "\n0\n"
null === +"\n0\n"
К решению
Для этого мы можем использовать инструкцию if и условный оператор ? , который также называют оператором
«вопросительный знак».
Инструкция «if»
Инструкция if(...) вычисляет условие в скобках и, если результат true , то выполняет блок кода.
Например:
let year = prompt('В каком году была опубликована спецификация ECMAScript-2015?', '');
В примере выше, условие – это простая проверка на равенство ( year == 2015 ), но оно может быть и гораздо более
сложным.
Если мы хотим выполнить более одной инструкции, то нужно заключить блок кода в фигурные скобки:
if (year == 2015) {
alert( "Правильно!" );
alert( "Вы такой умный!" );
}
Мы рекомендуем использовать фигурные скобки {} всегда, когда вы используете инструкцию if , даже если
выполняется только одна команда. Это улучшает читаемость кода.
44/597
Преобразование к логическому типу
if (0) { // 0 is falsy
...
}
if (1) { // 1 is truthy
...
}
Мы также можем передать заранее вычисленное в переменной логическое значение в if , например так:
if (condition) {
...
}
Блок «else»
Инструкция if может содержать необязательный блок «else» («иначе»). Он выполняется, когда условие ложно.
Например:
let year = prompt('В каком году была опубликована спецификация ECMAScript-2015?', '');
if (year == 2015) {
alert( 'Да вы знаток!' );
} else {
alert( 'А вот и неправильно!' ); // любое значение, кроме 2015
}
Иногда нужно проверить несколько вариантов условия. Для этого используется блок else if .
Например:
let year = prompt('В каком году была опубликована спецификация ECMAScript-2015?', '');
В приведённом выше коде JavaScript сначала проверит year < 2015 . Если это неверно, он переходит к
следующему условию year > 2015 . Если оно тоже ложно, тогда сработает последний alert .
Блоков else if может быть и больше. Присутствие блока else не является обязательным.
45/597
Условный оператор „?“
Например:
let accessAllowed;
let age = prompt('Сколько вам лет?', '');
alert(accessAllowed);
Так называемый «условный» оператор «вопросительный знак» позволяет нам сделать это более коротким и простым
способом.
Оператор представлен знаком вопроса ? . Его также называют «тернарный», так как этот оператор, единственный в
своём роде, имеет три аргумента.
Синтаксис:
Сначала вычисляется условие : если оно истинно, тогда возвращается значение1 , в противном случае –
значение2 .
Например:
Технически, мы можем опустить круглые скобки вокруг age > 18 . Оператор вопросительного знака имеет низкий
приоритет, поэтому он выполняется после сравнения > .
Но скобки делают код более простым для восприятия, поэтому мы рекомендуем их использовать.
На заметку:
В примере выше вы можете избежать использования оператора вопросительного знака ? , т.к. сравнение само по
себе уже возвращает true/false :
// то же самое
let accessAllowed = age > 18;
Последовательность операторов вопросительного знака ? позволяет вернуть значение, которое зависит от более
чем одного условия.
Например:
46/597
(age < 18) ? 'Привет!' :
(age < 100) ? 'Здравствуйте!' :
'Какой необычный возраст!';
alert( message );
Поначалу может быть сложно понять, что происходит. Но при ближайшем рассмотрении мы видим, что это обычная
последовательная проверка:
if (age < 3) {
message = 'Здравствуй, малыш!';
} else if (age < 18) {
message = 'Привет!';
} else if (age < 100) {
message = 'Здравствуйте!';
} else {
message = 'Какой необычный возраст!';
}
(company == 'Netscape') ?
alert('Верно!') : alert('Неправильно.');
В зависимости от условия company == 'Netscape' , будет выполнена либо первая, либо вторая часть после ? .
Здесь мы не присваиваем результат переменной. Вместо этого мы выполняем различный код в зависимости от
условия.
Несмотря на то, что такая запись короче, чем эквивалентная инструкция if , она хуже читается.
if (company == 'Netscape') {
alert('Верно!');
} else {
alert('Неправильно.');
}
При чтении глаза сканируют код по вертикали. Блоки кода, занимающие несколько строк, воспринимаются гораздо
легче, чем длинный горизонтальный набор инструкций.
Смысл оператора «вопросительный знак» ? – вернуть то или иное значение, в зависимости от условия. Пожалуйста,
используйте его именно для этого. Когда вам нужно выполнить разные ветви кода – используйте if .
47/597
Задачи
if (строка с нулём)
важность: 5
Выведется ли alert ?
if ("0") {
alert( 'Привет' );
}
К решению
Название JavaScript
важность: 2
Используя конструкцию if..else , напишите код, который будет спрашивать: „Какое «официальное» название
JavaScript?“
Если пользователь вводит «ECMAScript», то показать: «Верно!», в противном случае – отобразить: «Не знаете?
ECMAScript!»
Начало
Какое
"официальное" название
JavaScript?
Другое ECMAScript
Не знаете?
Верно!
“ECMAScript”!
К решению
Используя конструкцию if..else , напишите код, который получает число через prompt , а затем выводит в
alert :
●
1 , если значение больше нуля,
●
-1 , если значение меньше нуля,
●
0 , если значение равно нулю.
К решению
let result;
48/597
if (a + b < 4) {
result = 'Мало';
} else {
result = 'Много';
}
К решению
let message;
if (login == 'Сотрудник') {
message = 'Привет';
} else if (login == 'Директор') {
message = 'Здравствуйте';
} else if (login == '') {
message = 'Нет логина';
} else {
message = '';
}
К решению
Логические операторы
Несмотря на своё название, данные операторы могут применяться к значениям любых типов. Полученные результаты
также могут иметь различный тип.
|| (ИЛИ)
result = a || b;
Традиционно в программировании ИЛИ предназначено только для манипулирования булевыми значениями: в случае,
если какой-либо из аргументов true , он вернёт true , в противоположной ситуации возвращается false .
В JavaScript, как мы увидим далее, этот оператор работает несколько иным образом. Но давайте сперва посмотрим,
что происходит с булевыми значениями.
Существует всего четыре возможные логические комбинации:
49/597
alert( true || true ); // true
alert( false || true ); // true
alert( true || false ); // true
alert( false || false ); // false
Как мы можем наблюдать, результат операций всегда равен true , за исключением случая, когда оба аргумента
false .
let hour = 9;
Другими словами, цепочка ИЛИ || возвращает первое истинное значение или последнее, если такое значение не
найдено.
Например:
50/597
alert( null || 0 || 1 ); // 1 (первое истинное значение)
alert( undefined || null || 0 ); // 0 (поскольку все ложно, возвращается последнее значение)
Это делает возможным более интересное применение оператора по сравнению с «чистым, традиционным, только
булевым ИЛИ».
Например, у нас есть переменные firstName , lastName и nickName , все они необязательные (т.е. они могут
быть неопределенными или иметь ложные значения).
Давайте воспользуемся оператором ИЛИ || , чтобы выбрать ту переменную, в которой есть данные, и показать её
(или «Аноним», если ни в одной переменной данных нет):
2. Сокращённое вычисление.
Ещё одной отличительной особенностью оператора ИЛИ || является так называемое «сокращённое
вычисление».
Это означает, что ИЛИ || обрабатывает свои операнды до тех пор, пока не будет достигнуто первое истинностное
значение, и затем это значение сразу же возвращается, даже не затрагивая другие операнды.
Важность этой особенности становится очевидной, если операнд – это не просто значение, а выражение с
сопутствующим эффектом, как, например, присваивание переменной или вызов функции.
В первой строке оператор ИЛИ || останавливает выполнение сразу после того, как сталкивается с истинным
значением ( true ), поэтому сообщение не показывается.
Иногда люди используют эту возможность для выполнения инструкций только в том случае, если условие в левой
части является ложным.
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Оператор логического присваивания ИЛИ ||= записывается как обычный ИЛИ || с добавлением символа
присваивания = . Такая запись не случайна, так как результат выполнения данного оператора напрямую зависит от
действий уже известного нам || .
Вот его синтаксис:
a ||= b;
Концепция оператора ||= заключается в «сокращённом вычислении», принцип работы которого мы разобрали
ранее.
51/597
Теперь давайте перепишем a ||= b под вид «сокращённого вычисления»:
a || (a = b);
Мы уже знаем, что ИЛИ || возвращает первое истинное значение, поэтому, если a является таковым, вычисление
до правой части выражения не дойдёт.
johnHasCar ||= "У Джона нет машины!"; // то же самое, что false || (johnHasCar = "...")
let manufacturer = ""; // оператор ||= преобразует пустую строку "" к логическому значению false
Оператор логического присваивания ИЛИ ||= – это «синтаксический сахар », добавленный в язык в качестве
более короткого варианта записи if -выражений с присваиванием.
if (johnHasCar == false) {
johnHasCar = "У Джона нет машины!";
}
if (manufacturer == false) {
manufacturer = "Неизвестный производитель";
}
&& (И)
result = a && b;
В традиционном программировании И возвращает true , если оба аргумента истинны, а иначе – false :
Пример с if :
52/597
if (hour == 12 && minute == 30) {
alert( 'Время 12:30' );
}
Другими словами, И возвращает первое ложное значение. Или последнее, если ничего не найдено.
Вышеуказанные правила схожи с поведением ИЛИ. Разница в том, что И возвращает первое ложное значение, а
ИЛИ – первое истинное.
Примеры:
Можно передать несколько значений подряд. В таком случае возвратится первое «ложное» значение, на котором
остановились вычисления.
Таким образом, код a && b || c && d по существу такой же, как если бы выражения && были в круглых
скобках: (a && b) || (c && d) .
53/597
Не заменяйте if на || или &&
Иногда люди используют оператор И && как «более короткий способ записи if -выражения».
Например:
let x = 1;
Инструкция в правой части && будет выполнена только в том случае, если вычисление дойдет до нее. То есть,
только если (x > 0) истинно.
Таким образом, мы имеем аналог для следующего кода:
let x = 1;
Несмотря на то, что вариант с && кажется более коротким, if более нагляден и, как правило, более читабелен.
Поэтому мы рекомендуем использовать каждую конструкцию по назначению: использовать if , если нам нужно
if , и использовать && , если нам нужно И.
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Оператор логического присваивания И &&= записывается как два амперсанда && и символ присваивания = .
a &&= b;
Принцип действия &&= практически такой же, как и у оператора логического присваивания ИЛИ ||= . Единственное
отличие заключается в том, что &&= присвоит a значение b только в том случае, если a истинно.
a && (a = b);
Пример использования:
let greeting = "Привет"; // строка непустая, поэтому будет преобразована к логическому значению true оператором &&=
greeting &&= greeting + ", пользователь!"; // то же самое, что true && (greeting = greeting + "...")
Так как оператор логического присваивания И &&= также как и ||= является «синтаксическим сахаром», мы можем
без проблем переписать пример выше с использованием привычного для нас if :
if (greeting) {
greeting = greeting + ", пользователь!"
}
54/597
На практике, в отличие от ||= , оператор &&= используется достаточно редко – обычно, в комбинации с более
сложными языковыми конструкциями, о которых мы будем говорить позже. Подобрать контекст для применения
данного оператора – довольно непростая задача.
! (НЕ)
result = !value;
Например:
То есть первое НЕ преобразует значение в логическое значение и возвращает обратное, а второе НЕ снова
инвертирует его. В конце мы имеем простое преобразование значения в логическое.
Есть немного более подробный способ сделать то же самое – встроенная функция Boolean :
Приоритет НЕ ! является наивысшим из всех логических операторов, поэтому он всегда выполняется первым, перед
&& или || .
Задачи
К решению
К решению
55/597
Что выведет alert (И)?
важность: 5
К решению
К решению
К решению
alert(value);
К решению
Напишите условие if для проверки, что переменная age находится в диапазоне между 14 и 90 включительно.
«Включительно» означает, что значение переменной age может быть равно 14 или 90 .
К решению
Напишите условие if для проверки, что значение переменной age НЕ находится в диапазоне 14 и 90
включительно.
Напишите два варианта: первый с использованием оператора НЕ ! , второй – без этого оператора.
56/597
К решению
Вопрос об "if"
важность: 5
К решению
Проверка логина
важность: 3
Если посетитель вводит «Админ», то prompt запрашивает пароль, если ничего не введено или нажата клавиша Esc
– показать «Отменено», в противном случае отобразить «Я вас не знаю».
Блок-схема:
Начало
Кто там?
Пароль?
Для решения используйте вложенные блоки if . Обращайте внимание на стиль и читаемость кода.
Подсказка: передача пустого ввода в приглашение prompt возвращает пустую строку '' . Нажатие клавиши Esc во
время запроса возвращает null .
Запустить демо
К решению
57/597
Операторы нулевого слияния и присваивания: '??', '??='
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Иначе говоря, оператор ?? возвращает первый аргумент, если он не null/undefined , иначе второй.
Оператор нулевого слияния не является чем-то принципиально новым. Это всего лишь удобный синтаксис, как из
двух значений получить одно, которое «определено».
Вот как можно переписать выражение result = a ?? b , используя уже знакомые нам операторы:
Теперь должно быть абсолютно ясно, что делает ?? . Давайте посмотрим, где это может быть полезно.
Как правило, оператор ?? нужен для того, чтобы задать значение по умолчанию для потенциально неопределённой
переменной.
Например, здесь мы отобразим user , если её значение не null/undefined , в противном случае Аноним :
let user;
Кроме этого, можно записать последовательность из операторов ?? , чтобы получить первое значение из списка,
которое не является null/undefined .
Допустим, у нас есть данные пользователя в переменных firstName , lastName или nickName . Все они могут не
существовать, если пользователь решил не вводить соответствующие значение.
Мы хотели бы отобразить имя пользователя, используя одну из этих переменных, или показать «Аноним», если все
они null/undefined .
Сравнение с ||
Оператор ИЛИ || можно использовать для того же, что и ?? , как это было показано в предыдущей главе.
Например, если в приведённом выше коде заменить ?? на || , то будет тот же самый результат:
58/597
let firstName = null;
let lastName = null;
let nickName = "Суперкодер";
Исторически сложилось так, что оператор ИЛИ || появился первым. Он существует с самого начала в JavaScript,
поэтому разработчики долгое время использовали его для таких целей.
С другой стороны, сравнительно недавно в язык был добавлен оператор нулевого слияния ?? – как раз потому, что
многие были недовольны оператором || .
Проще говоря, оператор || не различает false , 0 , пустую строку "" и null/undefined . Для него они все
одинаковы, т.е. являются ложными значениями. Если первым аргументом для оператора || будет любое из
перечисленных значений, то в качестве результата мы получим второй аргумент.
Однако на практике часто требуется использовать значение по умолчанию только тогда, когда переменная является
null/undefined . Ведь именно тогда значение действительно неизвестно/не определено.
let height = 0;
●
height || 100 проверяет height на ложное значение, оно равно 0 , да, ложное.
●
поэтому результатом || является второй аргумент, т.е. 100 .
●
height ?? 100 проверяет, что переменная height содержит null/undefined , а поскольку это не так,
●
то результатом является сама переменная height , т.е. 0 .
На практике нулевая высота часто является вполне нормальным значением, которое не следует заменять значением
по умолчанию. Таким образом, ?? здесь как раз работает так, как нужно.
Приоритет
Приоритет оператора ?? такой же, как и у || . Они оба равны 3 в таблице на MDN .
Это означает, что, как и || , оператор нулевого слияния ?? вычисляется до = и ? , но после большинства других
операций, таких как + , * .
alert(area); // 5000
Иначе, если опустить скобки, оператор * выполнится первым, так как у него приоритет выше, чем у ?? , и это
приведёт к неправильным результатам.
// без скобок
let area = height ?? 100 * width ?? 50;
59/597
Использование ?? вместе с && или ||
По соображениям безопасности JavaScript запрещает использование оператора ?? вместе с && и || , если
приоритет явно не указан при помощи круглых скобок.
Это, безусловно, спорное ограничение было добавлено в спецификацию языка с целью избежать программные
ошибки, когда люди начнут переходить с || на ?? .
alert(x); // 2
Предположим, нам необходимо проверить, равна ли переменная null или undefined , и если это так — присвоить
этой переменной какое-либо другое значение.
Выглядит громоздко, правда? Существует оператор, более подходящий для подобных задач. Вот его синтаксис:
x ??= y
Оператор ??= присвоит x значение y только в том случае, если x не определено ( null / undefined ).
Теперь попробуем переписать уже знакомый нам фрагмент кода используя новый оператор:
alert(userAge) // 18
Обратите внимание: если бы userAge не был равен null / undefined , то выражение справа от ??= никогда бы
не выполнилось:
alert(userAge) // по-прежнему 18
Итого
●
Оператор нулевого слияния ?? — это быстрый способ выбрать первое «определённое» значение из списка.
60/597
// будет height=100, если переменная height равна null или undefined
height = height ?? 100;
●
Оператор ?? имеет очень низкий приоритет, лишь немного выше, чем у ? и = , поэтому при использовании его в
выражении, скорее всего, потребуются скобки.
●
Запрещено использовать вместе с || или && без явно указанного приоритета, то есть без скобок.
●
Для присвоения переменной значения в зависимости от того, «определена» она или нет, используется оператор
нулевого присваивания ??= .
Задачи
К решению
alert(city);
К решению
К решению
При написании скриптов зачастую встаёт задача сделать однотипное действие много раз.
61/597
Например, вывести товары из списка один за другим. Или просто перебрать все числа от 1 до 10 и для каждого
выполнить одинаковый код.
Для многократного повторения одного участка кода предусмотрены циклы.
Если вы пришли к этой статье в поисках других типов циклов, вот указатели:
●
См. for…in для перебора свойств объекта.
●
См. for…of и Перебираемые объекты для перебора массивов и перебираемых объектов.
Цикл «while»
while (condition) {
// код
// также называемый "телом цикла"
}
let i = 0;
while (i < 3) { // выводит 0, затем 1, затем 2
alert( i );
i++;
}
Одно выполнение тела цикла по-научному называется итерация. Цикл в примере выше совершает три итерации.
Если бы строка i++ отсутствовала в примере выше, то цикл бы повторялся (в теории) вечно. На практике, конечно,
браузер не позволит такому случиться, он предоставит пользователю возможность остановить «подвисший» скрипт, а
JavaScript на стороне сервера придётся «убить» процесс.
Любое выражение или переменная может быть условием цикла, а не только сравнение: условие while вычисляется
и преобразуется в логическое значение.
let i = 3;
while (i) { // когда i будет равно 0, условие станет ложным, и цикл остановится
alert( i );
i--;
}
let i = 3;
while (i) alert(i--);
Цикл «do…while»
Проверку условия можно разместить под телом цикла, используя специальный синтаксис do..while :
62/597
do {
// тело цикла
} while (condition);
Цикл сначала выполнит тело, а затем проверит условие condition , и пока его значение равно true , он будет
выполняться снова и снова.
Например:
let i = 0;
do {
alert( i );
i++;
} while (i < 3);
Такая форма синтаксиса оправдана, если вы хотите, чтобы тело цикла выполнилось хотя бы один раз, даже если
условие окажется ложным. На практике чаще используется форма с предусловием: while(…) {…} .
Цикл «for»
Выглядит он так:
Давайте разберёмся, что означает каждая часть, на примере. Цикл ниже выполняет alert(i) для i от 0 до (но не
включая) 3 :
часть
шаг i++ Выполняется после тела цикла на каждой итерации перед проверкой условия.
Выполнить начало
→ (Если условие == true → Выполнить тело, Выполнить шаг)
→ (Если условие == true → Выполнить тело, Выполнить шаг)
→ (Если условие == true → Выполнить тело, Выполнить шаг)
→ ...
То есть, начало выполняется один раз, а затем каждая итерация заключается в проверке условия , после которой
выполняется тело и шаг .
Если тема циклов для вас нова, может быть полезным вернуться к примеру выше и воспроизвести его работу на
листе бумаги, шаг за шагом.
63/597
// for (let i = 0; i < 3; i++) alert(i)
// Выполнить начало
let i = 0;
// Если условие == true → Выполнить тело, Выполнить шаг
if (i < 3) { alert(i); i++ }
// Если условие == true → Выполнить тело, Выполнить шаг
if (i < 3) { alert(i); i++ }
// Если условие == true → Выполнить тело, Выполнить шаг
if (i < 3) { alert(i); i++ }
// ...конец, потому что теперь i == 3
let i = 0;
Для примера, мы можем пропустить начало если нам ничего не нужно делать перед стартом цикла.
Вот так:
let i = 0;
for (;;) {
// будет выполняться вечно
}
При этом сами точки с запятой ; обязательно должны присутствовать, иначе будет ошибка синтаксиса.
64/597
Прерывание цикла: «break»
Например, следующий код подсчитывает сумму вводимых чисел до тех пор, пока посетитель их вводит, а затем –
выдаёт:
let sum = 0;
while (true) {
sum += value;
}
alert( 'Сумма: ' + sum );
Директива break в строке (*) полностью прекращает выполнение цикла и передаёт управление на строку за его
телом, то есть на alert .
Вообще, сочетание «бесконечный цикл + break » – отличная штука для тех ситуаций, когда условие, по которому
нужно прерваться, находится не в начале или конце цикла, а посередине или даже в нескольких местах его тела.
Директива continue – «облегчённая версия» break . При её выполнении цикл не прерывается, а переходит к
следующей итерации (если условие все ещё равно true ).
Её используют, если понятно, что на текущем повторе цикла делать больше нечего.
Например, цикл ниже использует continue , чтобы выводить только нечётные значения:
alert(i); // 1, затем 3, 5, 7, 9
}
Для чётных значений i , директива continue прекращает выполнение тела цикла и передаёт управление на
следующую итерацию for (со следующим числом). Таким образом alert вызывается только для нечётных
значений.
if (i % 2) {
alert( i );
}
С технической точки зрения он полностью идентичен. Действительно, вместо continue можно просто завернуть
действия в блок if .
Однако мы получили дополнительный уровень вложенности фигурных скобок. Если код внутри if более
длинный, то это ухудшает читаемость, в отличие от варианта с continue .
65/597
Нельзя использовать break/continue справа от оператора „?“
Обратите внимание, что эти синтаксические конструкции не являются выражениями и не могут быть
использованы с тернарным оператором ? . В частности, использование таких директив, как break/continue ,
вызовет ошибку.
if (i > 5) {
alert(i);
} else {
continue;
}
Например, в коде ниже мы проходимся циклами по i и j , запрашивая с помощью prompt координаты (i, j) с
(0,0) до (2,2) :
alert('Готово!');
Обычный break после input лишь прервёт внутренний цикл, но этого недостаточно. Достичь желаемого
поведения можно с помощью меток.
Метка имеет вид идентификатора с двоеточием перед циклом:
Вызов break <labelName> в цикле ниже ищет ближайший внешний цикл с такой меткой и переходит в его конец.
66/597
}
alert('Готово!');
В примере выше это означает, что вызовом break outer будет разорван внешний цикл до метки с именем outer .
outer:
for (let i = 0; i < 3; i++) { ... }
Директива continue также может быть использована с меткой. В этом случае управление перейдёт на следующую
итерацию цикла с меткой.
Директива break должна находиться внутри блока кода. Технически, подойдет любой маркированный блок кода,
например:
label: {
// ...
break label; // работает
// ...
}
…Хотя в 99.9% случаев break используется внутри циклов, как мы видели в примерах выше.
Итого
Чтобы организовать бесконечный цикл, используют конструкцию while (true) . При этом он, как и любой другой
цикл, может быть прерван директивой break .
Если на данной итерации цикла делать больше ничего не надо, но полностью прекращать цикл не следует –
используют директиву continue .
Обе этих директивы поддерживают метки, которые ставятся перед циклом. Метки – единственный способ для
break/continue выйти за пределы текущего цикла, повлиять на выполнение внешнего.
Заметим, что метки не позволяют прыгнуть в произвольное место кода, в JavaScript нет такой возможности.
Задачи
67/597
let i = 3;
while (i) {
alert( i-- );
}
К решению
Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом.
1.
let i = 0;
while (++i < 5) alert( i );
2.
let i = 0;
while (i++ < 5) alert( i );
К решению
Для каждого цикла запишите, какие значения он выведет. Потом сравните с ответом.
1.
Постфиксная форма:
2.
Префиксная форма:
К решению
Запустить демо
К решению
68/597
Замените for на while
важность: 5
Перепишите код, заменив цикл for на while , без изменения поведения цикла.
К решению
Напишите цикл, который предлагает prompt ввести число, большее 100 . Если посетитель ввёл другое число –
попросить ввести ещё раз, и так далее.
Цикл должен спрашивать число пока либо посетитель не введёт число, большее 100 , либо не нажмёт кнопку
Отмена (ESC).
Предполагается, что посетитель вводит только числа. Предусматривать обработку нечисловых строк в этой задаче
необязательно.
Запустить демо
К решению
Натуральное число, большее 1 , называется простым , если оно ни на что не делится, кроме себя и 1 .
Другими словами, n > 1 – простое, если при его делении на любое число кроме 1 и n есть остаток.
Например, 5 это простое число, оно не может быть разделено без остатка на 2 , 3 и 4 .
P.S. Код также должен легко модифицироваться для любых других интервалов.
К решению
Конструкция "switch"
Конструкция switch заменяет собой сразу несколько if .
Она представляет собой более наглядный способ сравнить выражение сразу с несколькими вариантами.
Синтаксис
Конструкция switch имеет один или более блок case и необязательный блок default .
switch(x) {
case 'value1': // if (x === 'value1')
...
[break]
69/597
...
[break]
default:
...
[break]
}
●
Переменная x проверяется на строгое равенство первому значению value1 , затем второму value2 и так
далее.
●
Если соответствие установлено – switch начинает выполняться от соответствующей директивы case и далее,
до ближайшего break (или до конца switch ).
●
Если ни один case не совпал – выполняется (если есть) вариант default .
Пример работы
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Маловато' );
break;
case 4:
alert( 'В точку!' );
break;
case 5:
alert( 'Перебор' );
break;
default:
alert( "Нет таких значений" );
}
Сначала 3 , затем – так как нет совпадения – 4 . Совпадение найдено, будет выполнен этот вариант, со строки
alert( 'В точку!' ) и далее, до ближайшего break , который прервёт выполнение.
Если break нет, то выполнение пойдёт ниже по следующим case , при этом остальные проверки
игнорируются.
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Маловато' );
case 4:
alert( 'В точку!' );
case 5:
alert( 'Перебор' );
default:
alert( "Нет таких значений" );
}
70/597
Любое выражение может быть аргументом для switch/case
И switch и case допускают любое выражение в качестве аргумента.
Например:
let a = "1";
let b = 0;
switch (+a) {
case b + 1:
alert("Выполнится, т.к. значением +a будет 1, что в точности равно b+1");
break;
default:
alert("Это не выполнится");
}
Группировка «case»
Для примера, выполним один и тот же код для case 3 и case 5 , сгруппировав их:
let a = 3;
switch (a) {
case 4:
alert('Правильно!');
break;
default:
alert('Результат выглядит странновато. Честно.');
}
Возможность группировать case – это побочный эффект того, как switch/case работает без break . Здесь
выполнение case 3 начинается со строки (*) и продолжается в case 5 , потому что отсутствует break .
Нужно отметить, что проверка на равенство всегда строгая. Значения должны быть одного типа, чтобы выполнялось
равенство.
case '2':
alert( 'Два' );
break;
case 3:
71/597
alert( 'Никогда не выполнится!' );
break;
default:
alert( 'Неизвестное значение' );
}
Задачи
switch (browser) {
case 'Edge':
alert( "You've got the Edge!" );
break;
case 'Chrome':
case 'Firefox':
case 'Safari':
case 'Opera':
alert( 'Okay we support these browsers too' );
break;
default:
alert( 'We hope that this page looks ok!' );
}
К решению
if (number === 0) {
alert('Вы ввели число 0');
}
if (number === 1) {
alert('Вы ввели число 1');
}
К решению
Функции
Например, необходимо красиво вывести сообщение при приветствии посетителя, при выходе посетителя с сайта, ещё
где-нибудь.
72/597
Чтобы не повторять один и тот же код во многих местах, придуманы функции. Функции являются основными
«строительными блоками» программы.
Объявление функции
function showMessage() {
alert( 'Всем привет!' );
}
Вначале идёт ключевое слово function , после него имя функции, затем список параметров в круглых скобках
через запятую (в вышеприведённом примере он пустой) и, наконец, код функции, также называемый «телом
функции», внутри фигурных скобок.
function имя(параметры) {
...тело...
}
Например:
function showMessage() {
alert( 'Всем привет!' );
}
showMessage();
showMessage();
Этот пример явно демонстрирует одно из главных предназначений функций: избавление от дублирования кода.
Если понадобится поменять сообщение или способ его вывода – достаточно изменить его в одном месте: в функции,
которая его выводит.
Локальные переменные
function showMessage() {
let message = "Привет, я JavaScript!"; // локальная переменная
alert( message );
}
alert( message ); // <-- будет ошибка, т.к. переменная видна только внутри функции
Внешние переменные
73/597
function showMessage() {
let message = 'Привет, ' + userName;
alert(message);
}
Например:
function showMessage() {
userName = "Петя"; // (1) изменяем значение внешней переменной
showMessage();
Внешняя переменная используется, только если внутри функции нет такой локальной.
Если одноимённая переменная объявляется внутри функции, тогда она перекрывает внешнюю. Например, в коде
ниже функция использует локальную переменную userName . Внешняя будет проигнорирована:
function showMessage() {
let userName = "Петя"; // объявляем локальную переменную
Глобальные переменные
Переменные, объявленные снаружи всех функций, такие как внешняя переменная userName в
вышеприведённом коде – называются глобальными.
Глобальные переменные видимы для любой функции (если только их не перекрывают одноимённые локальные
переменные).
Желательно сводить использование глобальных переменных к минимуму. В современном коде обычно мало или
совсем нет глобальных переменных. Хотя они иногда полезны для хранения важнейших «общепроектовых»
данных.
Параметры
74/597
showMessage('Аня', 'Привет!'); // Аня: Привет! (*)
showMessage('Аня', "Как дела?"); // Аня: Как дела? (**)
Когда функция вызывается в строках (*) и (**) , переданные значения копируются в локальные переменные
from и text . Затем они используются в теле функции.
Вот ещё один пример: у нас есть переменная from , и мы передаём её функции. Обратите внимание: функция
изменяет значение from , но это изменение не видно снаружи. Функция всегда получает только копию значения:
Другими словами:
●
Параметр – это переменная, указанная в круглых скобках в объявлении функции.
●
Аргумент – это значение, которое передаётся функции при её вызове.
Рассматривая приведённый выше пример, мы могли бы сказать: «функция showMessage объявляется с двумя
параметрами, затем вызывается с двумя аргументами: from и "Привет" ».
Значения по умолчанию
Если при вызове функции аргумент не был указан, то его значением становится undefined .
Например, вышеупомянутая функция showMessage(from, text) может быть вызвана с одним аргументом:
showMessage("Аня");
Это не приведёт к ошибке. Такой вызов выведет "*Аня*: undefined" . В вызове не указан параметр text ,
поэтому предполагается, что text === undefined .
Если мы хотим задать параметру text значение по умолчанию, мы должны указать его после = :
Теперь, если параметр text не указан, его значением будет "текст не добавлен"
В данном случае "текст не добавлен" это строка, но на её месте могло бы быть и более сложное выражение,
которое бы вычислялось и присваивалось при отсутствии параметра. Например:
75/597
Вычисление параметров по умолчанию
В JavaScript параметры по умолчанию вычисляются каждый раз, когда функция вызывается без соответствующего
аргумента.
В приведённом выше примере, функция anotherFunction() не будет вызвана вообще, если указан аргумент
text .
С другой стороны, функция будет независимо вызываться каждый раз, когда аргумент text отсутствует.
Во время выполнения функции мы можем проверить, передан ли параметр, сравнив его с undefined :
function showMessage(text) {
// ...
if (text === undefined) { // если параметр отсутствует
text = 'пустое сообщение';
}
alert(text);
}
showMessage(); // пустое сообщение
function showMessage(text) {
// если значение text ложно или равняется undefined, тогда присвоить text значение 'пусто'
text = text || 'пусто';
...
}
Современные движки JavaScript поддерживают оператор нулевого слияния ?? . Его использование будет лучшей
практикой, в случае, если большинство ложных значений, таких как 0 , следует расценивать как «нормальные».
function showCount(count) {
// если count равен undefined или null, показать "неизвестно"
alert(count ?? "неизвестно");
}
showCount(0); // 0
76/597
showCount(null); // неизвестно
showCount(); // неизвестно
Возврат значения
function sum(a, b) {
return a + b;
}
Директива return может находиться в любом месте тела функции. Как только выполнение доходит до этого места,
функция останавливается, и значение возвращается в вызвавший её код (присваивается переменной result выше).
function checkAge(age) {
if (age >= 18) {
return true;
} else {
return confirm('А родители разрешили?');
}
}
if ( checkAge(age) ) {
alert( 'Доступ получен' );
} else {
alert( 'Доступ закрыт' );
}
Возможно использовать return и без значения. Это приведёт к немедленному выходу из функции.
Например:
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
77/597
Результат функции с пустым return или без него – undefined
Если функция не возвращает значения, это всё равно, как если бы она возвращала undefined :
function doNothing() {
return;
}
return
(some + long + expression + or + whatever * f(a) + f(b))
Код не выполнится, потому что интерпретатор JavaScript подставит точку с запятой после return . Для него это
будет выглядеть так:
return;
(some + long + expression + or + whatever * f(a) + f(b))
Если мы хотим, чтобы возвращаемое выражение занимало несколько строк, нужно начать его на той же строке,
что и return . Или, хотя бы, поставить там открывающую скобку, вот так:
return (
some + long + expression
+ or +
whatever * f(a) + f(b)
)
Функция – это действие. Поэтому имя функции обычно является глаголом. Оно должно быть кратким, точным и
описывать действие функции, чтобы программист, который будет читать код, получил верное представление о том,
что делает функция.
Как правило, используются глагольные префиксы, обозначающие общий характер действия, после которых следует
уточнение. Обычно в командах разработчиков действуют соглашения, касающиеся значений этих префиксов.
78/597
Примеры таких имён:
Благодаря префиксам, при первом взгляде на имя функции становится понятным, что делает её код, и какое
значение она может возвращать.
Два независимых действия обычно подразумевают две функции, даже если предполагается, что они будут
вызываться вместе (в этом случае мы можем создать третью функцию, которая будет их вызывать).
В этих примерах использовались общепринятые смыслы префиксов. Конечно, вы в команде можете договориться
о других значениях, но обычно они мало отличаются от общепринятых. В любом случае вы и ваша команда
должны чётко понимать, что значит префикс, что функция с ним может делать, а чего не может.
Это исключения. В основном имена функций должны быть в меру краткими и описательными.
Функции == Комментарии
Функции должны быть короткими и делать только что-то одно. Если это что-то большое, имеет смысл разбить
функцию на несколько меньших. Иногда следовать этому правилу непросто, но это определённо хорошее правило.
Небольшие функции не только облегчают тестирование и отладку – само существование таких функций выполняет
роль хороших комментариев!
Например, сравним ниже две функции showPrimes(n) . Каждая из них выводит простое число до n .
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
alert( i ); // простое
}
}
79/597
function showPrimes(n) {
alert(i); // простое
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}
Второй вариант легче для понимания, не правда ли? Вместо куска кода мы видим название действия ( isPrime ).
Иногда разработчики называют такой код самодокументируемым.
Таким образом, допустимо создавать функции, даже если мы не планируем повторно использовать их. Такие функции
структурируют код и делают его более понятным.
Итого
●
Передаваемые значения копируются в параметры функции и становятся локальными переменными.
●
Функции имеют доступ к внешним переменным. Но это работает только изнутри наружу. Код вне функции не имеет
доступа к её локальным переменным.
●
Функция может возвращать значение. Если этого не происходит, тогда результат равен undefined .
Для того, чтобы сделать код более чистым и понятным, рекомендуется использовать локальные переменные и
параметры функций, не пользоваться внешними переменными.
Функция, которая получает параметры, работает с ними и затем возвращает результат, гораздо понятнее функции,
вызываемой без параметров, но изменяющей внешние переменные, что чревато побочными эффектами.
Именование функций:
●
Имя функции должно понятно и чётко отражать, что она делает. Увидев её вызов в коде, вы должны тут же
понимать, что она делает, и что возвращает.
●
Функция – это действие, поэтому её имя обычно является глаголом.
●
Есть много общепринятых префиксов, таких как: create… , show… , get… , check… и т.д. Пользуйтесь ими как
подсказками, поясняющими, что делает функция.
Функции являются основными строительными блоками скриптов. Мы рассмотрели лишь основы функций в JavaScript,
но уже сейчас можем создавать и использовать их. Это только начало пути. Мы будем неоднократно возвращаться к
функциям и изучать их всё более и более глубоко.
Задачи
Обязателен ли "else"?
важность: 4
В ином случае она запрашивает подтверждение через confirm и возвращает его результат:
function checkAge(age) {
if (age > 18) {
return true;
80/597
} else {
// ...
return confirm('Родители разрешили?');
}
}
function checkAge(age) {
if (age > 18) {
return true;
}
// ...
return confirm('Родители разрешили?');
}
К решению
function checkAge(age) {
if (age > 18) {
return true;
} else {
return confirm('Родители разрешили?');
}
}
1. Используя оператор ?
2. Используя оператор ||
К решению
Функция min(a, b)
важность: 1
Пример вызовов:
min(2, 5) == 2
min(3, -1) == -1
min(1, 1) == 1
К решению
Функция pow(x,n)
важность: 4
81/597
pow(3, 2) = 3 * 3 = 9
pow(3, 3) = 3 * 3 * 3 = 27
pow(1, 100) = 1 * 1 * ...* 1 = 1
Запустить демо
P.S. В этой задаче функция обязана поддерживать только натуральные значения n , т.е. целые от 1 и выше.
К решению
Function Expression
Функция в JavaScript – это не магическая языковая структура, а особого типа значение.
function sayHi() {
alert( "Привет" );
}
Существует ещё один синтаксис создания функций, который называется Function Expression (Функциональное
Выражение).
Данный синтаксис позволяет нам создавать новую функцию в середине любого выражения.
Здесь мы можем видеть переменную sayHi , получающую значение, новую функцию, созданную как function() {
alert("Привет"); } .
Поскольку создание функции происходит в контексте выражения присваивания (с правой стороны от = ), это Function
Expression.
Обратите внимание, что после ключевого слова function нет имени. Для Function Expression допускается его
отсутствие.
Здесь мы сразу присваиваем её переменной, так что смысл этих примеров кода один и тот же: «создать функцию и
поместить её в переменную sayHi ».
В более сложных ситуациях, с которыми мы столкнёмся позже, функция может быть создана и немедленно вызвана,
или запланирована для дальнейшего выполнения, нигде не сохраняясь, таким образом, оставаясь анонимной.
Давайте повторим: независимо от того, как создаётся функция – она является значением. В обоих приведённых выше
примерах функция хранится в переменной sayHi .
function sayHi() {
alert( "Привет" );
}
Обратите внимание, что последняя строка не вызывает функцию, потому что после sayHi нет круглых скобок.
Существуют языки программирования, в которых любое упоминание имени функции приводит к её выполнению, но
JavaScript к таким не относится.
82/597
В JavaScript функция – это значение, поэтому мы можем обращаться с ней как со значением. Приведённый выше код
показывает её строковое представление, которое является её исходным кодом.
Конечно, функция – это особое значение, в том смысле, что мы можем вызвать её как sayHi() .
Но всё же это значение. Поэтому мы можем работать с ней так же, как и с другими видами значений.
1. Объявление Function Declaration (1) создаёт функцию и помещает её в переменную с именем sayHi .
2. В строке (2) мы скопировали её значение в переменную func . Обратите внимание (ещё раз): нет круглых скобок
после sayHi . Если бы они были, то выражение func = sayHi() записало бы результат вызова sayHi() в
переменную func , а не саму функцию sayHi .
3. Теперь функция может вызываться как sayHi() , так и func() .
Мы также могли бы использовать Function Expression для объявления sayHi в первой строке:
function sayHi() {
// ...
}
Ответ прост: Function Expression создаётся здесь как function(...) {...} внутри выражения присваивания:
let sayHi = …; . Точку с запятой ; рекомендуется ставить в конце выражения, она не является частью
синтаксиса функции.
Точка с запятой нужна там для более простого присваивания, такого как let sayHi = 5; , а также для
присваивания функции.
Функции-«колбэки»
Давайте рассмотрим больше примеров передачи функции в виде значения и использования функциональных
выражений.
question
Текст вопроса
83/597
yes
Функция, которая будет вызываться, если ответ будет «Yes»
no
Функция, которая будет вызываться, если ответ будет «No»
Наша функция должна задать вопрос question и, в зависимости от того, как ответит пользователь, вызвать yes()
или no() :
function showOk() {
alert( "Вы согласны." );
}
function showCancel() {
alert( "Вы отменили выполнение." );
}
На практике подобные функции очень полезны. Основное отличие «реальной» функции ask от примера выше будет
в том, что она использует более сложные способы взаимодействия с пользователем, чем простой вызов confirm . В
браузерах такие функции обычно отображают красивые диалоговые окна. Но это уже другая история.
Аргументы showOk и showCancel функции ask называются функциями-колбэками или просто колбэками.
Ключевая идея в том, что мы передаём функцию и ожидаем, что она вызовется обратно (от англ. «call back» –
обратный вызов) когда-нибудь позже, если это будет необходимо. В нашем случае, showOk становится колбэком для
ответа «yes», а showCancel – для ответа «no».
ask(
"Вы согласны?",
function() { alert("Вы согласились."); },
function() { alert("Вы отменили выполнение."); }
);
Здесь функции объявляются прямо внутри вызова ask(...) . У них нет имён, поэтому они называются анонимными.
Такие функции недоступны снаружи ask (потому что они не присвоены переменным), но это как раз то, что нам
нужно.
Подобный код, появившийся в нашем скрипте выглядит очень естественно, в духе JavaScript.
84/597
// Function Declaration
function sum(a, b) {
return a + b;
}
●
Function Expression: функция, созданная внутри другого выражения или синтаксической конструкции. В данном
случае функция создаётся в правой части «выражения присваивания» = :
// Function Expression
let sum = function(a, b) {
return a + b;
};
Более тонкое отличие состоит в том, когда создаётся функция движком JavaScript.
Function Expression создаётся, когда выполнение доходит до него, и затем уже может использоваться.
После того, как поток выполнения достигнет правой части выражения присваивания let sum = function… – с
этого момента, функция считается созданной и может быть использована (присвоена переменной, вызвана и т.д. ).
Другими словами, когда движок JavaScript готовится выполнять скрипт или блок кода, прежде всего он ищет в нём
Function Declaration и создаёт все такие функции. Можно считать этот процесс «стадией инициализации».
И только после того, как все объявления Function Declaration будут обработаны, продолжится выполнение.
В результате функции, созданные как Function Declaration, могут быть вызваны раньше своих определений.
function sayHi(name) {
alert( `Привет, ${name}` );
}
Функция sayHi была создана, когда движок JavaScript подготавливал скрипт к выполнению, и такая функция видна
повсюду в этом скрипте.
sayHi("Вася"); // ошибка!
Функции, объявленные при помощи Function Expression, создаются тогда, когда выполнение доходит до них. Это
случится только на строке, помеченной звёздочкой (*) . Слишком поздно.
Ещё одна важная особенность Function Declaration заключается в их блочной области видимости.
В строгом режиме, когда Function Declaration находится в блоке {...} , функция доступна везде внутри
блока. Но не снаружи него.
Для примера давайте представим, что нам нужно объявить функцию welcome() в зависимости от значения
переменной age , которое мы получим во время выполнения кода. И затем запланируем использовать её когда-
нибудь в будущем.
Если мы попробуем использовать Function Declaration, это не заработает так, как задумывалось:
function welcome() {
85/597
alert("Привет!");
}
} else {
function welcome() {
alert("Здравствуйте!");
}
// ...не работает
welcome(); // Error: welcome is not defined
Это произошло, так как объявление Function Declaration видимо только внутри блока кода, в котором располагается.
} else {
function welcome() {
alert("Здравствуйте!");
}
}
Верным подходом будет воспользоваться функцией, объявленной при помощи Function Expression, и присвоить
значение welcome переменной, объявленной снаружи if , что обеспечит нам нужную видимость.
Такой код заработает, как ожидалось:
let welcome;
welcome = function() {
alert("Привет!");
};
} else {
welcome = function() {
alert("Здравствуйте!");
};
86/597
let age = prompt("Сколько Вам лет?", 18);
…Но если Function Declaration нам не подходит по какой-то причине, или нам нужно условное объявление (мы
рассмотрели это в примере выше), то следует использовать Function Expression.
Итого
● Функции – это значения. Они могут быть присвоены, скопированы или объявлены в любом месте кода.
●
Если функция объявлена как отдельная инструкция в основном потоке кода, то это “Function Declaration”.
● Если функция была создана как часть выражения, то это “Function Expression”.
● Function Declaration обрабатываются перед выполнением блока кода. Они видны во всём блоке.
●
Функции, объявленные при помощи Function Expression, создаются только когда поток выполнения достигает их.
В большинстве случаев, когда нам нужно объявить функцию, Function Declaration предпочтительнее, т.к функция
будет видна до своего объявления в коде. Это даёт нам больше гибкости в организации кода, и, как правило, делает
его более читабельным.
Исходя из этого, мы должны использовать Function Expression только тогда, когда Function Declaration не подходит
для нашей задачи. Мы рассмотрели несколько таких примеров в этой главе, и увидим ещё больше в будущем.
Существует ещё один очень простой и лаконичный синтаксис для создания функций, который часто лучше, чем
Function Expression.
Он называется «функции-стрелки» или «стрелочные функции» (arrow functions), т.к. выглядит следующим образом:
Это создаёт функцию func , которая принимает аргументы arg1..argN , затем вычисляет expression в правой
части с их использованием и возвращает результат.
Другими словами, это сокращённая версия:
87/597
alert( sum(1, 2) ); // 3
Как вы можете видеть, (a, b) => a + b задаёт функцию, которая принимает два аргумента с именами a и b . И
при выполнении она вычисляет выражение a + b и возвращает результат.
● Если у нас только один аргумент, то круглые скобки вокруг параметров можно опустить, сделав запись ещё короче:
alert( double(3) ); // 6
● Если аргументов нет, круглые скобки будут пустыми, но они должны присутствовать:
sayHi();
welcome();
Поначалу стрелочные функции могут показаться необычными и даже трудночитаемыми, но это быстро пройдёт по
мере того, как глаза привыкнут к этим конструкциям.
Они очень удобны для простых однострочных действий, когда лень писать много слов.
Стрелочные функции, которые мы видели до этого, были очень простыми. Они брали аргументы слева от => и
вычисляли и возвращали выражение справа.
Иногда нам нужна более сложная функция, с несколькими выражениями и инструкциями. Это также возможно, нужно
лишь заключить их в фигурные скобки. При этом важное отличие – в том, что в таких скобках для возврата значения
нужно использовать return (как в обычных функциях).
Вроде этого:
let sum = (a, b) => { // фигурная скобка, открывающая тело многострочной функции
let result = a + b;
return result; // если мы используем фигурные скобки, то нам нужно явно указать "return"
};
alert( sum(1, 2) ); // 3
Дальше – больше
Здесь мы представили главной целью стрелочных функций краткость. Но это ещё не всё!
Стрелочные функции обладают и другими интересными возможностями.
Чтобы изучить их более подробно, нам сначала нужно познакомиться с некоторыми другими аспектами JavaScript,
поэтому мы вернёмся к стрелочным функциям позже, в главе Повторяем стрелочные функции.
А пока мы можем использовать их для простых однострочных действий и колбэков.
88/597
Итого
Стрелочные функции очень удобны для простых действий, особенно для однострочных.
Они бывают двух типов:
1. Без фигурных скобок: (...args) => expression – правая сторона выражения: функция вычисляет его и
возвращает результат. Скобки можно не ставить, если аргумент только один: n => n * 2 .
2. С фигурными скобками: (...args) => { body } – скобки позволяют нам писать несколько инструкций внутри
функции, но при этом необходимо явно вызывать return , чтобы вернуть значение.
Задачи
ask(
"Вы согласны?",
function() { alert("Вы согласились."); },
function() { alert("Вы отменили выполнение."); }
);
К решению
Особенности JavaScript
Структура кода
alert('Привет'); alert('Мир');
Как правило, перевод строки также интерпретируется как разделитель, так тоже будет работать:
alert('Привет')
alert('Мир')
Это так называемая «автоматическая вставка точки с запятой». Впрочем, она не всегда срабатывает, например:
[1, 2].forEach(alert)
Большинство руководств по стилю кода рекомендуют ставить точку с запятой после каждой инструкции.
Точка с запятой не требуется после блоков кода {…} и синтаксических конструкций с ними, таких как, например,
циклы:
function f() {
// после объявления функции необязательно ставить точку с запятой
}
for(;;) {
89/597
// после цикла точка с запятой также необязательна
}
…Впрочем, если даже мы и поставим «лишнюю» точку с запятой, ошибки не будет. Она просто будет
проигнорирована.
Строгий режим
Чтобы по максимуму использовать возможности современного JavaScript, все скрипты рекомендуется начинать с
добавления директивы "use strict" .
'use strict';
...
Эту директиву следует размещать в первой строке скрипта или в начале тела функции.
Без "use strict" код также запустится, но некоторые возможности будут работать в «режиме совместимости» со
старыми версиями языка JavaScript. Нам же предпочтительнее современное поведение.
Некоторые конструкции языка (например, классы, которые нам ещё предстоит изучить) включают строгий режим по
умолчанию.
Подробности: Строгий режим — "use strict".
Переменные
let x = 5;
x = "Вася";
90/597
Взаимодействие с посетителем
В качестве рабочей среды мы используем браузер, так что простейшими функциями взаимодействия с посетителем
являются:
prompt(question, [default])
Задаёт вопрос question и возвращает то, что ввёл посетитель, либо null , если посетитель нажал на кнопку
«Отмена».
confirm(question)
Задаёт вопрос question и предлагает выбрать «ОК» или «Отмена». Выбор возвращается в формате true/false .
alert(message)
Выводит сообщение message .
Все эти функции показывают модальные окна, они останавливают выполнение кода и не позволяют посетителю
взаимодействовать со страницей, пока не будет дан ответ на вопрос.
Например:
Операторы
Арифметические
Простые * + - / , а также деление по модулю % и возведение в степень ** .
Бинарный плюс + объединяет строки. А если одним из операндов является строка, то второй тоже будет
конвертирован в строку:
Операторы присваивания
Простые a = b и составные a *= 2 .
Битовые операции
Битовые операторы работают с 32-битными целыми числами на самом низком, побитовом уровне. Подробнее об их
использовании можно прочитать на ресурсе MDN и в разделе Побитовые операторы.
Условный оператор
Единственный оператор с тремя параметрами: cond ? resultA : resultB . Если условие cond истинно,
возвращается resultA , иначе – resultB .
Логические операторы
Логические И && , ИЛИ || используют так называемое «ленивое вычисление» и возвращают значение, на котором
оно остановилось (не обязательно true или false ). Логическое НЕ ! конвертирует операнд в логический тип и
возвращает инвертированное значение.
91/597
Сравнение
Проверка на равенство == значений разных типов конвертирует их в число (за исключением null и undefined ,
которые могут равняться только друг другу), так что примеры ниже равны:
Другие операторы сравнения тоже конвертируют значения разных типов в числовой тип.
Оператор строгого равенства === не выполняет конвертирования: разные типы для него всегда означают разные
значения.
Значения null и undefined особенные: они равны == только друг другу, но не равны ничему ещё.
Операторы сравнения больше/меньше сравнивают строки посимвольно, остальные типы конвертируются в число.
Другие операторы
Существуют и другие операторы, такие как запятая.
Подробности: Базовые операторы, математика, Операторы сравнения, Логические операторы, Операторы нулевого
слияния и присваивания: '??', '??='.
Циклы
// 1
while (condition) {
...
}
// 2
do {
...
} while (condition);
// 3
for(let i = 0; i < 10; i++) {
...
}
● Переменная, объявленная в цикле for(let...) , видна только внутри цикла. Но мы также можем опустить let
и переиспользовать существующую переменную.
● Директивы break/continue позволяют выйти из цикла/текущей итерации. Используйте метки для выхода из
вложенных циклов.
Конструкция «switch»
Конструкция «switch» может заменить несколько проверок if . При сравнении она использует оператор строгого
равенства === .
Например:
switch (age) {
case 18:
alert("Так не сработает"); // результатом prompt является строка, а не число
case "18":
alert("А так сработает!");
break;
92/597
default:
alert("Любое значение, неравное значению выше");
}
Функции
function sum(a, b) {
let result = a + b;
return result;
}
return result;
};
3. Стрелочные функции:
// без аргументов
let sayHi = () => alert("Привет");
// с одним аргументом
let double = n => n * 2;
● У функций могут быть локальные переменные: т.е. объявленные в теле функции. Такие переменные видимы
только внутри функции.
●
У параметров могут быть значения по умолчанию: function sum(a = 1, b = 2) {...} .
● Функции всегда что-нибудь возвращают. Если нет оператора return , результатом будет undefined .
Это был краткий список возможностей JavaScript. На данный момент мы изучили только основы. Далее в учебнике вы
найдёте больше особенностей и продвинутых возможностей JavaScript.
Качество кода
В этой главе объясняются подходы к написанию кода, которые мы будем использовать в дальнейшем при разработке.
Отладка в браузере
93/597
Отладка – это процесс поиска и исправления ошибок в скрипте. Все современные браузеры и большинство других
сред разработки поддерживают инструменты для отладки – специальный графический интерфейс, который сильно
упрощает отладку. Он также позволяет по шагам отследить, что именно происходит в нашем коде.
Мы будем использовать браузер Chrome, так как у него достаточно возможностей, в большинстве других браузеров
процесс будет схожим.
Версия Chrome, установленная у вас, может выглядеть немного иначе, однако принципиальных отличий не будет.
● Работая в Chrome, откройте тестовую страницу.
●
Включите инструменты разработчика, нажав F12 (Mac: Cmd+Opt+I ).
● Щёлкните по панели Sources («исходный код»).
1 2 3
Интерфейс состоит из трёх зон:
1. В зоне File Navigator (панель для навигации файлов) показаны файлы HTML, JavaScript, CSS, включая
изображения, используемые на странице. Здесь также могут быть файлы различных расширений Chrome.
2. Зона Code Editor (редактор кода) показывает исходный код.
3. Наконец, зона JavaScript Debugging (панель отладки JavaScript) отведена для отладки, скоро мы к ней вернёмся.
Чтобы скрыть список ресурсов и освободить экранное место для исходного кода, щёлкните по тому же
переключателю .
Консоль
При нажатии на клавишу Esc в нижней части экрана вызывается консоль, где можно вводить команды и выполнять
их клавишей Enter .
94/597
Точки останова (breakpoints)
Давайте разберёмся, как работает код нашей тестовой страницы. В файле [Link] щёлкните на номере строки 4 .
Да-да, щёлкайте именно по самой цифре, не по коду.
Ура! Вы поставили точку останова. А теперь щёлкните по цифре 8 на восьмой линии.
Вот что в итоге должно получиться (синим это те места, по которым вы должны щёлкнуть):
вот список
точки останова
Точка останова – это участок кода, где отладчик автоматически приостановит исполнение JavaScript.
Пока исполнение поставлено «на паузу», мы можем просмотреть текущие значения переменных, выполнить команды
в консоли, другими словами, выполнить отладку кода.
В правой части графического интерфейса мы видим список точек останова. А когда таких точек выставлено много, да
ещё и в разных файлах, этот список поможет эффективно ими управлять:
●
Быстро перейдите к точке останова в коде (нажав на неё на правой панели).
● Временно отключите точку останова, сняв с неё галочку.
●
Удалите точку останова, щёлкнув правой кнопкой мыши и выбрав Remove (Удалить).
●
…и так далее.
Команда debugger
Выполнение кода можно также приостановить с помощью команды debugger прямо изнутри самого кода:
function hello(name) {
let phrase = `Привет, ${name}!`;
say(phrase);
}
Такая команда сработает только если открыты инструменты разработки, иначе браузер ее проигнорирует.
95/597
Остановимся и оглядимся
В нашем примере функция hello() вызывается во время загрузки страницы, поэтому для начала отладки (после
того, как мы поставили точки останова) проще всего её перезагрузить. Нажмите F5 (Windows, Linux) или Cmd+R
(Mac).
Выполнение прервётся на четвёртой строчке (где находится точка останова):
смотреть за выражениями 1
2
посмотреть детали внешнего вызова
текущие переменные 3
В текущий момент отладчик находится внутри вызова hello() , вызываемого скриптом в [Link] (там нет
функции, поэтому она называется “анонимной”).
Если вы нажмёте на элемент стека (например, «anonymous»), отладчик перейдёт к соответствующему коду, и нам
представляется возможность его проанализировать.
Там также есть ключевое слово this , которое мы ещё не изучали, но скоро изучим.
Для этого есть кнопки в верхней части правой панели. Давайте рассмотрим их.
вложенные вызовы
96/597
Выполнение кода возобновилось, дошло до другой точки останова внутри say() , и отладчик снова приостановил
выполнение. Обратите внимание на пункт «Call stack» справа: в списке появился ещё один вызов. Сейчас мы внутри
say() .
– «Step over»: выполнить следующую команду, но не заходя внутрь функции, быстрая клавиша F10 .
Работает аналогично предыдущей команде «Step», но ведёт себя по-другому, если следующая инструкция является
вызовом функции (имеется ввиду: не встроенная, как alert , а объявленная нами функция).
Если сравнить, то команда «Step» переходит во вложенный вызов функцию и приостанавливает выполнение в первой
строке, в то время как «Step over» выполняет вызов вложенной функции незаметно для нас, пропуская её внутренний
код.
– «Step out»: продолжить выполнение до завершения текущей функции, быстрая клавиша Shift+F11 .
Продолжает выполнение и останавливает его в самой последней строке текущей функции. Это удобно, когда мы
случайно вошли во вложенный вызов, используя , но это нас не интересует, и мы хотим продолжить его до конца
как можно скорее.
Continue to here
Щелчок правой кнопкой мыши по строке кода открывает контекстное меню с отличной опцией под названием
«Continue to here» («продолжить до этого места»).
Это удобно, когда мы хотим перейти на несколько шагов вперёд к строке, но лень устанавливать точку останова
(breakpoint).
Логирование
Обычный пользователь сайта не увидит такой вывод, так как он в консоли. Чтобы увидеть его, либо откройте
консольную панель инструментов разработчика, либо нажмите Esc , находясь в другой панели: это откроет консоль
внизу.
97/597
Если правильно выстроить логирование в приложении, то можно и без отладчика разобраться, что происходит в коде.
Итого
При остановке мы можем отлаживать: анализировать переменные и пошагово пройти по процессу, чтобы отыскать
проблему.
В инструментах разработчика гораздо больше опций, чем описано здесь. С полным руководством можно
ознакомиться на [Link] .
Информации из этой главы достаточно, чтобы начать отладку, но позже, особенно если вы много работаете с
браузером, пожалуйста, перейдите туда и ознакомьтесь с расширенными возможностями инструментов
разработчика.
И, конечно, вы можете просто кликать по различным местам инструментов разработки и смотреть, что при этом
появляется. Пожалуй, это наискорейший способ ими овладеть. Не забывайте про правый клик мыши и контекстные
меню!
Синтаксис
Пробелы
вокруг операторов
Отступ
2 пробела 2
Пробел
Точка с запятой ;
после for/if/while…
обязательна
Пробел
между
аргументами
98/597
Фигурные скобки
В большинстве JavaScript проектов фигурные скобки пишутся в так называемом «египетском» стиле с открывающей
скобкой на той же строке, что и соответствующее ключевое слово – не на новой строке. Перед открывающей скобкой
должен быть пробел, как здесь:
if (condition) {
// делай это
// ...и это
// ...и потом это
}
А что если у нас однострочная запись, типа if (condition) doSomething() , должны ли мы использовать
фигурные скобки?
Вот различные варианты расстановки скобок с комментариями, посмотрите сами, какой вам кажется самым
читаемым:
1. 😠 Такое иногда бывает в коде начинающих. Плохо, фигурные скобки не нужны:
2. 😠 Никогда не разделяйте строки без фигурных скобок, можно ненароком сделать ошибку при добавлении строк:
if (n < 0)
alert(`Степень ${n} не поддерживается`);
if (n < 0) {
alert(`Степень ${n} не поддерживается`);
}
Для очень короткого кода допустима одна строка. Например: if (cond) return null . Но блок кода (последний
вариант) обычно всё равно читается лучше.
Длина строки
Никто не любит читать длинные горизонтальные строки кода. Лучше всего разбивать их, например:
if (
id === 123 &&
moonPhase === 'Waning Gibbous' &&
zodiacSign === 'Libra'
) {
letTheSorceryBegin();
}
Максимальную длину строки согласовывают в команде. Обычно это 80 или 120 символов.
99/597
Отступы
Существует два типа отступов:
●
Горизонтальные отступы: 2 или 4 пробела.
Горизонтальный отступ выполняется с помощью 2 или 4 пробелов, или символа табуляции (клавиша Tab ). Какой
из них выбрать – это уже на ваше усмотрение. Пробелы больше распространены.
Одно из преимуществ пробелов над табуляцией заключается в том, что пробелы допускают более гибкие
конфигурации отступов, чем символ табуляции.
show(parameters,
aligned, // 5 пробелов слева
one,
after,
another
) {
// ...
}
function pow(x, n) {
let result = 1;
// <--
for (let i = 0; i < n; i++) {
result *= x;
}
// <--
return result;
}
Вставляйте дополнительный перевод строки туда, где это сделает код более читаемым. Не должно быть более 9
строк кода подряд без вертикального отступа.
Точка с запятой
Точки с запятой должны присутствовать после каждого выражения, даже если их, казалось бы, можно пропустить.
Есть языки, в которых точка с запятой необязательна и редко используется. Однако в JavaScript бывают случаи, когда
перенос строки не интерпретируется, как точка с запятой, что может привести к ошибкам. Подробнее об этом – в
главе о структуре кода.
Если вы – опытный разработчик на JavaScript, то можно выбрать стиль кода без точек с запятой, например
StandardJS . В ином случае, лучше будет использовать точки с запятой, чтобы избежать подводных камней.
Большинство разработчиков их ставят.
Уровни вложенности
Уровней вложенности должно быть немного.
Например, в цикле бывает полезно использовать директиву continue , чтобы избежать лишней вложенности.
Например, вместо добавления вложенного условия if , как здесь:
Мы можем написать:
100/597
... // <- нет лишнего уровня вложенности
}
Первая:
function pow(x, n) {
if (n < 0) {
alert("Отрицательные значения 'n' не поддерживаются");
} else {
let result = 1;
return result;
}
}
Вторая:
function pow(x, n) {
if (n < 0) {
alert("Отрицательные значения 'n' не поддерживаются");
return;
}
let result = 1;
return result;
}
Второй вариант является более читабельным, потому что «особый случай» n < 0 обрабатывается на ранней
стадии. После проверки можно переходить к «основному» потоку кода без необходимости увеличения вложенности.
Размещение функций
Если вы пишете несколько вспомогательных функций, а затем используемый ими код, то существует три способа
организации функций.
1. Объявить функции перед кодом, который их вызовет:
// объявление функций
function createElement() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}
101/597
2. Сначала код, затем функции
function setHandler(elem) {
...
}
function walkAround() {
...
}
Это потому, что при чтении кода мы сначала хотим знать, что он делает. Если сначала идёт код, то это тут же
становится понятно. И тогда, может быть, нам вообще не нужно будет читать функции, особенно если их имена
хорошо подобраны.
Руководство по стилю содержит общие правила о том, как писать код, например: какие кавычки использовать,
сколько пробелов отступать, максимальную длину строки и так далее – в общем, множество мелочей.
Когда все участники команды используют одно и то же руководство по стилю, код выглядит одинаково, независимо от
того, кто из команды его написал.
Конечно, команда всегда может написать собственное руководство по стилю, но обычно в этом нет необходимости.
Существует множество уже готовых.
Некоторые популярные руководства:
●
Google JavaScript Style Guide
● Airbnb JavaScript Style Guide (есть перевод )
● [Link] (есть перевод )
● StandardJS
Если вы – начинающий разработчик, то начните со шпаргалки в начале этой главы. Как только вы освоитесь,
просмотрите другие руководства, чтобы выбрать общие принципы и решить, какое вам больше подходит.
Автоматизированные средства проверки, так называемые «линтеры» – это инструменты, которые могут
автоматически проверять стиль вашего кода и вносить предложения по его улучшению.
Самое замечательное в них то, что проверка стиля может также найти программные ошибки, такие как опечатки в
именах переменных или функций. Из-за этой особенности использовать линтер рекомендуется, даже если вы не
хотите придерживаться какого-то конкретного «стиля кода».
Вот некоторые известные инструменты для проверки:
● JSLint – проверяет код на соответствие стилю JSLint , в онлайн-интерфейсе вверху можно ввести код, а внизу
– различные настройки проверки, чтобы попробовать её в действии.
● JSHint – больше проверок, чем в JSLint.
● ESLint – пожалуй, самый современный линтер.
102/597
Большинство линтеров интегрированы со многими популярными редакторами: просто включите плагин в редакторе и
настройте стиль.
Например, для ESLint вы должны выполнить следующее:
1. Установите [Link] .
2. Установите ESLint с помощью команды npm install -g eslint (npm – установщик пакетов JavaScript).
3. Создайте файл конфигурации с именем .eslintrc в корне вашего JavaScript-проекта (в папке, содержащей все
ваши файлы).
4. Установите/включите плагин для вашего редактора, который интегрируется с ESLint. У большинства редакторов он
есть.
{
"extends": "eslint:recommended",
"env": {
"browser": true,
"node": true,
"es6": true
},
"rules": {
"no-console": 0,
"indent": ["warning", 2]
}
}
Здесь директива "extends" означает, что конфигурация основана на наборе настроек «eslint:recommended». После
этого мы уточняем наши собственные.
Кроме того, возможно загрузить наборы правил стиля из сети и расширить их. Смотрите [Link]
guide/getting-started для получения более подробной информации об установке.
Также некоторые среды разработки имеют встроенные линтеры, возможно, удобные, но не такие гибкие в настройке,
как ESLint.
Итого
Все правила синтаксиса, описанные в этой главе (и в ссылках на руководства по стилю), направлены на повышение
читаемости вашего кода. О любых можно поспорить.
Когда мы думаем о написании «лучшего» кода, мы должны задать себе вопросы: «Что сделает код более читаемым и
лёгким для понимания?» и «Что может помочь избегать ошибок?». Это – основные моменты, о которых следует
помнить при выборе и обсуждении стилей кода.
Чтение популярных руководств по стилю позволит вам быть в курсе лучших практик и последних идей и тенденций в
стилях написания кода.
Задачи
Плохой стиль
важность: 4
function pow(x,n)
{
let result=1;
for(let i=0;i<n;i++) {result*=x;}
return result;
}
103/597
{
alert(pow(x,n))
}
К решению
Комментарии
Как мы знаем из главы Структура кода, комментарии могут быть однострочными, начинающимися с // , и
многострочными: /* ... */ .
Плохие комментарии
Новички склонны использовать комментарии, чтобы объяснять, «что делает код». Например, так:
Но в хорошем коде количество «объясняющих» комментариев должно быть минимальным. Серьёзно, код должен
быть таким, чтобы его можно было понять без комментариев.
Про это есть хорошее правило: «Если код настолько запутанный, что требует комментариев, то, может быть, его
стоит переделать?»
function showPrimes(n) {
nextPrime:
for (let i = 2; i < n; i++) {
alert(i);
}
}
function showPrimes(n) {
alert(i);
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if (n % i == 0) return false;
}
return true;
}
104/597
Теперь код легче понять. Функция сама становится комментарием. Такой код называется самодокументированным.
// ...
addWhiskey(glass);
addJuice(glass);
function addWhiskey(container) {
for(let i = 0; i < 10; i++) {
let drop = getWhiskey();
//...
}
}
function addJuice(container) {
for(let t = 0; t < 3; t++) {
let tomato = getTomato();
//...
}
}
Здесь комментарии тоже не нужны: функции сами говорят, что делают (если вы понимаете английский язык). И ещё,
структура кода лучше, когда он разделён на части. Понятно, что делает каждая функция, что она принимает и что
возвращает.
Хорошие комментарии
Итак, обычно «объясняющие» комментарии – это плохо. Но тогда какой комментарий считается хорошим?
Описывайте архитектуру
Сделайте высокоуровневый обзор компонентов, того, как они взаимодействуют, каков поток управления в различных
ситуациях… Если вкратце – обзор кода с высоты птичьего полёта. Существует специальный язык UML для
создания диаграмм, разъясняющих архитектуру кода. Его определённо стоит изучить.
Например:
/**
* Возвращает x, возведённое в n-ную степень.
*
105/597
* @param {number} x Возводимое в степень число.
* @param {number} n Степень, должна быть натуральным числом.
* @return {number} x, возведённое в n-ную степень.
*/
function pow(x, n) {
...
}
Подобные комментарии позволяют нам понимать назначение функции и правильно её использовать без
необходимости заглядывать в код.
Кстати, многие редакторы, такие как WebStorm , прекрасно их распознают для того, чтобы выполнить
автодополнение ввода и различные автоматические проверки кода.
Также существуют инструменты, например, JSDoc 3 , которые умеют генерировать HTML-документацию из
комментариев. Получить больше информации о JSDoc вы можете здесь: [Link] .
Комментарии, объясняющие решение, очень важны. Они помогают продолжать разработку в правильном
направлении.
Итого
Комментарии – важный признак хорошего разработчика, причём как их наличие, так и отсутствие.
Хорошие комментарии позволяют нам поддерживать код, дают возможность вернуться к нему после перерыва и
эффективнее его использовать.
Комментируйте:
● Общую архитектуру, вид «с высоты птичьего полёта».
●
Использование функций.
● Неочевидные решения, важные детали.
Избегайте комментариев:
●
Которые объясняют, как работает код, и что он делает.
● Используйте их только в тех случаях, когда невозможно сделать настолько простой и самодокументированный код,
что он не потребует комментариев.
Средства для генерации документации по коду, такие как JSDoc3, также используют комментарии: они их читают и
генерируют HTML-документацию (или документацию в другом формате).
Ниндзя-код
Предлагаю вашему вниманию советы мастеров древности.
Программисты прошлого использовали их, чтобы заострить разум тех, кто после них будет поддерживать код.
Гуру разработки при найме старательно ищут их применение в тестовых заданиях.
106/597
Новички иногда используют их ещё лучше, чем матёрые ниндзя.
Прочитайте их и решите, кто вы: ниндзя, новичок или, может быть, гуру?
Осторожно, ирония!
Многие пытались пройти по пути ниндзя. Мало, кто преуспел.
«Меньше букв» – уважительная причина для нарушения любых соглашений. Ваш верный помощник – возможности
языка, использованные неочевидным образом.
// код из jQuery
i = i ? i < 0 ? [Link](0, len + i) : i : 0;
Разработчик, встретивший эту строку и попытавшийся понять, чему же всё-таки равно i , скорее всего, придёт к вам
за разъяснениями. Смело скажите ему, что короче – это всегда лучше. Посвятите и его в пути ниндзя. Не забудьте
вручить Дао дэ цзин .
Однобуквенные переменные
Ещё один способ писать быстрее – использовать короткие имена переменных. Называйте их a , b или c .
Короткая переменная прячется в коде лучше, чем ниндзя в лесу. Никто не сможет найти её, используя функцию
«Поиск» текстового редактора. Более того, даже найдя – никто не сможет «расшифровать» её и догадаться, что она
означает.
…Но есть одно исключение. В тех местах, где однобуквенные переменные общеприняты, например, в счётчике цикла
– ни в коем случае не используйте стандартные названия i , j , k . Где угодно, только не здесь!
Используйте сокращения
Если правила, принятые в вашей команде, запрещают использовать абстрактные имена или имена из одной буквы –
сокращайте их.
Например:
●
list → lst .
● userAgent → ua .
● browser → brsr .
●
…и т.д.
Только коллеги с хорошо развитой интуицией поймут такие имена. Вообще, старайтесь сокращать всё. Только
одарённые интуицией люди достойны заниматься поддержкой вашего кода.
107/597
Великий образ не имеет формы.
При выборе имени старайтесь применить максимально абстрактное слово, например obj , data , value , item ,
elem и т.п.
●
Идеальное имя для переменной: data . Используйте это имя везде, где можно. В конце концов, каждая
переменная содержит данные, не правда ли?
…Но что делать, если имя data уже занято? Попробуйте value , оно не менее универсально. Ведь каждая
переменная содержит значение.
● Называйте переменную по типу данных, которые она хранит: str , num …
Ведь как раз тип легко понять, запустив отладчик и посмотрев, что внутри. Но в чём смысл этой переменной? Что
за массив/объект/число в ней хранится? Без долгой медитации над кодом тут не обойтись!
● …Но что делать, если и эти имена закончились? Просто добавьте цифру: data1, item2, elem5 …
Проверка внимания
Только истинно внимательный программист достоин понять ваш код. Но как проверить, достоин ли читающий?
Один из способов – использовать похожие имена переменных, например, date и data .
Бегло прочитать такой код почти невозможно. А уж заметить опечатку и поправить её… Ммммм… Мы здесь надолго,
время попить чайку.
Если вам приходится использовать длинные, понятные имена переменных – что поделать… Но и здесь есть простор
для творчества!
Назовите переменные «калькой» с русского языка или как-то «улучшите» английское слово.
В одном месте напишите let ssilka , в другом let ssylka , в третьем let link , в четвёртом – let lnk … Это
действительно великолепно работает и очень креативно!
Количество ошибок при поддержке такого кода увеличивается во много раз.
Хитрые синонимы
Чтобы было не скучно – используйте похожие названия для обозначения одинаковых действий.
Например, если метод показывает что-то на экране – начните его название с display.. (скажем,
displayElement ), а в другом месте объявите аналогичный метод как show.. ( showFrame ).
Как бы намекните этим, что существует тонкое различие между способами показа в этих методах, хотя на
самом деле его нет.
По возможности, договоритесь с членами своей команды. Если Вася в своих классах использует display.. , то
Валера – обязательно render.. , а Петя – paint.. .
…И напротив, если есть две функции с важными отличиями – используйте одно и то же слово для их описания!
Например, с print... можно начать метод печати на принтере printPage , а также – метод добавления текста на
страницу printText .
А теперь пусть читающий код думает: «Куда же выводит сообщение printMessage?». Особый шик – добавить элемент
неожиданности. Пусть printMessage выводит не туда, куда все, а в новое окно!
108/597
Повторно используйте имена
По возможности, повторно используйте имена переменных, функций и свойств. Просто записывайте в них новые
значения.
Добавляйте новое имя, только если это абсолютно необходимо. В функции старайтесь обойтись только теми
переменными, которые были переданы как параметры.
Это не только затруднит идентификацию того, что сейчас находится в переменной, но и сделает почти невозможным
поиск места, в котором конкретное значение было присвоено.
Цель – развить интуицию и память читающего код программиста. Ну, а пока интуиция слаба, он может построчно
анализировать код и конспектировать изменения переменных для каждой ветки исполнения.
Продвинутый вариант этого подхода – незаметно (!) подменить переменную на нечто похожее, например:
function ninjaFunction(elem) {
// 20 строк кода, работающего с elem
elem = clone(elem);
Программист, пожелавший добавить действия с elem во вторую часть функции, будет удивлён. Лишь во время
отладки, посмотрев весь код, он с удивлением обнаружит, что, оказывается, имел дело с клоном!
Регулярные встречи с этим приёмом на практике говорят: защититься невозможно. Эффективно даже против
опытного ниндзи.
Добавляйте подчёркивания
Добавляйте подчёркивания _ и __ к именам переменных. Например, _name или __value . Желательно, чтобы их
смысл был известен только вам, а лучше – вообще без явной причины.
Этим вы достигните двух целей. Во-первых, код станет длиннее и менее читаемым, а во-вторых, другой программист
будет долго искать смысл в подчёркиваниях. Особенно хорошо сработает и внесёт сумятицу в его мысли, если в
некоторых частях проекта подчёркивания будут, а в некоторых – нет.
В процессе развития кода вы, скорее всего, будете путаться и смешивать стили: добавлять имена с подчёркиваниями
там, где обычно подчёркиваний нет, и наоборот. Это нормально и полностью соответствует третьей цели – увеличить
количество ошибок при внесении исправлений.
Пусть все видят, какими замечательными сущностями вы оперируете! Имена superElement , megaFrame и
niceItem при благоприятном положении звёзд могут привести к просветлению читающего.
Действительно, с одной стороны, кое-что написано: super.. , mega.. , nice.. С другой – это не несёт никакой
конкретики. Читающий может решить поискать в этом глубинный смысл и замедитировать на часок-другой
оплаченного рабочего времени.
109/597
Почему бы не использовать одинаковые переменные внутри и снаружи функции? Это просто и не требует
придумывать новых имён.
function render() {
let user = anotherValue();
...
...многобукв...
...
... // <-- программист захочет внести исправления сюда, и...
...
}
Зашедший в середину метода render программист, скорее всего, не заметит, что переменная user локально
перекрыта и попытается работать с ней, полагая, что это – результат authenticateUser() … Ловушка
захлопнулась! Здравствуй, отладчик.
Внимание… Сюр-при-из!
Есть функции, название которых говорит о том, что они ничего не меняют. Например, isReady() ,
checkPermission() , findTags() … Предполагается, что при вызове они произведут некие вычисления или
найдут и возвратят полезные данные, но при этом их не изменят. В трактатах это называется «отсутствие сторонних
эффектов».
По-настоящему красивый приём – делать в таких функциях что-нибудь полезное, заодно с процессом
проверки. Что именно – совершенно неважно.
Удивление и ошеломление, которое возникнет у вашего коллеги, когда он увидит, что функция с названием на is.. ,
check.. или find... что-то меняет – несомненно, расширит его границы разумного!
Мощные функции!
Например, функция validateEmail(email) может, кроме проверки e-mail на правильность, выводить сообщение
об ошибке и просить заново ввести e-mail.
Выберите хотя бы пару дополнительных действий, кроме основного назначения функции. Главное – они должны быть
неочевидны из названия функции. Истинный ниндзя-разработчик сделает так, что они будут неочевидны и из кода
тоже.
Объединение нескольких смежных действий в одну функцию защитит ваш код от повторного использования.
Представьте, что другому разработчику нужно только проверить адрес, а сообщение – не выводить. Ваша функция
validateEmail(email) , которая делает и то и другое, ему не подойдёт. И он не прервёт вашу медитацию
вопросами о ней.
Итого
Все советы выше пришли из реального кода… И в том числе, от разработчиков с большим опытом. Возможно, даже
больше вашего, так что не судите опрометчиво ;)
●
Следуйте нескольким из них – и ваш код станет полон сюрпризов.
110/597
●
Следуйте многим – и ваш код станет истинно вашим, никто не захочет изменять его.
● Следуйте всем – и ваш код станет ценным уроком для молодых разработчиков, ищущих просветления.
Далее у нас будут задачи, для проверки которых используется автоматическое тестирование. Также его часто
применяют в реальных проектах.
Обычно, когда мы пишем функцию, мы легко можем представить, что она должна делать, и как она будет вести себя в
зависимости от переданных параметров.
Во время разработки мы можем проверить правильность работы функции, просто вызвав её, например, из консоли и
сравнив полученный результат с ожидаемым.
Если функция работает не так, как мы ожидаем, то можно внести исправления в код и запустить её ещё раз. Так
можно повторять до тех пор, пока функция не станет работать так, как нам нужно.
Например, мы работаем над функцией f . Написали часть кода и решили протестировать. Выясняется, что f(1)
работает правильно, в то время как f(2) – нет. Мы вносим в код исправления, и теперь f(2) работает правильно.
Вроде бы, всё хорошо, не так ли? Однако, мы забыли заново протестировать f(1) . Возможно, после внесения
правок f(1) стала работать неправильно.
Это – типичная ситуация. Во время разработки мы учитываем множество различных сценариев использования. Но
сложно ожидать, что программист станет вручную проверять каждый из них после любого изменения кода. Поэтому
легко исправить что-то одно и при этом сломать что-то другое.
Автоматическое тестирование означает, что тесты пишутся отдельно, в дополнение к коду. Они по-разному
запускают наши функции и сравнивают результат с ожидаемым.
Давайте начнём с техники под названием Behavior Driven Development или, коротко, BDD.
Эта задача взята в качестве примера. В JavaScript есть оператор ** , который служит для возведения в степень. Мы
сосредоточимся на процессе разработки, который также можно применять и для более сложных задач.
Перед тем, как начать писать код функции pow , мы можем представить себе, что она должна делать, и описать её.
Такое описание называется спецификацией (specification). Она содержит описания различных способов
использования и тесты для них, например:
describe("pow", function() {
});
111/597
Какую функциональность мы описываем. В нашем случае мы описываем функцию pow . Используется для
группировки рабочих лошадок – блоков it .
[Link](value1, value2)
Код внутри блока it , если функция работает верно, должен выполняться без ошибок.
Функции вида assert.* используются для проверки того, что функция pow работает так, как мы ожидаем. В этом
примере мы используем одну из них – [Link] , которая сравнивает переданные значения и выбрасывает
ошибку, если они не равны друг другу. Существуют и другие типы сравнений и проверок, которые мы добавим позже.
Спецификация может быть запущена, и при этом будет выполнена проверка, указанная в блоке it , мы увидим это
позднее.
Процесс разработки
Таким образом, разработка проходит итеративно. Мы пишем спецификацию, реализуем её, проверяем, что тесты
выполняются без ошибок, пишем ещё тесты, снова проверяем, что они проходят и т.д.
Давайте посмотрим этот поток разработки на нашем примере.
Первый шаг уже завершён. У нас есть спецификация для функции pow . Теперь, перед тем, как писать реализацию,
давайте подключим библиотеки для пробного запуска тестов, просто чтобы убедиться, что тесты работают
(разумеется, они завершатся ошибками).
Спецификация в действии
Эти библиотеки подходят как для тестирования внутри браузера, так и на стороне сервера. Мы рассмотрим вариант с
браузером.
<!DOCTYPE html>
<html>
<head>
<!-- добавим стили mocha для отображения результатов -->
<link rel="stylesheet" href="[Link]
<!-- добавляем сам фреймворк mocha -->
<script src="[Link]
<script>
// включаем режим тестирования в стиле BDD
112/597
[Link]('bdd');
</script>
<!-- добавим chai -->
<script src="[Link]
<script>
// chai предоставляет большое количество функций. Объявим assert глобально
let assert = [Link];
</script>
</head>
<body>
<script>
function pow(x, n) {
/* Здесь будет реализация функции, пока пусто */
}
</script>
</html>
Результаты:
pow
✖ возводит число в степень n ‣
Пока что тест завершается ошибкой. Это логично, потому что у нас пустая функция pow , так что pow(2,3)
возвращает undefined вместо 8 .
На будущее отметим, что существуют более высокоуровневые фреймворки для тестирования, такие как karma и
другие. С их помощью легко сделать автозапуск множества тестов.
Начальная реализация
function pow(x, n) {
return 8; // :) сжульничаем!
}
113/597
Вау, теперь всё работает!
pow
✓ возводит в степень n ‣
Улучшаем спецификацию
Конечно, мы сжульничали. Функция не работает. Попытка посчитать pow(3, 3) даст некорректный результат,
однако тесты проходят.
…Такая ситуация вполне типична, она случается на практике. Тесты проходят, но функция работает неправильно.
Наша спецификация не идеальна. Нужно дополнить её тестами.
Давайте добавим ещё один тест, чтобы посмотреть, что pow(3, 3) = 27 .
describe("pow", function() {
});
describe("pow", function() {
});
Принципиальная разница в том, что когда один из assert выбрасывает ошибку, то выполнение it блока тут же
прекращается. Таким образом, если первый assert выбросит ошибку, результат работы второго assert мы уже не
узнаем.
Разделять тесты предпочтительнее, так как мы получаем больше информации о том, что конкретно пошло не так.
Если вы посмотрите на тест и увидите в нём две независимые проверки, то такой тест лучше разделить на два более
простых.
114/597
passes: 1 failures: 1 duration: 0.48s
pow
✓ 2 в степени 3 будет 8 ‣
✖ 3 в степени 3 будет 27 ‣
Как мы и ожидали, второй тест провалился. Естественно, наша функция всегда возвращает 8 , в то время как
assert ожидает 27 .
Улучшаем реализацию
Давайте напишем что-то более похожее на функцию возведения в степень, чтобы заставить тесты проходить.
function pow(x, n) {
let result = 1;
return result;
}
Чтобы убедиться, что эта реализация работает нормально, давайте протестируем её на большем количестве
значений. Чтобы не писать вручную каждый блок it , мы можем генерировать их в цикле for :
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} в степени 3 будет ${expected}`, function() {
[Link](pow(x, 3), expected);
});
}
});
Результат:
pow
✓ 1 в степени 3 будет 1 ‣
✓ 2 в степени 3 будет 8 ‣
✓ 3 в степени 3 будет 27 ‣
✓ 4 в степени 3 будет 64 ‣
✓ 5 в степени 3 будет 125 ‣
Мы собираемся добавить больше тестов. Однако, перед этим стоит сгруппировать вспомогательную функцию
makeTest и цикл for . Нам не нужна функция makeTest в других тестах, она нужна только в цикле for . Её
предназначение – проверить, что pow правильно возводит число в заданную степень.
Группировка производится вложенными блоками describe :
115/597
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} в степени 3 будет ${expected}`, function() {
[Link](pow(x, 3), expected);
});
}
});
Вложенные describe образуют новую подгруппу тестов. В результатах мы можем видеть дополнительные отступы в
названиях.
pow
возводит x в степень 3
✓ 1 в степени 3 будет 1 ‣
✓ 2 в степени 3 будет 8 ‣
✓ 3 в степени 3 будет 27 ‣
✓ 4 в степени 3 будет 64 ‣
✓ 5 в степени 3 будет 125 ‣
В будущем мы можем написать новые it и describe блоки на верхнем уровне со своими собственными
вспомогательными функциями. Им не будет доступна функция makeTest из примера выше.
116/597
before/after и beforeEach/afterEach
Мы можем задать before/after функции, которые будут выполняться до/после тестов, а также функции
beforeEach/afterEach , выполняемые до/после каждого it .
Например:
describe("тест", function() {
});
Расширение спецификации
Основная функциональность pow реализована. Первая итерация разработки завершена. Когда мы закончим
отмечать и пить шампанское, давайте продолжим работу и улучшим pow .
Как было сказано, функция pow(x, n) предназначена для работы с целыми положительными значениями n .
Для обозначения математических ошибок функции JavaScript обычно возвращают NaN . Давайте делать также для
некорректных значений n .
Сначала давайте опишем это поведение в спецификации.
describe("pow", function() {
// ...
});
117/597
passes: 5 failures: 2 duration: 0.31s
pow
✖ для отрицательных n возвращает NaN ‣
возводит x в степень 3
✓ 1 в степени 3 будет 1 ‣
✓ 2 в степени 3 будет 8 ‣
✓ 3 в степени 3 будет 27 ‣
✓ 4 в степени 3 будет 64 ‣
✓ 5 в степени 3 будет 125 ‣
Новые тесты падают, потому что наша реализация не поддерживает их. Так работает BDD. Сначала мы добавляем
тесты, которые падают, а уже потом пишем под них реализацию.
function pow(x, n) {
if (n < 0) return NaN;
if ([Link](n) != n) return NaN;
let result = 1;
return result;
}
118/597
passes: 7 failures: 0 duration: 1.41s
pow
✓ если n - отрицательное число, результат будет NaN ‣
✓ если n - дробное число, результат будет NaN ‣
возводит x в степень 3
✓ 1 в степени 3 будет 1 ‣
✓ 2 в степени 3 будет 8 ‣
✓ 3 в степени 3 будет 27 ‣
✓ 4 в степени 3 будет 64 ‣
✓ 5 в степени 3 будет 125 ‣
Итого
В BDD сначала пишут спецификацию, а потом реализацию. В конце у нас есть и то, и другое.
Спецификацию можно использовать тремя способами:
1. Как Тесты – они гарантируют, что функция работает правильно.
2. Как Документацию – заголовки блоков describe и it описывают поведение функции.
3. Как Примеры – тесты, по сути, являются готовыми примерами использования функции.
Имея спецификацию, мы можем улучшить, изменить и даже переписать функцию с нуля, и при этом мы будем
уверены, что она продолжает работать правильно.
Это особенно важно в больших проектах, когда одна функция может быть использована во множестве мест. Когда мы
вносим в такую функцию изменения, у нас нет никакой возможности вручную проверить, что она продолжает
работать правильно во всех местах, где её используют.
Не имея тестов, людям приходится выбирать один из двух путей:
1. Внести изменения, и неважно, что будет. Потом у наших пользователей станут проявляться ошибки, ведь мы
наверняка что-то забудем проверить вручную.
2. Или же, если наказание за ошибки в коде серьёзное, то люди просто побоятся вносить изменения в такие функции.
Код будет стареть, «зарастать паутиной», и никто не захочет в него лезть. Это нехорошо для разработки.
Кроме того, код, хорошо покрытый тестами, как правило, имеет лучшую архитектуру.
Это естественно, ведь такой код легче менять и улучшать. Но не только по этой причине.
Для написания тестов нужно организовать код таким образом, чтобы у каждой функции была ясно поставленная
задача и точно определены её аргументы и возвращаемое значение. А это означает, что мы получаем хорошую
архитектуру с самого начала.
В реальности это не всегда так просто. Иногда сложно написать спецификацию до того, как будет написана
реализация, потому что не всегда чётко понятно, как та или иная функция должна себя вести. Но в общем и целом
написание тестов делает разработку быстрее, а итоговый продукт более стабильным.
Далее по книге мы встретим много задач с тестами, так что вы увидите много практических примеров.
Написание тестов требует хорошего знания JavaScript. Но мы только начали учить его. Не волнуйтесь. Пока вам не
нужно писать тесты, но вы уже умеете их читать и поймёте даже более сложные примеры, чем те, что были
представлены в этой главе.
Задачи
119/597
it("Возводит x в степень n", function() {
let x = 5;
let result = x;
[Link](pow(x, 1), result);
result *= x;
[Link](pow(x, 2), result);
result *= x;
[Link](pow(x, 3), result);
});
К решению
Полифилы
Разработчики JavaScript-движков сами решают, какие предложения реализовывать в первую очередь. Они могут
заранее добавить в браузеры поддержку функций, которые всё ещё находятся в черновике, и отложить разработку
функций, которые уже перенесены в спецификацию, потому что они менее интересны разработчикам или более
сложные в реализации.
Таким образом, довольно часто реализуется только часть стандарта.
Можно проверить текущее состояние поддержки различных возможностей JavaScript на странице [Link]
[Link]/compat-table/es6/ (нам ещё предстоит изучить многое из этого списка).
Babel
Когда мы используем современные возможности JavaScript, некоторые движки могут не поддерживать их. Как было
сказано выше, не везде реализованы все функции.
2. Во-вторых, полифил.
Новые возможности языка могут включать встроенные функции и синтаксические конструкции. Транспилер
переписывает код, преобразовывая новые синтаксические конструкции в старые. Но что касается новых
встроенных функций, нам нужно их как-то реализовать. JavaScript является высокодинамичным языком, скрипты
могут добавлять/изменять любые функции, чтобы они вели себя в соответствии с современным стандартом.
Термин «полифил» означает, что скрипт «заполняет» пробелы и добавляет современные функции.
Интересное хранилище полифилов:
● core js поддерживает много функций, можно подключать только нужные.
Таким образом, чтобы современные функции поддерживались в старых движках, нам надо установить транспилер и
добавить полифил.
Примеры в учебнике
120/597
Google Chrome обычно поддерживает современные функции, можно запускать новейшие примеры без каких-либо
транспилеров, но и другие современные браузеры тоже хорошо работают.
Объекты: основы
Объекты
Как мы знаем из главы Типы данных, в JavaScript существует 8 типов данных. Семь из них называются
«примитивными», так как содержат только одно значение (будь то строка, число или что-то другое).
Объекты же используются для хранения коллекций различных значений и более сложных сущностей. В JavaScript
объекты используются очень часто, это одна из основ языка. Поэтому мы должны понять их, прежде чем углубляться
куда-либо ещё.
Объект может быть создан с помощью фигурных скобок {…} с необязательным списком свойств. Свойство – это
пара «ключ: значение», где ключ – это строка (также называемая «именем свойства»), а значение может быть чем
угодно.
Мы можем представить объект в виде ящика с подписанными папками. Каждый элемент данных хранится в своей
папке, на которой написан ключ. По ключу папку легко найти, удалить или добавить в неё что-либо.
key1
key2
key3
Пустой объект («пустой ящик») можно создать, используя один из двух вариантов синтаксиса:
пусто
user
Обычно используют вариант с фигурными скобками {...} . Такое объявление называют литералом объекта или
литеральной нотацией.
Литералы и свойства
При использовании литерального синтаксиса {...} мы сразу можем поместить в объект несколько свойств в виде
пар «ключ: значение»:
У каждого свойства есть ключ (также называемый «имя» или «идентификатор»). После имени свойства следует
двоеточие ":" , и затем указывается значение свойства. Если в объекте несколько свойств, то они перечисляются
через запятую.
Можно сказать, что наш объект user – это ящик с двумя папками, подписанными «name» и «age».
121/597
name
age
user
Мы можем в любой момент добавить в него новые папки, удалить папки или прочитать содержимое любой папки.
Для обращения к свойствам используется запись «через точку»:
Значение может быть любого типа. Давайте добавим свойство с логическим значением:
[Link] = true;
isAdmin
name
age
user
delete [Link];
isAdmin
name
user
Имя свойства может состоять из нескольких слов, но тогда оно должно быть заключено в кавычки:
let user = {
name: "John",
age: 30,
"likes birds": true // имя свойства из нескольких слов должно быть в кавычках
};
name
likes birds
age
user
122/597
let user = {
name: "John",
age: 30,
}
Это называется «висячая запятая». Такой подход упрощает добавление, удаление и перемещение свойств, так как
все строки объекта становятся одинаковыми.
Например:
const user = {
name: "John"
};
alert([Link]); // Pete
Может показаться, что строка (*) должна вызвать ошибку, но нет, здесь всё в порядке. Дело в том, что
объявление const защищает от изменений только саму переменную user , а не её содержимое.
Определение const выдаст ошибку только если мы присвоим переменной другое значение: user=... .
Есть ещё один способ сделать константами свойства объекта, который мы рассмотрим в главе Флаги и
дескрипторы свойств.
Квадратные скобки
Для свойств, имена которых состоят из нескольких слов, доступ к значению «через точку» не работает:
JavaScript видит, что мы обращаемся к свойству [Link] , а затем идёт непонятное слово birds . В итоге
синтаксическая ошибка.
Точка требует, чтобы ключ был именован по правилам именования переменных. То есть не имел пробелов, не
начинался с цифры и не содержал специальные символы, кроме $ и _ .
Для таких случаев существует альтернативный способ доступа к свойствам через квадратные скобки. Такой способ
сработает с любым именем свойства:
// удаление свойства
delete user["likes birds"];
Сейчас всё в порядке. Обратите внимание, что строка в квадратных скобках заключена в кавычки (подойдёт любой
тип кавычек).
Квадратные скобки также позволяют обратиться к свойству, имя которого может быть результатом выражения.
Например, имя свойства может храниться в переменной:
123/597
// то же самое, что и user["likes birds"] = true;
user[key] = true;
Здесь переменная key может быть вычислена во время выполнения кода или зависеть от пользовательского ввода.
После этого мы используем её для доступа к свойству. Это даёт нам большую гибкость.
Пример:
let user = {
name: "John",
age: 30
};
let user = {
name: "John",
age: 30
};
Вычисляемые свойства
Мы можем использовать квадратные скобки в литеральной нотации для создания вычисляемого свойства.
Пример:
let bag = {
[fruit]: 5, // имя свойства будет взято из переменной fruit
};
Смысл вычисляемого свойства прост: запись [fruit] означает, что имя свойства необходимо взять из переменной
fruit .
И если посетитель введёт слово "apple" , то в объекте bag теперь будет лежать свойство {apple: 5} .
Квадратные скобки дают намного больше возможностей, чем запись через точку. Они позволяют использовать любые
имена свойств и переменные, хотя и требуют более громоздких конструкций кода.
124/597
Подведём итог: в большинстве случаев, когда имена свойств известны и просты, используется запись через точку.
Если же нам нужно что-то более сложное, то мы используем квадратные скобки.
Свойство из переменной
В реальном коде часто нам необходимо использовать существующие переменные как значения для свойств с тем же
именем.
Например:
В примере выше название свойств name и age совпадают с названиями переменных, которые мы подставляем в
качестве значений этих свойств. Такой подход настолько распространён, что существуют специальные короткие
свойства для упрощения этой записи.
Вместо name:name мы можем написать просто name :
Мы можем использовать как обычные свойства, так и короткие в одном и том же объекте:
let user = {
name, // тоже самое, что и name:name
age: 30
};
Как мы уже знаем, имя переменной не может совпадать с зарезервированными словами, такими как «for», «let»,
«return» и т.д.
Но для свойств объекта такого ограничения нет:
Иными словами, нет никаких ограничений к именам свойств. Они могут быть в виде строк или символов
(специальный тип для идентификаторов, который будет рассмотрен позже).
Все другие типы данных будут автоматически преобразованы к строке.
Например, если использовать число 0 в качестве ключа, то оно превратится в строку "0" :
125/597
let obj = {
0: "Тест" // то же самое что и "0": "Тест"
};
// обе функции alert выведут одно и то же свойство (число 0 преобразуется в строку "0")
alert( obj["0"] ); // Тест
alert( obj[0] ); // Тест (то же свойство)
Есть небольшой подводный камень, связанный со специальным свойством __proto__ . Мы не можем установить его
в необъектное значение:
Мы более подробно исследуем особенности свойства __proto__ в следующих главах Прототипное наследование, а
также предложим способы исправления такого поведения.
В отличие от многих других языков, особенность JavaScript-объектов в том, что можно получить доступ к любому
свойству. Даже если свойства не существует – ошибки не будет!
При обращении к свойству, которого нет, возвращается undefined . Это позволяет просто проверить существование
свойства:
Также существует специальный оператор "in" для проверки существования свойства в объекте.
Синтаксис оператора:
"key" in object
Пример:
Обратите внимание, что слева от оператора in должно быть имя свойства. Обычно это строка в кавычках.
Если мы опускаем кавычки, это значит, что мы указываем переменную, в которой находится имя свойства. Например:
В большинстве случаев прекрасно сработает сравнение с undefined . Но есть особый случай, когда оно не
подходит и нужно использовать "in" .
Это когда свойство существует, но содержит значение undefined :
let obj = {
test: undefined
126/597
};
В примере выше свойство [Link] технически существует в объекте. Оператор in сработал правильно.
Подобные ситуации случаются очень редко, так как undefined обычно явно не присваивается. Для «неизвестных»
или «пустых» свойств мы используем значение null .
Цикл "for..in"
Для перебора всех свойств объекта используется цикл for..in . Этот цикл отличается от изученного ранее цикла
for(;;) .
Синтаксис:
let user = {
name: "John",
age: 30,
isAdmin: true
};
Обратите внимание, что все конструкции «for» позволяют нам объявлять переменную внутри цикла, как, например,
let key здесь.
Кроме того, мы могли бы использовать другое имя переменной. Например, часто используется вариант "for (let
prop in obj)" .
let codes = {
"49": "Германия",
"41": "Швейцария",
"44": "Великобритания",
// ..,
"1": "США"
};
Если мы делаем сайт для немецкой аудитории, то, вероятно, мы хотим, чтобы код 49 был первым.
Но если мы запустим код, мы увидим совершенно другую картину:
●
США (1) идёт первым
127/597
●
затем Швейцария (41) и так далее.
Телефонные коды идут в порядке возрастания, потому что они являются целыми числами: 1, 41, 44, 49 .
…С другой стороны, если ключи не целочисленные, то они перебираются в порядке создания, например:
let user = {
name: "John",
surname: "Smith"
};
[Link] = 25; // добавим ещё одно свойство
Таким образом, чтобы решить нашу проблему с телефонными кодами, мы можем схитрить, сделав коды не
целочисленными свойствами. Добавления знака "+" перед каждым кодом будет достаточно.
Пример:
let codes = {
"+49": "Германия",
"+41": "Швейцария",
"+44": "Великобритания",
// ..,
"+1": "США"
};
Итого
Дополнительные операторы:
●
Удаление свойства: delete [Link] .
128/597
●
Проверка существования свойства: "key" in obj .
● Перебор свойств объекта: цикл for for (let key in obj) .
То, что мы изучали в этой главе, называется «простым объектом» («plain object») или просто Object .
У них есть свои особенности, которые мы изучим позже. Иногда люди говорят что-то вроде «тип данных Array» или
«тип данных Date», но формально они не являются отдельными типами, а относятся к типу данных Object . Они
лишь расширяют его различными способами.
Объекты в JavaScript очень мощные. Здесь мы только немного углубились в действительно огромную тему. Мы будем
плотно работать с объектами и узнаем о них больше в следующих частях учебника.
Задачи
Привет, object
важность: 5
К решению
Проверка на пустоту
важность: 5
Напишите функцию isEmpty(obj) , которая возвращает true , если у объекта нет свойств, иначе false .
К решению
Объекты-константы?
важность: 5
const user = {
name: "John"
};
129/597
// это будет работать?
[Link] = "Pete";
К решению
let salaries = {
John: 100,
Ann: 160,
Pete: 130
}
Напишите код для суммирования всех зарплат и сохраните результат в переменной sum . Должно получиться 390 .
К решению
Создайте функцию multiplyNumeric(obj) , которая умножает все числовые свойства объекта obj на 2 .
Например:
// до вызова функции
let menu = {
width: 200,
height: 300,
title: "My menu"
};
multiplyNumeric(menu);
Обратите внимание, что multiplyNumeric не нужно ничего возвращать. Следует напрямую изменять объект.
К решению
130/597
let message = "Привет!";
let phrase = message;
В результате мы имеем две независимые переменные, каждая из которых хранит строку "Привет!" .
"П
"П
ри
ри
ве
ве
т!
т!
"
"
message phrase
let user = {
name: "John"
};
user
Объект хранится где-то в памяти (справа от изображения), в то время как переменная user (слева) имеет лишь
«ссылку» на него.
Мы можем думать о переменной объекта, такой как user , как о листе бумаги с адресом объекта на нем.
Когда мы выполняем действия с объектом, к примеру, берём свойство [Link] , движок JavaScript просматривает
то, что находится по этому адресу, и выполняет операцию с самим объектом.
Теперь вот почему это важно.
Теперь у нас есть две переменные, каждая из которых содержит ссылку на один и тот же объект:
131/597
name
user admin
Как вы можете видеть, все ещё есть один объект, но теперь с двумя переменными, которые ссылаются на него.
Мы можем использовать любую переменную для доступа к объекту и изменения его содержимого:
Это как если бы у нас был шкафчик с двумя ключами, и мы использовали один из них ( admin ), чтобы войти в него и
внести изменения. А затем, если мы позже используем другой ключ ( user ), мы все равно открываем тот же шкафчик
и можем получить доступ к изменённому содержимому.
Сравнение по ссылке
Два объекта равны только в том случае, если это один и тот же объект.
let a = {};
let b = a; // копирование по ссылке
И здесь два независимых объекта не равны, даже если они выглядят одинаково (оба пусты):
let a = {};
let b = {}; // два независимых объекта
alert( a == b ); // false
Для сравнений типа obj1 > obj2 или для сравнения с примитивом obj == 5 объекты преобразуются в
примитивы. Очень скоро мы изучим, как работают преобразования объектов, но, по правде говоря, такие сравнения
требуются очень редко и обычно они появляются в результате ошибок программиста.
Итак, копирование объектной переменной создаёт ещё одну ссылку на тот же объект.
Но что, если нам всё же нужно дублировать объект? Создать независимую копию, клон?
Это тоже выполнимо, но немного сложнее, потому что в JavaScript для этого нет встроенного метода. Но на самом
деле в этом редко возникает необходимость, копирования по ссылке в большинстве случаев вполне хватает.
Но если мы действительно этого хотим, то нам нужно создать новый объект и воспроизвести структуру
существующего, перебрав его свойства и скопировав их на примитивном уровне.
Например так:
132/597
let user = {
name: "John",
age: 30
};
Синтаксис:
●
Первый аргумент dest — целевой объект.
●
Остальные аргументы src1, ..., srcN (может быть столько, сколько необходимо) являются исходными
объектами
● Метод копирует свойства всех исходных объектов src1, ..., srcN в целевой объект dest . Другими словами,
свойства всех аргументов, начиная со второго, копируются в первый объект.
●
Возвращает объект dest .
Мы также можем использовать [Link] для замены цикла for..in для простого клонирования:
let user = {
name: "John",
age: 30
};
133/597
Вложенное клонирование
До сих пор мы предполагали, что все свойства user примитивныe. Но свойства могут быть и ссылками на другие
объекты. Что с ними делать?
Например, есть объект:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
Теперь недостаточно просто скопировать [Link] = [Link] , потому что [Link] – это объект, он
будет скопирован по ссылке. Таким образом, clone и user будут иметь общий объект sizes :
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
Чтобы исправить это, мы должны использовать цикл клонирования, который проверяет каждое значение user[key]
и, если это объект, тогда также копирует его структуру. Это называется «глубоким клонированием».
Мы можем реализовать глубокое клонирование, используя рекурсию. Или, чтобы не изобретать велосипед заново,
возьмите готовую реализацию, например _.cloneDeep(obj) из библиотеки JavaScript lodash .
Также мы можем использовать глобальный метод structuredClone() , который позволяет сделать полную копию
объекта. К сожалению он поддерживается только современными браузерами. Здесь можно ознакомиться с
поддержкой этого метода.
134/597
Объекты, объявленные как константа, могут быть изменены
Важным побочным эффектом хранения объектов в качестве ссылок является то, что объект, объявленный как
const , может быть изменён.
Например:
const user = {
name: "John"
};
alert([Link]); // Pete
Может показаться, что строка (*) вызовет ошибку, но, это не так. Значение user это константа, оно всегда
должно ссылаться на один и тот же объект, но свойства этого объекта могут свободно изменяться.
Другими словами, const user выдаст ошибку только в том случае, если мы попытаемся задать user=... в
целом.
Тем не менее, если нам действительно нужно создать постоянные свойства объекта, это тоже возможно, но с
использованием совершенно других методов. Мы затронем это в главе Флаги и дескрипторы свойств.
Итого
Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не «значение объекта», а
«ссылку» (адрес в памяти) на это значение. Таким образом, копирование такой переменной или передача её в
качестве аргумента функции копирует эту ссылку, а не сам объект.
Все операции с использованием скопированных ссылок (например, добавление/удаление свойств) выполняются с
одним и тем же объектом.
Чтобы создать «реальную копию» (клон), мы можем использовать [Link] для так называемой
«поверхностной копии» (вложенные объекты копируются по ссылке) или функцию «глубокого клонирования», такую
как _.cloneDeep(obj) .
Сборка мусора
Управление памятью в JavaScript выполняется автоматически и незаметно. Мы создаём примитивы, объекты,
функции… Всё это занимает память.
Но что происходит, когда что-то больше не нужно? Как движок JavaScript обнаруживает, что пора очищать память?
Достижимость
Например:
● Выполняемая в данный момент функция, её локальные переменные и параметры.
●
Другие функции в текущей цепочке вложенных вызовов, их локальные переменные и параметры.
●
Глобальные переменные.
● (некоторые другие внутренние значения)
135/597
В движке JavaScript есть фоновый процесс, который называется сборщиком мусора . Он отслеживает все объекты
и удаляет те, которые стали недоступными.
Простой пример
<global>
user
Object
name: "John"
Здесь стрелка обозначает ссылку на объект. Глобальная переменная user ссылается на объект {name: "John"}
(мы будем называть его просто «John» для краткости). В свойстве "name" объекта John хранится примитив, поэтому
оно нарисовано внутри объекта.
Если перезаписать значение user , то ссылка потеряется:
user = null;
<global>
user: null
Object
name: "John"
Теперь объект John становится недостижимым. К нему нет доступа, на него нет ссылок. Сборщик мусора удалит эти
данные и освободит память.
Две ссылки
<global>
user admin
Object
name: "John"
user = null;
136/597
…то объект John всё ещё достижим через глобальную переменную admin , поэтому он находится в памяти. Если бы
мы также перезаписали admin , то John был бы удалён.
Взаимосвязанные объекты
return {
father: man,
mother: woman
}
}
Функция marry «женит» два объекта, давая им ссылки друг на друга, и возвращает новый объект, содержащий
ссылки на два предыдущих.
В результате получаем такую структуру памяти:
<global variable>
family
Object
father mother
wife
Object Object
name: "John" husband name: "Ann"
delete [Link];
delete [Link];
<global variable>
family
Object
father mother
wife
Object Object
name: "John" husband name: "Ann"
Недостаточно удалить только одну из этих двух ссылок, потому что все объекты останутся достижимыми.
Но если мы удалим обе, то увидим, что у объекта John больше нет входящих ссылок:
137/597
<global>
family
Object
mother
wife
Object Object
name: "John" name: "Ann"
Исходящие ссылки не имеют значения. Только входящие ссылки могут сделать объект достижимым. Объект John
теперь недостижим и будет удалён из памяти со всеми своими данными, которые также стали недоступны.
После сборки мусора:
<global>
family
Object
mother
Object
name: "Ann"
Недостижимый «остров»
Вполне возможна ситуация, при которой целый «остров» взаимосвязанных объектов может стать недостижимым и
удалиться из памяти.
family = null;
<global>
family: null
Object
father mother
wife
Object Object
name: "John" husband name: "Ann"
Объекты John и Ann всё ещё связаны, оба имеют входящие ссылки, но этого недостаточно.
Бывший объект family был отсоединён от корня, на него больше нет ссылки, поэтому весь «остров» становится
недостижимым и будет удалён.
Внутренние алгоритмы
Основной алгоритм сборки мусора называется «алгоритм пометок» (от англ. «mark-and-sweep»).
138/597
● Затем он идёт по отмеченным объектам и отмечает их ссылки. Все посещённые объекты запоминаются, чтобы в
будущем не посещать один и тот же объект дважды.
●
…И так далее, пока не будут посещены все достижимые (из корней) ссылки.
● Все непомеченные объекты удаляются.
<global>
Мы ясно видим «недостижимый остров» справа. Теперь давайте посмотрим, как будет работать «алгоритм пометок»
сборщика мусора.
<global>
<global>
<global>
Теперь объекты, которые не удалось посетить в процессе, считаются недостижимыми и будут удалены:
<global> недостижимые
139/597
Мы также можем представить себе этот процесс как выливание огромного ведра краски из корней, которая течёт по
всем ссылкам и отмечает все достижимые объекты. Затем непомеченные удаляются.
Это концепция того, как работает сборка мусора. Движки JavaScript применяют множество оптимизаций, чтобы она
работала быстрее и не задерживала выполнение кода.
Вот некоторые из оптимизаций:
●
Сборка по поколениям (Generational collection) – объекты делятся на два набора: «новые» и «старые». В
типичном коде многие объекты имеют короткую жизнь: они появляются, выполняют свою работу и быстро умирают,
так что имеет смысл отслеживать новые объекты и, если это так, быстро очищать от них память. Те, которые
выживают достаточно долго, становятся «старыми» и проверяются реже.
● Инкрементальная сборка (Incremental collection) – если объектов много, и мы пытаемся обойти и пометить весь
набор объектов сразу, это может занять некоторое время и привести к видимым задержкам в выполнении скрипта.
Так что движок делит всё множество объектов на части, и далее очищает их одну за другой. Получается несколько
небольших сборок мусора вместо одной всеобщей. Это требует дополнительного учёта для отслеживания
изменений между частями, но зато получается много крошечных задержек вместо одной большой.
●
Сборка в свободное время (Idle-time collection) – чтобы уменьшить возможное влияние на производительность,
сборщик мусора старается работать только во время простоя процессора.
Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось
описать их здесь, я должен воздержаться, потому что разные движки реализуют разные хитрости и методы. И, что
ещё более важно, все меняется по мере развития движков, поэтому изучать тему глубоко «заранее», без реальной
необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будет несколько
ссылок ниже.
Итого
Также в блоге V8 время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы
изучить сборку мусора, вам лучше подготовиться, узнав о том как устроен движок V8 внутри в целом и почитав блог
Вячеслава Егорова , одного из инженеров, разрабатывавших V8. Я говорю про «V8», потому что он лучше всего
освещается в статьях в Интернете. Для других движков многие подходы схожи, но сборка мусора отличается во
многих аспектах.
Глубокое понимание работы движков полезно, когда вам нужна низкоуровневая оптимизация. Было бы разумно
запланировать их изучение как следующий шаг после того, как вы познакомитесь с языком.
Объекты обычно создаются, чтобы представлять сущности реального мира, будь то пользователи, заказы и так
далее:
// Объект пользователя
let user = {
name: "John",
age: 30
};
И так же, как и в реальном мире, пользователь может совершать действия: выбирать что-то из корзины покупок,
авторизовываться, выходить из системы, оплачивать и т.п.
140/597
Такие действия в JavaScript представлены функциями в свойствах.
Примеры методов
let user = {
name: "John",
age: 30
};
[Link] = function() {
alert("Привет!");
};
[Link](); // Привет!
Здесь мы просто использовали Function Expression (функциональное выражение), чтобы создать функцию
приветствия, и присвоили её свойству [Link] нашего объекта.
let user = {
// ...
};
// сначала, объявляем
function sayHi() {
alert("Привет!");
}
[Link](); // Привет!
Объектно-ориентированное программирование
Когда мы пишем наш код, используя объекты для представления сущностей реального мира, – это называется
объектно-ориентированным программированием или сокращённо: «ООП».
ООП является большой предметной областью и интересной наукой самой по себе. Как выбрать правильные
сущности? Как организовать взаимодействие между ними? Это – создание архитектуры, и на эту тему есть
отличные книги, такие как «Приёмы объектно-ориентированного проектирования. Паттерны проектирования»
авторов Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес или «Объектно-ориентированный анализ и
проектирование с примерами приложений» Гради Буча, а также ещё множество других книг.
user = {
sayHi: function() {
alert("Привет");
}
};
141/597
}
};
Как было показано, мы можем пропустить ключевое слово "function" и просто написать sayHi() .
Нужно отметить, что эти две записи не полностью эквивалентны. Есть тонкие различия, связанные с наследованием
объектов (что будет рассмотрено позже), но на данном этапе изучения это неважно. Почти во всех случаях
сокращённый синтаксис предпочтителен.
Как правило, методу объекта обычно требуется доступ к информации, хранящейся в объекте, для выполнения своей
работы.
Например, коду внутри [Link]() может потребоваться имя пользователя, которое хранится в объекте user .
Для доступа к информации внутри объекта метод может использовать ключевое слово this .
Значение this – это объект «перед точкой», который используется для вызова метода.
Например:
let user = {
name: "John",
age: 30,
sayHi() {
// "this" - это "текущий объект".
alert([Link]);
}
};
[Link](); // John
Здесь во время выполнения кода [Link]() значением this будет являться user (ссылка на объект user ).
Технически также возможно получить доступ к объекту без ключевого слова this , обратившись к нему через
внешнюю переменную (в которой хранится ссылка на этот объект):
let user = {
name: "John",
age: 30,
sayHi() {
alert([Link]); // "user" вместо "this"
}
};
…Но такой код ненадёжен. Если мы решим скопировать ссылку на объект user в другую переменную, например,
admin = user , и перезапишем переменную user чем-то другим, тогда будет осуществлён доступ к неправильному
объекту при вызове метода из admin .
Это показано ниже:
let user = {
name: "John",
age: 30,
sayHi() {
alert( [Link] ); // приведёт к ошибке
}
};
142/597
[Link](); // TypeError: Cannot read property 'name' of null
Если бы мы использовали [Link] вместо [Link] внутри alert , тогда этот код бы сработал.
В JavaScript ключевое слово «this» ведёт себя иначе, чем в большинстве других языков программирования. Его
можно использовать в любой функции, даже если это не метод объекта.
В следующем примере нет синтаксической ошибки:
function sayHi() {
alert( [Link] );
}
function sayHi() {
alert( [Link] );
}
admin['f'](); // Admin (нет разницы между использованием точки или квадратных скобок для доступа к объекту)
Правило простое: если вызывается obj.f() , то во время вызова f , this – это obj . Так что, в приведённом выше
примере это либо user , либо admin .
function sayHi() {
alert(this);
}
sayHi(); // undefined
В строгом режиме ( "use strict" ) в таком коде значением this будет являться undefined . Если мы
попытаемся получить доступ к [Link] – это вызовет ошибку.
В нестрогом режиме значением this в таком случае будет глобальный объект ( window в браузерe, мы
вернёмся к этому позже в главе Глобальный объект). Это – исторически сложившееся поведение this , которое
исправляется использованием строгого режима ( "use strict" ).
Обычно подобный вызов является ошибкой программирования. Если внутри функции используется this , тогда
она ожидает, что будет вызвана в контексте какого-либо объекта.
143/597
Последствия свободного this
Если вы до этого изучали другие языки программирования, то вы, вероятно, привыкли к идее
«фиксированного this » – когда методы, определённые в объекте, всегда имеют this , ссылающееся на этот
объект.
В JavaScript this является «свободным», его значение вычисляется в момент вызова метода и не зависит от
того, где этот метод был объявлен, а скорее от того, какой объект вызывает метод (какой объект стоит «перед
точкой»).
Эта концепция вычисления this в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны,
функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С
другой стороны, большая гибкость увеличивает вероятность ошибок.
Здесь наша позиция заключается не в том, чтобы судить, является ли это архитектурное решение в языке
хорошим или плохим. Скоро мы поймем, как с этим работать, как получить выгоду и избежать проблем.
Стрелочные функции особенные: у них нет своего «собственного» this . Если мы ссылаемся на this внутри такой
функции, то оно берётся из внешней «нормальной» функции.
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert([Link]);
arrow();
}
};
[Link](); // Ilya
Это особенность стрелочных функций. Она полезна, когда мы на самом деле не хотим иметь отдельное this , а
скорее хотим взять его из внешнего контекста. Позже в главе Повторяем стрелочные функции мы увидим больше
примеров на эту тему.
Итого
●
Функции, которые находятся в свойствах объекта, называются «методами».
● Методы позволяют объектам «действовать»: [Link]() .
●
Методы могут ссылаться на объект через this .
Также ещё раз заметим, что стрелочные функции являются особенными – у них нет this . Когда внутри стрелочной
функции обращаются к this , то его значение берётся извне.
Задачи
144/597
function makeUser() {
return {
name: "John",
ref: this
};
}
К решению
Создайте калькулятор
важность: 5
● read() (читать) запрашивает два значения и сохраняет их как свойства объекта с именами a и b .
● sum() (суммировать) возвращает сумму сохранённых значений.
● mul() (умножить) перемножает сохранённые значения и возвращает результат.
let calculator = {
// ... ваш код ...
};
[Link]();
alert( [Link]() );
alert( [Link]() );
Запустить демо
К решению
Цепь вызовов
важность: 2
let ladder = {
step: 0,
up() {
[Link]++;
},
down() {
[Link]--;
},
showStep: function() { // показывает текущую ступеньку
alert( [Link] );
}
};
Теперь, если нам нужно выполнить несколько последовательных вызовов, мы можем сделать это так:
[Link]();
[Link]();
[Link]();
[Link](); // 1
[Link]();
[Link](); // 0
Измените код методов up , down и showStep таким образом, чтобы их вызов можно было сделать по цепочке,
например так:
145/597
[Link]().up().down().showStep().down().showStep(); // показывает 1 затем 0
К решению
Обычный синтаксис {...} позволяет создать только один объект. Но зачастую нам нужно создать множество
похожих, однотипных объектов, таких как пользователи, элементы меню и так далее.
Это можно сделать при помощи функции-конструктора и оператора "new" .
Функция-конструктор
Например:
function User(name) {
[Link] = name;
[Link] = false;
}
alert([Link]); // Jack
alert([Link]); // false
function User(name) {
// this = {}; (неявно)
Таким образом, let user = new User("Jack") возвращает тот же результат, что и:
let user = {
name: "Jack",
isAdmin: false
};
Теперь, если нам будет необходимо создать других пользователей, мы можем просто вызвать new User("Ann") ,
new User("Alice") и так далее. Данная конструкция гораздо удобнее и читабельнее, чем многократное создание
литерала объекта.
146/597
Это и является основной целью конструкторов – реализовать код для многократного создания однотипных объектов.
Давайте ещё раз отметим – технически любая функция (кроме стрелочных функций, поскольку у них нет this )
может использоваться в качестве конструктора. Его можно запустить с помощью new , и он выполнит выше
указанный алгоритм. Подобные функции должны начинаться с заглавной буквы – это общепринятое соглашение,
чтобы было ясно, что функция должна вызываться с помощью «new».
new function() { … }
Если в нашем коде присутствует большое количество строк, создающих один сложный объект, то мы можем
обернуть их в функцию-конструктор, которая будет немедленно вызвана, вот так:
Такой конструктор не может быть вызван снова, так как он нигде не сохраняется, просто создаётся и тут же
вызывается. Таким образом, этот трюк направлен на инкапсуляцию кода, который создаёт отдельный объект, без
возможности повторного использования в будущем.
Продвинутая возможность
Синтаксис из этого раздела используется крайне редко. Вы можете пропустить его, если не хотите углубляться в
детали языка.
Используя специальное свойство [Link] внутри функции, мы можем проверить, вызвана ли функция при
помощи оператора new или без него.
В случае обычного вызова функции [Link] будет undefined . Если же она была вызвана при помощи new ,
[Link] будет равен самой функции.
function User() {
alert([Link]);
}
// без "new":
User(); // undefined
// с "new":
new User(); // function User { ... }
Это можно использовать внутри функции, чтобы узнать, была ли она вызвана при помощи new , «в режиме
конструктора», или без него, «в обычном режиме».
Также мы можем сделать, чтобы вызовы с new и без него делали одно и то же:
function User(name) {
if (![Link]) { // в случае, если вы вызвали меня без оператора new
return new User(name); // ...я добавлю new за вас
}
[Link] = name;
}
147/597
Такой подход иногда используется в библиотеках, чтобы сделать синтаксис более гибким. Чтобы люди могли
вызывать функцию с new и без него, и она все ещё могла работать.
Впрочем, вероятно, это не очень хорошая практика использовать этот трюк везде, так как отсутствие new может
ввести разработчика в заблуждение. С new мы точно знаем, что создаётся новый объект.
Обычно конструкторы не имеют оператора return . Их задача – записать все необходимое в this , и это
автоматически становится результатом.
Другими словами, return с объектом возвращает этот объект, во всех остальных случаях возвращается this .
function BigUser() {
[Link] = "John";
А вот пример с пустым return (или мы могли бы поставить примитив после return , неважно):
function SmallUser() {
[Link] = "John";
Обычно у конструкторов отсутствует return . Здесь мы упомянули особое поведение с возвращаемыми объектами в
основном для полноты картины.
Пропуск скобок
Кстати, мы можем не ставить круглые скобки после new :
Пропуск скобок считается плохой практикой, но просто чтобы вы знали, такой синтаксис разрешён
спецификацией.
Использование конструкторов для создания объектов даёт большую гибкость. Функции-конструкторы могут иметь
параметры, определяющие, как создавать объект и что в него записывать.
Конечно, мы можем добавить к this не только свойства, но и методы.
Например, new User(name) ниже создаёт объект с заданным name и методом sayHi :
function User(name) {
[Link] = name;
148/597
[Link] = function() {
alert( "Меня зовут: " + [Link] );
};
}
/*
john = {
name: "John",
sayHi: function() { ... }
}
*/
Для создания сложных объектов есть и более продвинутый синтаксис – классы, который мы рассмотрим позже.
Итого
●
Функции-конструкторы или просто конструкторы, являются обычными функциями, но существует общепринятое
соглашение именовать их с заглавной буквы.
● Функции-конструкторы следует вызывать только с помощью new . Такой вызов подразумевает создание пустого
this в начале и возврат заполненного в конце.
JavaScript предоставляет функции-конструкторы для множества встроенных объектов языка: таких как Date , Set , и
других, которые нам ещё предстоит изучить.
Задачи
alert( a == b ); // true
К решению
●
read() запрашивает два значения при помощи prompt и сохраняет их значение в свойствах объекта.
● sum() возвращает сумму этих свойств.
149/597
●
mul() возвращает произведение этих свойств.
Например:
Запустить демо
К решению
● Хранить «текущее значение» в свойстве value . Начальное значение устанавливается в аргументе конструктора
startingValue .
● Метод read() должен использовать prompt для считывания нового числа и прибавления его к value .
Другими словами, свойство value представляет собой сумму всех введённых пользователем значений, с учётом
начального значения startingValue .
Запустить демо
К решению
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Опциональная цепочка ?. — это безопасный способ доступа к свойствам вложенных объектов, даже если какое-
либо из промежуточных свойств не существует.
Если вы только начали читать учебник и изучать JavaScript, то, возможно, проблема вас ещё не коснулась, но она
довольно распространена.
В качестве примера предположим, что у нас есть объекты user , которые содержат информацию о наших
пользователях.
У большинства наших пользователей есть адреса в свойстве [Link] с улицей [Link] , но
некоторые из них их не указали.
150/597
В таком случае, когда мы попытаемся получить [Link] , а пользователь окажется без адреса, мы
получим ошибку:
alert([Link]); // Ошибка!
Это ожидаемый результат. JavaScript работает следующим образом. Поскольку [Link] имеет значение
undefined , попытка получить [Link] завершается ошибкой.
Во многих практических случаях мы бы предпочли получить здесь undefined вместо ошибки (что означало бы
«улицы нет»).
…Или ещё один пример. В веб-разработке мы можем получить объект, соответствующий элементу веб-страницы, с
помощью специального вызова метода, такого как [Link]('.elem') , и он возвращает null ,
когда такого элемента нет.
Ещё раз, если элемент не существует, мы получим сообщение об ошибке доступа к свойству .innerHTML у null . И
в некоторых случаях, когда отсутствие элемента является нормальным, мы хотели бы избежать ошибки и просто
принять html = null в качестве результата.
Как мы можем это сделать?
Очевидным решением было бы проверить значение с помощью if или условного оператора ? , прежде чем
обращаться к его свойству, вот так:
Это работает, тут нет ошибки… Но это довольно неэлегантно. Как вы можете видеть, "[Link]" появляется в
коде дважды.
Как видно, поиск элемента [Link]('.elem') здесь вызывается дважды, что не очень хорошо.
Для более глубоко вложенных свойств это ещё менее красиво, поскольку потребуется больше повторений.
Это просто ужасно, у кого-то могут даже возникнуть проблемы с пониманием такого кода.
Есть немного лучший способ написать это, используя оператор && :
Проход при помощи логического оператора И && через весь путь к свойству гарантирует, что все компоненты
существуют (если нет, вычисление прекращается), но также не является идеальным.
151/597
Как вы можете видеть, имена свойств по-прежнему дублируются в коде. Например, в приведённом выше коде
[Link] появляется три раза.
Вот почему в язык была добавлена опциональная цепочка ?. . Чтобы решить эту проблему – раз и навсегда!
Опциональная цепочка
Опциональная цепочка ?. останавливает вычисление и возвращает undefined , если значение перед ?. равно
undefined или null .
Далее в этой статье, для краткости, мы будем говорить, что что-то «существует», если оно не является null
и не undefined .
Другими словами, value?.prop :
● работает как [Link] , если значение value существует,
●
в противном случае (когда value равно undefined/null ) он возвращает undefined .
Считывание адреса с помощью user?.address работает, даже если объект user не существует:
Обратите внимание: синтаксис ?. делает необязательным значение перед ним, но не какое-либо последующее.
В этом случае, если вдруг user окажется undefined , мы увидим программную ошибку по этому поводу и
исправим её. В противном случае, если слишком часто использовать ?. , ошибки могут замалчиваться там, где
это неуместно, и их будет сложнее отлаживать.
Переменная должна быть объявлена (к примеру, как let/const/var user или как параметр функции).
Опциональная цепочка работает только с объявленными переменными.
152/597
Сокращённое вычисление
Как было сказано ранее, ?. немедленно останавливает вычисление, если левая часть не существует.
Так что если после ?. есть какие-то вызовы функций или операции, то они не произойдут.
Например:
Опциональная цепочка ?. — это не оператор, а специальная синтаксическая конструкция, которая также работает с
функциями и квадратными скобками.
let userAdmin = {
admin() {
alert("Я админ");
}
};
[Link]?.(); // Я админ
Здесь в обеих строках мы сначала используем точку ( [Link] ), чтобы получить свойство admin , потому
что мы предполагаем, что объект userAdmin существует, так что читать из него безопасно.
Затем ?.() проверяет левую часть: если функция admin существует, то она запускается (это так для userAdmin ).
В противном случае (для userGuest ) вычисление остановится без ошибок.
Синтаксис ?.[] также работает, если мы хотим использовать скобки [] для доступа к свойствам вместо точки . .
Как и в предыдущих случаях, он позволяет безопасно считывать свойство из объекта, который может не
существовать.
let user1 = {
firstName: "John"
};
153/597
Мы можем использовать ?. для безопасного чтения и удаления, но не для записи
Опциональная цепочка ?. не имеет смысла в левой части присваивания.
Например:
Итого
Как мы видим, все они просты и понятны в использовании. ?. проверяет левую часть на null/undefined и
позволяет продолжить вычисление, если это не так.
По спецификации, в качестве ключей для свойств объекта могут использоваться только строки или символы. Ни
числа, ни логические значения не подходят, разрешены только эти два типа данных.
До сих пор мы видели только строки. Теперь давайте разберём символы, увидим, что хорошего они нам дают.
Символы
При создании, символу можно дать описание (также называемое имя), в основном использующееся для отладки кода:
Символы гарантированно уникальны. Даже если мы создадим множество символов с одинаковым описанием, это всё
равно будут разные символы. Описание – это просто метка, которая ни на что не влияет.
Например, вот два символа с одинаковым описанием – но они не равны:
Если вы знаете Ruby или какой-то другой язык программирования, в котором есть своего рода «символы» –
пожалуйста, будьте внимательны. Символы в JavaScript имеют свои особенности, и не стоит думать о них, как о
символах в Ruby или в других языках.
154/597
Символы не преобразуются автоматически в строки
Большинство типов данных в JavaScript могут быть неявно преобразованы в строку. Например, функция alert
принимает практически любое значение, автоматически преобразовывает его в строку, а затем выводит это
значение, не сообщая об ошибке. Символы же особенные и не преобразуются автоматически.
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
Это – языковая «защита» от путаницы, ведь строки и символы – принципиально разные типы данных и не должны
неконтролируемо преобразовываться друг в друга.
Если же мы действительно хотим вывести символ с помощью alert , то необходимо явно преобразовать его с
помощью метода .toString() , вот так:
let id = Symbol("id");
alert([Link]()); // Symbol(id), теперь работает
let id = Symbol("id");
alert([Link]); // id
«Скрытые» свойства
Символы позволяют создавать «скрытые» свойства объектов, к которым нельзя нечаянно обратиться и перезаписать
их из других частей программы.
Например, мы работаем с объектами user , которые принадлежат стороннему коду. Мы хотим добавить к ним
идентификаторы.
Используем для этого символьный ключ:
let user = {
name: "Вася"
};
let id = Symbol("id");
user[id] = 1;
// ...
let id = Symbol("id");
Конфликта между их и нашим идентификатором не будет, так как символы всегда уникальны, даже если их имена
совпадают.
155/597
А вот если бы мы использовали строку "id" вместо символа, то тогда был бы конфликт:
let id = Symbol("id");
let user = {
name: "Вася",
[id]: 123 // просто "id: 123" не сработает
};
Это вызвано тем, что нам нужно использовать значение переменной id в качестве ключа, а не строку «id».
let id = Symbol("id");
let user = {
name: "Вася",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (свойства с ключом-символом нет среди перечисленных)
Это – часть общего принципа «сокрытия символьных свойств». Если другая библиотека или скрипт будут работать с
нашим объектом, то при переборе они не получат ненароком наше символьное свойство. [Link](user)
также игнорирует символы.
А вот [Link] , в отличие от цикла for..in , копирует и строковые, и символьные свойства:
let id = Symbol("id");
let user = {
[id]: 123
};
Здесь нет никакого парадокса или противоречия. Так и задумано. Идея заключается в том, что, когда мы клонируем
или объединяем объекты, мы обычно хотим скопировать все свойства (включая такие свойства с ключами-
символами, как, например, id в примере выше).
156/597
Глобальные символы
Итак, как мы видели, обычно все символы уникальны, даже если их имена совпадают. Но иногда мы наоборот хотим,
чтобы символы с одинаковыми именами были одной сущностью. Например, разные части нашего приложения хотят
получить доступ к символу "id" , подразумевая именно одно и то же свойство.
Для этого существует глобальный реестр символов. Мы можем создавать в нём символы и обращаться к ним позже,
и при каждом обращении нам гарантированно будет возвращаться один и тот же символ.
Для чтения (или, при отсутствии, создания) символа из реестра используется вызов [Link](key) .
Он проверяет глобальный реестр и, при наличии в нём символа с именем key , возвращает его, иначе же создаётся
новый символ Symbol(key) и записывается в реестр под ключом key .
Например:
// читаем его снова и записываем в другую переменную (возможно, из другого места кода)
let idAgain = [Link]("id");
Символы, содержащиеся в реестре, называются глобальными символами. Если вам нужен символ, доступный везде
в коде – используйте глобальные символы.
Похоже на Ruby
В некоторых языках программирования, например, Ruby, на одно имя (описание) приходится один символ, и не
могут существовать разные символы с одинаковым именем.
В JavaScript, как мы видим, это утверждение верно только для глобальных символов.
[Link]
Для глобальных символов, кроме [Link](key) , который ищет символ по имени, существует обратный метод:
[Link](sym) , который, наоборот, принимает глобальный символ и возвращает его имя.
К примеру:
Внутри метода [Link] используется глобальный реестр символов для нахождения имени символа. Так что
этот метод не будет работать для неглобальных символов. Если символ неглобальный, метод не сможет его найти и
вернёт undefined .
Впрочем, для любых символов доступно свойство description .
Например:
157/597
Системные символы
Существует множество «системных» символов, использующихся внутри самого JavaScript, и мы можем использовать
их, чтобы настраивать различные аспекты поведения объектов.
В частности, [Link] позволяет описать правила для объекта, согласно которым он будет
преобразовываться к примитиву. Мы скоро увидим его применение.
С другими системными символами мы тоже скоро познакомимся, когда будем изучать соответствующие возможности
языка.
Итого
Символ (symbol) – примитивный тип данных, использующийся для создания уникальных идентификаторов.
Символы создаются вызовом функции Symbol() , в которую можно передать описание (имя) символа.
Даже если символы имеют одно и то же имя, это – разные символы. Если мы хотим, чтобы одноимённые символы
были равны, то следует использовать глобальный реестр: вызов [Link](key) возвращает (или создаёт)
глобальный символ с key в качестве имени. Многократные вызовы команды [Link] с одним и тем же
аргументом возвращают один и тот же символ.
Если мы хотим добавить свойство в объект, который «принадлежит» другому скрипту или библиотеке, мы можем
создать символ и использовать его в качестве ключа. Символьное свойство не появится в for..in , так что оно не
будет нечаянно обработано вместе с другими. Также оно не будет модифицировано прямым обращением, так как
другой скрипт не знает о нашем символе. Таким образом, свойство будет защищено от случайной перезаписи или
использования.
Так что, используя символьные свойства, мы можем спрятать что-то нужное нам, но что другие видеть не должны.
2. Существует множество системных символов, используемых внутри JavaScript, доступных как Symbol.* . Мы
можем использовать их, чтобы изменять встроенное поведение ряда объектов. Например, в дальнейших главах мы
будем использовать [Link] для итераторов, [Link] для настройки преобразования
объектов в примитивы и так далее.
Что произойдёт, если сложить два объекта obj1 + obj2 , вычесть один из другого obj1 - obj2 или вывести их на
экран, воспользовавшись alert(obj) ?
JavaScript совершенно не позволяет настраивать, как операторы работают с объектами. В отличие от некоторых
других языков программирования, таких как Ruby или C++, мы не можем реализовать специальный объектный метод
для обработки сложения (или других операторов).
В случае таких операций, объекты автоматически преобразуются в примитивы, затем выполняется сама операция
над этими примитивами, и на выходе мы получим примитивное значение.
Это важное ограничение: результатом obj1 + obj2 (или другой математической операции) не может быть другой
объект!
К примеру, мы не можем создавать объекты, представляющие векторы или матрицы (или достижения или может ещё
что-то), складывать их и ожидать в качестве результата «суммированный» объект. Такие архитектурные ходы
158/597
автоматически оказываются «за бортом».
Итак, поскольку мы технически здесь мало что можем сделать, в реальных проектах нет математики с объектами.
Если она всё же происходит, то за редким исключением, это из-за ошибок в коде.
В этой главе мы рассмотрим, как объект преобразуется в примитив и как это можно настроить.
У нас есть две цели:
1. Это позволит нам понять, что происходит в случае ошибок в коде, когда такая операция произошла случайно.
2. Есть исключения, когда такие операции возможны и вполне уместны. Например, вычитание или сравнение дат
( Date объекты). Мы встретимся с ними позже.
Правила преобразования
В главе Преобразование типов мы рассмотрели правила для числовых, строковых и логических преобразований
примитивов. Но мы оставили пробел для объектов. Теперь, когда мы уже знаем о методах и символах, пришло время
заполнить этот пробел.
1. Не существует преобразования к логическому значению. В логическом контексте все объекты являются true , всё
просто. Существует лишь их числовое и строковое преобразование.
2. Числовое преобразование происходит, когда мы вычитаем объекты или применяем математические функции.
Например, объекты Date (которые будут рассмотрены в главе Дата и время) могут быть вычтены, и результатом
date1 - date2 будет разница во времени между двумя датами.
3. Что касается преобразований к строке – оно обычно происходит, когда мы выводим на экран объект при помощи
alert(obj) и в подобных контекстах.
Мы можем реализовать свои преобразования к строкам и числам, используя специальные объектные методы.
Теперь давайте углубимся в детали. Это единственный путь для того, чтобы разобраться в нюансах этой темы.
Хинты
"string"
Для преобразования объекта к строке, когда мы выполняем операцию над объектом, которая ожидает строку,
например alert :
// вывод
alert(obj);
"number"
Для преобразования объекта к числу, в случае математических операций:
// явное преобразование
let num = Number(obj);
// сравнения больше/меньше
let greater = user1 > user2;
"default"
Происходит редко, когда оператор «не уверен», какой тип ожидать.
159/597
Например, бинарный плюс + может работать как со строками (объединяя их в одну), так и с числами (складывая их).
Поэтому, если бинарный плюс получает объект в качестве аргумента, он использует хинт "default" для его
преобразования.
Также, если объект сравнивается с помощью == со строкой, числом или символом, тоже неясно, какое
преобразование следует выполнить, поэтому используется хинт "default" .
Операторы сравнения больше/меньше, такие как < > , также могут работать как со строками, так и с числами. Тем не
менее, по историческим причинам, они используют хинт "number" , а не "default" .
Все встроенные объекты, за исключением одного (объект Date , который мы рассмотрим позже), реализуют
"default" преобразование тем же способом, что и "number" . И нам следует поступать так же.
Чтобы выполнить преобразование, JavaScript пытается найти и вызвать три следующих метода объекта:
[Link]
Давайте начнём с первого метода. Есть встроенный символ с именем [Link] , который следует
использовать для обозначения метода преобразования, вот так:
obj[[Link]] = function(hint) {
// вот код для преобразования этого объекта в примитив
// он должен вернуть примитивное значение
// hint = чему-то из "string", "number", "default"
};
Если метод [Link] существует, он используется для всех хинтов, и больше никаких методов не
требуется.
let user = {
name: "John",
money: 1000,
[[Link]](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${[Link]}"}` : [Link];
}
};
Как мы можем видеть из кода, user становится либо строкой со своим описанием, либо суммой денег в зависимости
от преобразования. Единый метод user[[Link]] обрабатывает все случаи преобразования.
160/597
toString/valueOf
Если нет [Link] , тогда JavaScript пытается найти методы toString и valueOf :
●
Для хинта "string" : вызвать метод toString , а если он не существует или возвращает объект вместо
примитивного значения, то valueOf (таким образом, toString имеет приоритет при строковом
преобразовании).
● Для других хинтов: вызвать метод valueOf , а если он не существует или возвращает объект вместо примитивного
значения, то toString (таким образом, valueOf имеет приоритет для математических операций).
Методы toString и valueOf берут своё начало с древних времён. Это не символы (символов тогда ещё не было),
а скорее просто «обычные» методы со строковыми именами. Они предоставляют альтернативный «старомодный»
способ реализации преобразования.
Эти методы должны возвращать примитивное значение. Если toString или valueOf возвращает объект, то он
игнорируется (так же, как если бы метода не было).
Взгляните на пример:
Таким образом, если мы попытаемся использовать объект в качестве строки, как например в alert или вроде того,
то по умолчанию мы увидим [object Object] .
Значение по умолчанию valueOf упоминается здесь только для полноты картины, чтобы избежать какой-либо
путаницы. Как вы можете видеть, он возвращает сам объект и поэтому игнорируется. Не спрашивайте меня почему,
это по историческим причинам. Так что мы можем предположить, что его не существует.
Давайте применим эти методы для настройки преобразования.
Для примера, используем их в реализации всё того же объекта user . Но уже используя комбинацию toString и
valueOf вместо [Link] :
let user = {
name: "John",
money: 1000,
};
let user = {
name: "John",
161/597
toString() {
return [Link];
}
};
Историческая справка
По историческим причинам, если toString или valueOf вернёт объект, то ошибки не будет, но такое значение
будет проигнорировано (как если бы метода вообще не существовало). Это всё потому, что в древние времена в
JavaScript не было хорошей концепции «ошибки».
А вот [Link] уже «четче», этот метод обязан возвращать примитив, иначе будет ошибка.
Дальнейшие преобразования
Как мы уже знаем, многие операторы и функции выполняют преобразования типов, например, умножение *
преобразует операнды в числа.
Если мы передаём объект в качестве аргумента, то в вычислениях будут две стадии:
1. Объект преобразуется в примитив (с использованием правил, описанных выше).
2. Если необходимо для дальнейших вычислений, этот примитив преобразуется дальше.
Например:
let obj = {
// toString обрабатывает все преобразования в случае отсутствия других методов
toString() {
return "2";
}
};
alert(obj * 2); // 4, объект был преобразован к примитиву "2", затем умножение сделало его числом
А вот, к примеру, бинарный плюс в подобной ситуации соединил бы строки, так как он совсем не брезгует строк:
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // "22" ("2" + 2), преобразование к примитиву вернуло строку => конкатенация
Итого
162/597
●
"string" (для alert и других операций, которым нужна строка)
● "number" (для математических операций)
● "default" (для некоторых других операторов, обычно объекты реализуют его как "number" )
Спецификация явно описывает для каждого оператора, какой ему следует использовать хинт.
Алгоритм преобразования таков:
Типы данных
Больше структур данных и более глубокое изучение типов.
Методы примитивов
JavaScript позволяет нам работать с примитивными типами данных – строками, числами и т.д., как будто они
являются объектами. У них есть и методы. Мы изучим их позже, а сначала разберём, как это всё работает, потому что,
конечно, примитивы – не объекты.
Давайте взглянем на ключевые различия между примитивами и объектами.
Примитив
● Это – значение «примитивного» типа.
●
Есть 7 примитивных типов: string , number , boolean , symbol , null , undefined и bigint .
Объект
● Может хранить множество значений как свойства.
●
Объявляется при помощи фигурных скобок {} , например: {name: "Рома", age: 30} . В JavaScript есть и
другие виды объектов: например, функции тоже являются объектами.
Одна из лучших особенностей объектов – это то, что мы можем хранить функцию как одно из свойств объекта.
let roma = {
name: "Рома",
sayHi: function() {
alert("Привет, дружище!");
}
};
Объекты «тяжелее» примитивов. Они нуждаются в дополнительных ресурсах для поддержания внутренней
структуры.
163/597
● Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно,
если бы мы могли обращаться к ним при помощи методов.
●
Примитивы должны быть лёгкими и быстрыми насколько это возможно.
Каждый примитив имеет свой собственный «объект-обёртку», которые называются: String , Number , Boolean ,
Symbol и BigInt . Таким образом, они имеют разный набор методов.
Очень просто, не правда ли? Вот, что на самом деле происходит в [Link]() :
1. Строка str – примитив. В момент обращения к его свойству, создаётся специальный объект, который знает
значение строки и имеет такие полезные методы, как toUpperCase() .
2. Этот метод запускается и возвращает новую строку (показывается в alert ).
3. Специальный объект удаляется, оставляя только примитив str .
Число имеет собственный набор методов. Например, toFixed(n) округляет число до n знаков после запятой.
164/597
Конструкторы String/Number/Boolean предназначены только для внутреннего пользования
Некоторые языки, такие как Java, позволяют явное создание «объектов-обёрток» для примитивов при помощи
такого синтаксиса как new Number(1) или new Boolean(false) .
В JavaScript, это тоже возможно по историческим причинам, но очень не рекомендуется. В некоторых местах
последствия могут быть катастрофическими.
Например:
Объекты в if всегда дают true , так что в нижеприведённом примере будет показан alert :
if (zero) {
// zero возвращает "true", так как является объектом
alert( "zero имеет «истинное» значение?!?" );
}
С другой стороны, использование функций String/Number/Boolean без оператора new – вполне разумно и
полезно. Они превращают значение в соответствующий примитивный тип: в строку, в число, в булевый тип.
К примеру, следующее вполне допустимо:
alert([Link]); // ошибка
Итого
● Все примитивы, кроме null и undefined , предоставляют множество полезных методов. Мы познакомимся с
ними поближе в следующих главах.
●
Формально эти методы работают с помощью временных объектов, но движки JavaScript внутренне очень хорошо
оптимизируют этот процесс, так что их вызов не требует много ресурсов.
Задачи
[Link] = 5;
alert([Link]);
165/597
К решению
Числа
В данной главе мы рассмотрим только первый тип чисел: числа типа number . Давайте глубже изучим, как с ними
работать в JavaScript.
Представьте, что нам надо записать число 1 миллиард. Самый очевидный путь:
Символ нижнего подчёркивания _ – это «синтаксический сахар », он делает число более читабельным. Движок
JavaScript попросту игнорирует _ между цифрами, поэтому в примере выше получается точно такой же миллиард,
как и в первом случае.
Однако в реальной жизни мы в основном стараемся не писать длинные последовательности нулей, так как можно
легко ошибиться. Укороченная запись может выглядеть как "1млрд" или "7.3млрд" для 7 миллиардов 300
миллионов. Такой принцип работает для всех больших чисел.
В JavaScript, чтобы укоротить запись числа, мы можем добавить к нему букву "e" и указать необходимое количество
нулей:
А сейчас давайте запишем что-нибудь очень маленькое. К примеру, 1 микросекунду (одна миллионная секунды):
В этом случае нам также поможет "e" . Если мы хотим избежать записи длинной последовательности из нулей, мы
можем сделать так:
Если мы подсчитаем количество нулей в 0.000001 , их будет 6. Естественно, верная запись 1e-6 .
Другими словами, отрицательное число после "e" подразумевает деление на 1 с указанным количеством нулей:
166/597
// 1 делится на 1 с 3 нулями
1e-3 === 1 / 1000 (=0.001)
Например:
Двоичные и восьмеричные числа используются не так часто, но они также поддерживаются: 0b для двоичных и 0o
для восьмеричных:
Есть только 3 системы счисления с такой поддержкой. Для других систем счисления мы рекомендуем использовать
функцию parseInt (рассмотрим позже в этой главе).
toString(base)
Метод [Link](base) возвращает строковое представление числа num в системе счисления base .
Например:
alert( [Link](16) ); // ff
alert( [Link](2) ); // 11111111
167/597
Округление
[Link]
Округление в меньшую сторону: 3.1 становится 3 , а -1.1 — -2 .
[Link]
Округление в большую сторону: 3.1 становится 4 , а -1.1 — -1 .
[Link]
Округление до ближайшего целого: 3.1 становится 3 , 3.6 — 4 , а -1.1 — -1 .
3.1 3 4 3 3
3.6 3 4 4 3
-1.1 -2 -1 -1 -1
-1.6 -2 -1 -2 -1
Эти функции охватывают все возможные способы обработки десятичной части. Что если нам надо округлить число
до n-ого количества цифр в дробной части?
Например, у нас есть 1.2345 и мы хотим округлить число до 2-х знаков после запятой, оставить только 1.23 .
Есть два пути решения:
1. Умножить и разделить.
Например, чтобы округлить число до второго знака после запятой, мы можем умножить число на 100 , вызвать
функцию округления и разделить обратно.
alert( [Link](num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
2. Метод toFixed(n) округляет число до n знаков после запятой и возвращает строковое представление
результата.
Округляет значение до ближайшего числа, как в большую, так и в меньшую сторону, аналогично методу
[Link] :
Обратите внимание, что результатом toFixed является строка. Если десятичная часть короче, чем необходима,
будут добавлены нули в конец строки:
168/597
Мы можем преобразовать полученное значение в число, используя унарный оператор + или Number() , пример с
унарным оператором: +[Link](5) .
Неточные вычисления
Внутри JavaScript число представлено в виде 64-битного формата IEEE-754 . Для хранения числа используется 64
бита: 52 из них используется для хранения цифр, 11 для хранения положения десятичной точки и один бит отведён на
хранение знака.
Если число слишком большое, оно переполнит 64-битное хранилище, JavaScript вернёт бесконечность:
Наиболее часто встречающаяся ошибка при работе с числами в JavaScript – это потеря точности.
Ой! Здесь гораздо больше последствий, чем просто некорректное сравнение. Представьте, вы делаете интернет-
магазин и посетители формируют заказ из 2-х позиций за $0.10 и $0.20 . Итоговый заказ будет
$0.30000000000000004 . Это будет сюрпризом для всех.
Но почему это происходит?
Число хранится в памяти в бинарной форме, как последовательность бит – единиц и нулей. Но дроби, такие как 0.1 ,
0.2 , которые выглядят довольно просто в десятичной системе счисления, на самом деле являются бесконечной
дробью в двоичной форме.
alert([Link](2)); // 0.0001100110011001100110011001100110011001100110011001101
alert([Link](2)); // 0.001100110011001100110011001100110011001100110011001101
alert((0.1 + 0.2).toString(2)); // 0.0100110011001100110011001100110011001100110011001101
Другими словами, что такое 0.1 ? Это единица делённая на десять — 1/10 , одна десятая. В десятичной системе
счисления такие числа легко представимы, по сравнению с одной третьей: 1/3 , которая становится бесконечной
дробью 0.33333(3) .
Деление на 10 гарантированно хорошо работает в десятичной системе, но деление на 3 – нет. По той же причине и
в двоичной системе счисления, деление на 2 обязательно сработает, а 1/10 становится бесконечной дробью.
В JavaScript нет возможности для хранения точных значений 0.1 или 0.2, используя двоичную систему, точно также,
как нет возможности хранить одну третью в десятичной системе счисления.
Числовой формат IEEE-754 решает эту проблему путём округления до ближайшего возможного числа. Правила
округления обычно не позволяют нам увидеть эту «крошечную потерю точности», но она существует.
Пример:
169/597
Не только в JavaScript
Справедливости ради заметим, что ошибка в точности вычислений для чисел с плавающей точкой сохраняется в
любом другом языке, где используется формат IEEE 754, включая PHP, Java, C, Perl и Ruby.
Можно ли обойти проблему? Конечно, наиболее надёжный способ — это округлить результат используя метод
toFixed(n) :
Помните, что метод toFixed всегда возвращает строку. Это гарантирует, что результат будет с заданным
количеством цифр в десятичной части. Также это удобно для форматирования цен в интернет-магазине $0.30 . В
других случаях можно использовать унарный оператор + , чтобы преобразовать строку в число:
Также можно временно умножить число на 100 (или на большее), чтобы привести его к целому, выполнить
математические действия, а после разделить обратно. Суммируя целые числа, мы уменьшаем погрешность, но она
всё равно появляется при финальном делении:
Забавный пример
Попробуйте выполнить его:
Причина та же – потеря точности. Из 64 бит, отведённых на число, сами цифры числа занимают до 52 бит,
остальные 11 бит хранят позицию десятичной точки и один бит – знак. Так что если 52 бит не хватает на цифры,
то при записи пропадут младшие разряды.
Интерпретатор не выдаст ошибку, но в результате получится «не совсем то число», что мы и видим в примере
выше. Как говорится: «как смог, так записал».
Два нуля
Другим забавным следствием внутреннего представления чисел является наличие двух нулей: 0 и -0 .
Все потому, что знак представлен отдельным битом, так что, любое число может быть положительным и
отрицательным, включая нуль.
В большинстве случаев это поведение незаметно, так как операторы в JavaScript воспринимают их одинаковыми.
170/597
Эти числовые значения принадлежат типу number , но они не являются «обычными» числами, поэтому есть функции
для их проверки:
●
isNaN(value) преобразует значение в число и проверяет является ли оно NaN :
Нужна ли нам эта функция? Разве не можем ли мы просто сравнить === NaN ? К сожалению, нет. Значение NaN
уникально тем, что оно не является равным ничему другому, даже самому себе:
● isFinite(value) преобразует аргумент в число и возвращает true , если оно является обычным числом, т.е.
не NaN/Infinity/-Infinity :
// вернёт true всегда, кроме ситуаций, когда аргумент - Infinity/-Infinity или не число
alert( isFinite(num) );
Помните, что пустая строка интерпретируется как 0 во всех числовых функциях, включая isFinite .
[Link] и [Link]
Методы [Link] и [Link] – это более «строгие» версии функций isNaN и isFinite . Они не
преобразуют аргумент в число, а наоборот – первым делом проверяют, является ли аргумент числом
(принадлежит ли он к типу number ).
●
[Link](value) возвращает true только в том случае, если аргумент принадлежит к типу number и
является NaN . Во всех остальных случаях возвращает false .
● [Link](value) возвращает true только в том случае, если аргумент принадлежит к типу
number и не является NaN/Infinity/-Infinity . Во всех остальных случаях возвращает false .
171/597
Сравнение [Link]
Существует специальный метод [Link] , который сравнивает значения примерно как === , но более надёжен
в двух особых ситуациях:
1. Работает с NaN : [Link](NaN, NaN) === true , здесь он хорош.
2. Значения 0 и -0 разные: [Link](0, -0) === false , это редко используется, но технически эти
значения разные.
parseInt и parseFloat
Для явного преобразования к числу можно использовать + или Number() . Если строка не является в точности
числом, то результат будет NaN :
В JavaScript встроен объект Math , который содержит различные математические функции и константы.
Несколько примеров:
[Link]()
Возвращает псевдослучайное число в диапазоне от 0 (включительно) до 1 (но не включая 1)
172/597
alert( [Link]() ); // 0.1234567894322
alert( [Link]() ); // 0.5435252343232
alert( [Link]() ); // ... (любое количество псевдослучайных чисел)
[Link](n, power)
Возвращает число n , возведённое в степень power
В объекте Math есть множество функций и констант, включая тригонометрические функции, подробнее можно
ознакомиться в документации по объекту Math .
Итого
Для дробей:
●
Используйте округления [Link] , [Link] , [Link] , [Link] или
[Link](precision) .
● Помните, что при работе с дробями происходит потеря точности.
Задачи
173/597
важность: 5
Создайте скрипт, который запрашивает ввод двух чисел (используйте prompt) и после показывает их сумму.
Запустить демо
К решению
Методы [Link] и toFixed , согласно документации, округляют до ближайшего целого числа: 0..4
округляется в меньшую сторону, тогда как 5..9 в большую сторону.
Например:
К решению
Создайте функцию readNumber , которая будет запрашивать ввод числового значения до тех пор, пока посетитель
его не введёт.
Также надо разрешить пользователю остановить процесс ввода, отправив пустую строку или нажав «Отмена». В этом
случае функция должна вернуть null .
Запустить демо
К решению
let i = 0;
while (i != 10) {
i += 0.2;
}
К решению
174/597
Напишите функцию random(min, max) , которая генерирует случайное число с плавающей точкой от min до max
(но не включая max ).
К решению
Напишите функцию randomInteger(min, max) , которая генерирует случайное целое (integer) число от min до
max (включительно).
alert( randomInteger(1, 5) ); // 1
alert( randomInteger(1, 5) ); // 3
alert( randomInteger(1, 5) ); // 5
К решению
Строки
В JavaScript любые текстовые данные являются строками. Не существует отдельного типа «символ», который есть в
ряде других языков.
Внутренний формат для строк — всегда UTF-16 , вне зависимости от кодировки страницы.
Кавычки
Одинарные и двойные кавычки работают, по сути, одинаково, а если использовать обратные кавычки, то в такую
строку мы сможем вставлять произвольные выражения, обернув их в ${…} :
function sum(a, b) {
return a + b;
}
Ещё одно преимущество обратных кавычек — они могут занимать более одной строки, вот так:
175/597
* Mary
`;
Выглядит вполне естественно, не правда ли? Что тут такого? Но если попытаться использовать точно так же
одинарные или двойные кавычки, то будет ошибка:
Одинарные и двойные кавычки в языке с незапамятных времён: тогда потребность в многострочных строках не
учитывалась. Что касается обратных кавычек, они появились существенно позже, и поэтому они гибче.
Обратные кавычки также позволяют задавать «шаблонную функцию» перед первой обратной кавычкой.
Используемый синтаксис: func`string` . Автоматически вызываемая функция func получает строку и встроенные
в неё выражения и может их обработать. Подробнее об этом можно прочитать в документации . Если перед
строкой есть выражение, то шаблонная строка называется «теговым шаблоном». Это позволяет использовать свою
шаблонизацию для строк, но на практике теговые шаблоны применяются редко.
Спецсимволы
Многострочные строки также можно создавать с помощью одинарных и двойных кавычек, используя так называемый
«символ перевода строки», который записывается как \n :
Символ Описание
\n Перевод строки
В текстовых файлах Windows для перевода строки используется комбинация символов \r\n , а на других ОС это просто \n . Это так по
\r
историческим причинам, ПО под Windows обычно понимает и просто \n .
\\ Обратный слеш
\t Знак табуляции
\b , \f , \v Backspace, Form Feed и Vertical Tab — оставлены для обратной совместимости, сейчас не используются.
Как вы можете видеть, все спецсимволы начинаются с обратного слеша, \ — так называемого «символа
экранирования».
Он также используется, если необходимо вставить в строку кавычку.
К примеру:
176/597
Здесь перед входящей в строку кавычкой необходимо добавить обратный слеш — \' — иначе она бы обозначала
окончание строки.
Разумеется, требование экранировать относится только к таким же кавычкам, как те, в которые заключена строка. Так
что мы можем применить и более элегантное решение, использовав для этой строки двойные или обратные кавычки:
Заметим, что обратный слеш \ служит лишь для корректного прочтения строки интерпретатором, но он не
записывается в строку после её прочтения. Когда строка сохраняется в оперативную память, в неё не добавляется
символ \ . Вы можете явно видеть это в выводах alert в примерах выше.
Но что, если нам надо добавить в строку собственно сам обратный слеш \ ?
Это можно сделать, добавив перед ним… ещё один обратный слеш!
Длина строки
alert( `My\n`.length ); // 3
Обратите внимание, \n — это один спецсимвол, поэтому тут всё правильно: длина строки 3 .
Доступ к символам
Получить символ, который занимает позицию pos , можно с помощью квадратных скобок: [pos] . Также можно
использовать метод [Link](pos) . Первый символ занимает нулевую позицию:
Как вы можете видеть, преимущество метода .at(pos) заключается в том, что он допускает отрицательную
позицию. Если pos – отрицательное число, то отсчет ведется от конца строки.
Таким образом, .at(-1) означает последний символ, а .at(-2) – тот, что перед ним, и т.д.
Квадратные скобки всегда возвращают undefined для отрицательных индексов. Например:
177/597
for (let char of "Hello") {
alert(char); // H,e,l,l,o (char — сначала "H", потом "e", потом "l" и т.д.)
}
Строки неизменяемы
Содержимое строки в JavaScript нельзя изменить. Нельзя взять символ посередине и заменить его. Как только строка
создана — она такая навсегда.
alert( str ); // hi
Изменение регистра
Поиск подстроки
[Link]
Первый метод — [Link](substr, pos) .
Он ищет подстроку substr в строке str , начиная с позиции pos , и возвращает позицию, на которой располагается
совпадение, либо -1 при отсутствии совпадений.
Например:
178/597
let str = 'Widget with id';
alert( [Link]('id', 2) ) // 12
Чтобы найти все вхождения подстроки, нужно запустить indexOf в цикле. Каждый раз, получив очередную позицию,
начинаем новый поиск со следующей:
let pos = 0;
while (true) {
let foundPos = [Link](target, pos);
if (foundPos == -1) break;
[Link](substr, position)
Также есть похожий метод [Link](substr, position) , который ищет с конца строки к её началу.
Он используется тогда, когда нужно получить самое последнее вхождение: перед концом строки или
начинающееся до (включительно) определённой позиции.
При проверке indexOf в условии if есть небольшое неудобство. Такое условие не будет работать:
if ([Link]("Widget")) {
alert("Совпадение есть"); // не работает
}
Мы ищем подстроку "Widget" , и она здесь есть, прямо на позиции 0 . Но alert не показывается, т. к.
[Link]("Widget") возвращает 0 , и if решает, что тест не пройден.
Поэтому надо делать проверку на -1 :
if ([Link]("Widget") != -1) {
alert("Совпадение есть"); // теперь работает
}
Трюк с побитовым НЕ
179/597
alert( ~2 ); // -3, то же, что -(2+1)
alert( ~1 ); // -2, то же, что -(1+1)
alert( ~0 ); // -1, то же, что -(0+1)
alert( ~-1 ); // 0, то же, что -(-1+1)
Таким образом, ~n равняется 0 только при n == -1 (для любого n , входящего в 32-разрядные целые числа со
знаком).
if (~[Link]("Widget")) {
alert( 'Совпадение есть' ); // работает
}
Обычно использовать возможности языка каким-либо неочевидным образом не рекомендуется, но этот трюк широко
используется в старом коде, поэтому его важно понимать.
Просто запомните: if (~[Link](…)) означает «если найдено».
Впрочем, если быть точнее, из-за того, что большие числа обрезаются до 32 битов оператором ~ , существуют другие
числа, для которых результат тоже будет 0 , самое маленькое из которых — ~4294967295=0 . Поэтому такая
проверка будет правильно работать только для строк меньшей длины.
На данный момент такой трюк можно встретить только в старом коде, потому что в новом он просто не нужен: есть
метод .includes (см. ниже).
Получение подстроки
[Link](start [, end])
Возвращает часть строки от start до (не включая) end .
Например:
180/597
// 's', от 0 до 1, не включая 1, т. е. только один символ на позиции 0
alert( [Link](0, 1) );
Также для start/end можно задавать отрицательные значения. Это означает, что позиция определена как заданное
количество символов с конца строки:
[Link](start [, end])
Возвращает часть строки между start и end (не включая) end .
Это — почти то же, что и slice , но можно задавать start больше end .
Если start больше end , то метод substring сработает так, как если бы аргументы были поменяны местами.
Например:
[Link](start [, length])
Возвращает часть строки от start длины length .
В противоположность предыдущим методам, этот позволяет указать длину вместо конечной позиции:
Значение первого аргумента может быть отрицательным, тогда позиция определяется с конца:
Этот метод находится в Annex B спецификации языка. Это означает, что его должны поддерживать только
браузерные движки JavaScript, и использовать его не рекомендуется. Но на практике он поддерживается везде.
slice(start, end) от start до end (не включая end ) можно передавать отрицательные значения
substring(start, end) между start и end (не включая end ) отрицательные значения равнозначны 0
substr(start, length) length символов, начиная от start значение start может быть отрицательным
181/597
Какой метод выбрать?
Все эти методы эффективно выполняют задачу. Формально у метода substr есть небольшой недостаток: он
описан не в собственно спецификации JavaScript, а в приложении к ней — Annex B. Это приложение описывает
возможности языка для использования в браузерах, существующие в основном по историческим причинам. Таким
образом, в другом окружении, отличном от браузера, он может не поддерживаться. Однако на практике он
работает везде.
Из двух других вариантов, slice более гибок, он поддерживает отрицательные аргументы, и его короче писать.
Так что, в принципе, можно запомнить только его.
Сравнение строк
Как мы знаем из главы Операторы сравнения, строки сравниваются посимвольно в алфавитном порядке.
Тем не менее, есть некоторые нюансы.
1. Строчные буквы больше заглавных:
Это может привести к своеобразным результатам при сортировке названий стран: нормально было бы ожидать, что
Zealand будет после Österreich в списке.
Чтобы разобраться, что происходит, давайте ознакомимся с внутренним представлением строк в JavaScript.
Строки кодируются в UTF-16 . Таким образом, у любого символа есть соответствующий код. Есть специальные
методы, позволяющие получить символ по его коду и наоборот.
[Link](pos)
Возвращает код для символа, находящегося на позиции pos :
[Link](code)
Создаёт символ по его коду code
alert( [Link](90) ); // Z
Давайте сделаем строку, содержащую символы с кодами от 65 до 220 — это латиница и ещё некоторые
распространённые символы:
Как видите, сначала идут заглавные буквы, затем несколько спецсимволов, затем строчные и Ö ближе к концу
вывода.
Теперь очевидно, почему a > Z .
182/597
Символы сравниваются по их кодам. Больший код — больший символ. Код a (97) больше кода Z (90).
● Все строчные буквы идут после заглавных, так как их коды больше.
●
Некоторые буквы, такие как Ö , вообще находятся вне основного алфавита. У этой буквы код больше, чем у любой
буквы от a до z .
Правильное сравнение
«Правильный» алгоритм сравнения строк сложнее, чем может показаться, так как разные языки используют разные
алфавиты.
Поэтому браузеру нужно знать, какой язык использовать для сравнения.
К счастью, все современные браузеры (для IE10− нужна дополнительная библиотека [Link] ) поддерживают
стандарт ECMA 402 , обеспечивающий правильное сравнение строк на разных языках с учётом их правил.
Например:
alert( 'Österreich'.localeCompare('Zealand') ); // -1
У этого метода есть два дополнительных аргумента, которые указаны в документации . Первый позволяет указать
язык (по умолчанию берётся из окружения) — от него зависит порядок букв. Второй — определить дополнительные
правила, такие как чувствительность к регистру, а также следует ли учитывать различия между "a" и "á" .
Итого
● Есть три типа кавычек. Строки, использующие обратные кавычки, могут занимать более одной строки в коде и
включать выражения ${…} .
● Строки в JavaScript кодируются в UTF-16.
● Есть специальные символы, такие как разрыв строки \n .
● Для получения символа используйте [] или метод at .
● Для получения подстроки используйте slice или substring .
● Для того, чтобы перевести строку в нижний или верхний регистр, используйте toLowerCase/toUpperCase .
●
Для поиска подстроки используйте indexOf или includes/startsWith/endsWith , когда надо только
проверить, есть ли вхождение.
● Чтобы сравнить строки с учётом правил языка, используйте localeCompare .
Для строк предусмотрены методы для поиска и замены с использованием регулярных выражений. Но это отдельная
большая тема, поэтому ей посвящена отдельная глава учебника Регулярные выражения.
Также, на данный момент важно знать, что строки основаны на кодировке Юникод, и поэтому иногда могут возникать
проблемы со сравнениями. Подробнее о Юникоде в главе Юникод, внутреннее устройство строк.
Задачи
Напишите функцию ucFirst(str) , возвращающую строку str с заглавным первым символом. Например:
183/597
ucFirst("вася") == "Вася";
К решению
Проверка на спам
важность: 5
Напишите функцию checkSpam(str) , возвращающую true , если str содержит 'viagra' или 'XXX' , а иначе
false .
К решению
Усечение строки
важность: 5
Создайте функцию truncate(str, maxlength) , которая проверяет длину строки str и, если она превосходит
maxlength , заменяет конец str на "…" , так, чтобы её длина стала равна maxlength .
Результатом функции должна быть та же строка, если усечение не требуется, либо, если необходимо, усечённая
строка.
Например:
truncate("Вот, что мне хотелось бы сказать на эту тему:", 20) = "Вот, что мне хотело…"
К решению
Выделить число
важность: 4
Есть стоимость в виде строки "$120" . То есть сначала идёт знак валюты, а затем – число.
Создайте функцию extractCurrencyValue(str) , которая будет из такой строки выделять числовое значение и
возвращать его.
Например:
К решению
184/597
Массивы
Объявление
Практически всегда используется второй вариант синтаксиса. В скобках мы можем указать начальные значения
элементов:
alert( [Link] ); // 3
185/597
// разные типы значений
let arr = [ 'Яблоко', { name: 'Джон' }, true, function() { alert('привет'); } ];
Висячая запятая
Список элементов массива, как и список свойств объекта, может оканчиваться запятой:
let fruits = [
"Яблоко",
"Апельсин",
"Слива",
];
«Висячая запятая» упрощает процесс добавления/удаления элементов, так как все строки становятся
идентичными.
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Немного громоздко, не так ли? Нам нужно дважды написать имя переменной.
К счастью, есть более короткий синтаксис: [Link](-1) :
Очередь – один из самых распространённых вариантов применения массива. В области компьютерных наук так
называется упорядоченная коллекция элементов, поддерживающая два вида операций:
● push добавляет элемент в конец.
● shift удаляет элемент в начале, сдвигая очередь, так что второй элемент становится первым.
186/597
shift push
Существует и другой вариант применения для массивов – структура данных, называемая стек .
Примером стека обычно служит колода карт: новые карты кладутся наверх и берутся тоже сверху:
push
pop
Массивы в JavaScript могут работать и как очередь, и как стек. Мы можем добавлять/удалять элементы как в начало,
так и в конец массива.
В компьютерных науках структура данных, делающая это возможным, называется двусторонняя очередь .
pop
Удаляет последний элемент из массива и возвращает его:
push
Добавляет элемент в конец массива:
[Link]("Груша");
shift
Удаляет из массива первый элемент и возвращает его:
187/597
alert( [Link]() ); // удаляем Яблоко и выводим его
unshift
Добавляет элемент в начало массива:
[Link]('Яблоко');
[Link]("Апельсин", "Груша");
[Link]("Ананас", "Лимон");
Массив – это особый подвид объектов. Квадратные скобки, используемые для того, чтобы получить доступ к свойству
arr[0] – это по сути обычный синтаксис доступа по ключу, как obj[key] , где в роли obj у нас arr , а в качестве
ключа – числовой индекс.
Массивы расширяют объекты, так как предусматривают специальные методы для работы с упорядоченными
коллекциями данных, а также свойство length . Но в основе всё равно лежит объект.
Следует помнить, что в JavaScript существует 8 основных типов данных. Массив является объектом и,
следовательно, ведёт себя как объект.
let arr = fruits; // копируется по ссылке (две переменные ссылаются на один и тот же массив)
…Но то, что действительно делает массивы особенными – это их внутреннее представление. Движок JavaScript
старается хранить элементы массива в непрерывной области памяти, один за другим, так, как это показано на
иллюстрациях к этой главе. Существуют и другие способы оптимизации, благодаря которым массивы работают очень
быстро.
Но все они утратят эффективность, если мы перестанем работать с массивом как с «упорядоченной коллекцией
данных» и начнём использовать его как обычный объект.
Например, технически мы можем сделать следующее:
Это возможно, потому что в основе массива лежит объект. Мы можем присвоить ему любые свойства.
188/597
Но движок поймёт, что мы работаем с массивом, как с обычным объектом. Способы оптимизации, используемые для
массивов, в этом случае не подходят, поэтому они будут отключены и никакой выгоды не принесут.
Массив следует считать особой структурой, позволяющей работать с упорядоченными данными. Для этого массивы
предоставляют специальные методы. Массивы тщательно настроены в движках JavaScript для работы с
однотипными упорядоченными данными, поэтому, пожалуйста, используйте их именно в таких случаях. Если вам
нужны произвольные ключи, вполне возможно, лучше подойдёт обычный объект {} .
Эффективность
unshift pop
0 1 2 3
shift push
Почему работать с концом массива быстрее, чем с его началом? Давайте посмотрим, что происходит во время
выполнения:
Просто взять и удалить элемент с номером 0 недостаточно. Нужно также заново пронумеровать остальные
элементы.
Операция shift должна выполнить 3 действия:
"Orange"
"Orange"
"Lemon"
"Lemon"
"Lemon"
"Apple"
"Pear"
"Pear"
"Pear"
length = 4 length = 3
очистить передвинуть
0 11 2 3 1 2 3 0 1 2
элементы
влево
Чем больше элементов содержит массив, тем больше времени потребуется для того, чтобы их переместить,
больше операций с памятью.
То же самое происходит с unshift : чтобы добавить элемент в начало массива, нам нужно сначала сдвинуть
существующие элементы вправо, увеличивая их индексы.
А что же с push/pop ? Им не нужно ничего перемещать. Чтобы удалить элемент в конце массива, метод pop
очищает индекс и уменьшает значение length .
Действия при операции pop :
189/597
"Orange"
"Orange"
"Lemon"
"Apple"
"Apple"
"Pear"
"Pear"
length = 4 length = 3
очистить
0 1 2 3 0 1 2
Метод pop не требует перемещения, потому что остальные элементы остаются с теми же индексами. Именно
поэтому он выполняется очень быстро.
Аналогично работает метод push .
Перебор элементов
Одним из самых старых способов перебора элементов массива является цикл for по цифровым индексам:
// проходит по значениям
for (let fruit of fruits) {
alert( fruit );
}
Цикл for..of не предоставляет доступа к номеру текущего элемента, только к его значению, но в большинстве
случаев этого достаточно. А также это короче.
Технически, так как массив является объектом, можно использовать и вариант for..in :
Но на самом деле это – плохая идея. Существуют скрытые недостатки этого способа:
1. Цикл for..in выполняет перебор всех свойств объекта, а не только цифровых.
В браузере и других программных средах также существуют так называемые «псевдомассивы» – объекты, которые
выглядят, как массив. То есть, у них есть свойство length и индексы, но они также могут иметь дополнительные
нечисловые свойства и методы, которые нам обычно не нужны. Тем не менее, цикл for..in выведет и их.
Поэтому, если нам приходится иметь дело с объектами, похожими на массив, такие «лишние» свойства могут стать
проблемой.
2. Цикл for..in оптимизирован под произвольные объекты, не массивы, и поэтому в 10-100 раз медленнее.
Увеличение скорости выполнения может иметь значение только при возникновении узких мест. Но мы всё же
должны представлять разницу.
Немного о «length»
Свойство length автоматически обновляется при изменении массива. Если быть точными, это не количество
элементов массива, а наибольший цифровой индекс плюс один.
Например, единственный элемент, имеющий большой индекс, даёт большую длину:
190/597
let fruits = [];
fruits[123] = "Яблоко";
Если мы вручную увеличим его, ничего интересного не произойдёт. Зато, если мы уменьшим его, массив станет
короче. Этот процесс необратим, как мы можем понять из примера:
new Array()
Он редко применяется, так как квадратные скобки [] короче. Кроме того, у него есть хитрая особенность.
Если new Array вызывается с одним аргументом, который представляет собой число, он создаёт массив без
элементов, но с заданной длиной.
Как мы видим, в коде, представленном выше, в new Array(number) все элементы равны undefined .
Чтобы избежать появления таких неожиданных ситуаций, мы обычно используем квадратные скобки, если, конечно,
не знаем точно, что по какой-то причине нужен именно Array .
Многомерные массивы
Массивы могут содержать элементы, которые тоже являются массивами. Это можно использовать для создания
многомерных массивов, например, для хранения матриц:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
toString
Массивы по-своему реализуют метод toString , который возвращает список элементов, разделённых запятыми.
Например:
191/597
let arr = [1, 2, 3];
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"
В JavaScript, в отличие от некоторых других языков программирования, массивы не следует сравнивать при помощи
оператора == .
У этого оператора нет специального подхода к массивам, он работает с ними, как и с любыми другими объектами.
Оператор строгого равенства === ещё проще, так как он не преобразует типы.
Итак, если мы всё же сравниваем массивы с помощью == , то они никогда не будут одинаковыми, если только мы не
сравним две переменные, которые ссылаются на один и тот же массив
Например:
alert( [] == [] ); // false
alert( [0] == [0] ); // false
Технически эти массивы являются разными объектами. Так что они не равны. Оператор == не выполняет
поэлементное сравнение.
alert( 0 == [] ); // true
alert('0' == [] ); // false
Здесь, в обоих случаях, мы сравниваем примитив с объектом массива. Таким образом, массив [] преобразуется в
примитив с целью сравнения и становится пустой строкой '' .
Затем продолжается процесс сравнения с примитивами, как описано в главе Преобразование типов:
192/597
Так как же сравнить массивы?
Это просто: не используйте оператор == . Вместо этого сравните их по элементам в цикле или используя методы
итерации, описанные в следующей главе.
Итого
Массив – это особый тип объекта, предназначенный для работы с упорядоченным набором элементов.
Объявление:
Получение элементов:
● Мы можем получить элемент по его индексу, например arr[0] .
● Также мы можем использовать метод at(i) для получения элементов с отрицательным индексом, для
отрицательных значений i , он отступает от конца массива. В остальном он работает так же, как arr[i] , если i
>= 0 .
Мы вернёмся к массивам и изучим другие методы добавления, удаления, выделения элементов и сортировки
массивов в главе: Методы массивов.
Задачи
Скопирован ли массив?
важность: 3
// что в fruits?
alert( [Link] ); // ?
К решению
193/597
Операции с массивами
важность: 5
Джаз, Блюз
Джаз, Блюз, Рок-н-ролл
Джаз, Классика, Рок-н-ролл
Классика, Рок-н-ролл
Рэп, Регги, Классика, Рок-н-ролл
К решению
[Link](function() {
alert( this );
});
arr[2](); // ?
К решению
P.S. Ноль 0 – считается числом, не останавливайте ввод значений при вводе «0».
Запустить демо
К решению
194/597
Например:
Если все элементы отрицательные – ничего не берём(подмассив пустой) и сумма равна «0»:
К решению
Методы массивов
Массивы предоставляют множество методов. Чтобы было проще, в этой главе они разбиты на группы.
Добавление/удаление элементов
Мы уже знаем методы, которые добавляют и удаляют элементы из начала или конца:
●
[Link](...items) – добавляет элементы в конец,
●
[Link]() – извлекает элемент из конца,
● [Link]() – извлекает элемент из начала,
● [Link](...items) – добавляет элементы в начало.
Есть и другие.
splice
Как удалить элемент из массива?
Так как массивы – это объекты, то можно попробовать delete :
Элемент был удалён, но в массиве всё ещё три элемента, мы можем увидеть, что [Link] == 3 .
Это естественно, потому что delete [Link] удаляет значение по ключу key . Это всё, что он делает. Хорошо для
объектов. Но для массивов мы обычно хотим, чтобы оставшиеся элементы сдвинулись и заняли освободившееся
место. Мы ждём, что массив станет короче.
Поэтому нужно использовать специальные методы.
Метод [Link] – это универсальный «швейцарский нож» для работы с массивами. Умеет всё: добавлять, удалять
и заменять элементы.
Синтаксис:
195/597
Он изменяет arr начиная с индекса start : удаляет deleteCount элементов и затем вставляет elem1, ...,
elemN на их место. Возвращает массив из удалённых элементов.
Этот метод легко понять, рассмотрев примеры.
Начнём с удаления:
Метод splice также может вставлять элементы без удаления, для этого достаточно установить deleteCount в 0 :
// с индекса 2
// удалить 0 элементов
// вставить "сложный", "язык"
[Link](2, 0, "сложный", "язык");
slice
Метод [Link] намного проще, чем похожий на него [Link] .
Синтаксис:
196/597
[Link]([start], [end])
Он возвращает новый массив, в который копирует все элементы с индекса start до end (не включая end ). start
и end могут быть отрицательными, в этом случае отсчёт позиции будет вестись с конца массива.
Это похоже на строковый метод [Link] , но вместо подстрок возвращает подмассивы.
Например:
Можно вызвать slice без аргументов: [Link]() создаёт копию arr . Это часто используют, чтобы создать
копию массива для дальнейших преобразований, которые не должны менять исходный массив.
concat
Метод [Link] создаёт новый массив, в который копирует данные из других массивов и дополнительные
значения.
Синтаксис:
[Link](arg1, arg2...)
Он принимает любое количество аргументов, которые могут быть как массивами, так и простыми значениями.
В результате – новый массив, включающий в себя элементы из arr , затем arg1 , arg2 и так далее.
Если аргумент argN – массив, то копируются все его элементы. Иначе копируется сам аргумент.
Например:
Обычно он копирует только элементы из массивов. Другие объекты, даже если они выглядят как массивы,
добавляются как есть:
let arrayLike = {
0: "что-то",
length: 1
};
let arrayLike = {
0: "что-то",
1: "ещё",
[[Link]]: true,
197/597
length: 2
};
Перебор: forEach
Синтаксис:
Поиск в массиве
indexOf/lastIndexOf и includes
У методов [Link] и [Link] одинаковый синтаксис и они делают по сути то же самое, что и их строковые
аналоги, но работают с элементами вместо символов:
●
[Link](item, from) ищет item начиная с индекса from и возвращает номер индекса, на котором был
найден искомый элемент, в противном случае -1 .
● [Link](item, from) ищет item начиная с индекса from и возвращает true , если поиск успешен.
Обычно эти методы используются только с одним аргументом: искомым item . По умолчанию поиск ведется с
начала.
Например:
alert( [Link](0) ); // 1
alert( [Link](false) ); // 2
alert( [Link](null) ); // -1
Пожалуйста, обратите внимание, что методы используют строгое сравнение === . Таким образом, если мы ищем
false , он находит именно false , а не ноль.
Если мы хотим проверить наличие элемента в массиве и нет необходимости знать его индекс, предпочтительно
использовать [Link] .
Метод [Link] похож на indexOf , но ищет справа налево.
198/597
alert( [Link]('Яблоко') ); // 0 (первый 'Яблоко')
alert( [Link]('Яблоко') ); // 2 (последний 'Яблоко')
Это связано с тем, что includes был добавлен в JavaScript гораздо позже и использует более современный
алгоритм сравнения.
find и findIndex/findLastIndex
Представьте, что у нас есть массив объектов. Как нам найти объект с определённым условием?
Синтаксис:
Если функция возвращает true , поиск прерывается и возвращается item . Если ничего не найдено, возвращается
undefined .
Например, у нас есть массив пользователей, каждый из которых имеет поля id и name . Найдем пользователя с id
== 1 :
let users = [
{id: 1, name: "Вася"},
{id: 2, name: "Петя"},
{id: 3, name: "Маша"}
];
alert([Link]); // Вася
В реальной жизни массивы объектов – обычное дело, поэтому метод find крайне полезен.
Обратите внимание, что в данном примере мы передаём find функцию item => [Link] == 1 с одним
аргументом. Это типично, другие аргументы этой функции используются редко.
У метода [Link] такой же синтаксис, но он возвращает индекс, на котором был найден элемент, а не сам
элемент. Значение -1 возвращается, если ничего не найдено.
Метод [Link] похож на findIndex , но ищет справа налево, наподобие lastIndexOf .
Например:
let users = [
{id: 1, name: "Вася"},
{id: 2, name: "Петя"},
{id: 3, name: "Маша"},
{id: 4, name: "Вася"}
199/597
];
filter
Метод find ищет один (первый) элемент, который заставит функцию вернуть true .
Если найденных элементов может быть много, можно использовать [Link](fn) .
Например:
let users = [
{id: 1, name: "Вася"},
{id: 2, name: "Петя"},
{id: 3, name: "Маша"}
];
alert([Link]); // 2
Преобразование массива
map
Метод [Link] является одним из наиболее полезных и часто используемых.
Он вызывает функцию для каждого элемента массива и возвращает массив результатов выполнения этой функции.
Синтаксис:
sort(fn)
Вызов [Link]() сортирует массив на месте, меняя в нём порядок элементов.
Он также возвращает отсортированный массив, но обычно возвращаемое значение игнорируется, так как изменяется
сам arr .
Например:
let arr = [ 1, 2, 15 ];
200/597
alert( arr ); // 1, 15, 2
function compare(a, b) {
if (a > b) return 1; // если первое значение больше второго
if (a == b) return 0; // если равны
if (a < b) return -1; // если первое значение меньше второго
}
function compareNumeric(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
let arr = [ 1, 2, 15 ];
[Link](compareNumeric);
alert(arr); // 1, 2, 15
Сделаем отступление и подумаем, что происходит. arr может быть массивом чего угодно, верно? Он может
содержать числа, строки, объекты или что-то ещё. У нас есть набор каких-то элементов. Чтобы отсортировать его,
нам нужна упорядочивающая функция, которая знает, как сравнивать его элементы. По умолчанию элементы
сортируются как строки.
Метод [Link](fn) реализует общий алгоритм сортировки. Нам не нужно заботиться о том, как он работает
внутри (в большинстве случаев это оптимизированная быстрая сортировка или Timsort ). Она проходится по
массиву, сравнивает его элементы с помощью предоставленной функции и переупорядочивает их. Всё, что нам
нужно, – предоставить fn , которая делает сравнение.
Кстати, если мы когда-нибудь захотим узнать, какие элементы сравниваются – ничто не мешает нам вывести их на
экран:
В процессе работы алгоритм может сравнивать элемент со множеством других, но он старается сделать как можно
меньше сравнений.
201/597
Функция сравнения может вернуть любое число
На самом деле от функции сравнения требуется любое положительное число, чтобы сказать «больше», и
отрицательное число, чтобы сказать «меньше».
Это позволяет писать более короткие функции:
let arr = [ 1, 2, 15 ];
alert(arr); // 1, 2, 15
Будет работать точно так же, как и более длинная версия выше.
alert( [Link]( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (неправильно)
reverse
Метод [Link] меняет порядок элементов в arr на обратный.
Например:
split и join
Ситуация из реальной жизни. Мы пишем приложение для обмена сообщениями, и посетитель вводит имена тех, кому
его отправить, через запятую: Вася, Петя, Маша . Но нам-то гораздо удобнее работать с массивом имён, чем с
одной строкой. Как его получить?
Метод [Link](delim) именно это и делает. Он разбивает строку на массив по заданному разделителю delim .
202/597
alert( `Сообщение получат: ${name}.` ); // Сообщение получат: Вася (и другие имена)
}
У метода split есть необязательный второй числовой аргумент – ограничение на количество элементов в массиве.
Если их больше, чем указано, то остаток массива будет отброшен. На практике это редко используется:
Разбивка по буквам
Вызов split(s) с пустым аргументом s разбил бы строку на массив букв:
Вызов [Link](glue) делает в точности противоположное split . Он создаёт строку из элементов arr , вставляя
glue между ними.
Например:
reduce/reduceRight
Когда нам нужно перебрать массив – мы можем использовать forEach , for или for..of .
Когда нам нужно перебрать массив и вернуть данные для каждого элемента – мы можем использовать map .
Методы [Link] и [Link] похожи на методы выше, но они немного сложнее. Они используются для
вычисления единого значения на основе всего массива.
Синтаксис:
Функция применяется по очереди ко всем элементам массива и «переносит» свой результат на следующий вызов.
Аргументы:
● accumulator – результат предыдущего вызова этой функции, равен initial при первом вызове (если передан
initial ),
● item – очередной элемент массива,
● index – его позиция,
● array – сам массив.
При вызове функции результат её предыдущего вызова передаётся на следующий вызов в качестве первого
аргумента.
Так, первый аргумент является по сути аккумулятором, который хранит объединённый результат всех предыдущих
вызовов функции. По окончании он становится результатом reduce .
Звучит сложно?
203/597
let arr = [1, 2, 3, 4, 5];
alert(result); // 15
Функция, переданная в reduce , использует только два аргумента, этого обычно достаточно.
Разберём детально как это работает.
1. При первом запуске sum равен initial (последний аргумент reduce ), то есть 0 , а current – первый
элемент массива, равный 1 . Таким образом, результат функции равен 1 .
2. При втором запуске sum = 1 , к нему мы добавляем второй элемент массива ( 2 ) и возвращаем.
3. При третьем запуске sum = 3 , к которому мы добавляем следующий элемент, и так далее…
1 2 3 4 5 0+1+2+3+4+5 = 15
Или в виде таблицы, где каждая строка показывает вызов функции на очередном элементе массива:
первый вызов 0 1 1
второй вызов 1 2 3
третий вызов 3 3 6
четвёртый вызов 6 4 10
пятый вызов 10 5 15
Здесь отчётливо видно, как результат предыдущего вызова передаётся в первый аргумент следующего.
alert( result ); // 15
Результат – точно такой же! Это потому, что при отсутствии initial в качестве первого значения берётся первый
элемент массива, а перебор стартует со второго.
Таблица вычислений будет такая же за вычетом первой строки.
Но такое использование требует крайней осторожности. Если массив пуст, то вызов reduce без начального значения
выдаст ошибку.
Вот пример:
204/597
[Link]
…Но массивы используются настолько часто, что для этого придумали специальный метод: [Link](value) . Он
возвращает true , если value массив, и false , если нет.
alert([Link]({})); // false
alert([Link]([])); // true
Почти все методы массива, которые вызывают функции – такие как find , filter , map , за исключением метода
sort , принимают необязательный параметр thisArg .
Этот параметр не объяснялся выше, так как очень редко используется, но для наиболее полного понимания темы мы
обязаны его рассмотреть.
Вот полный синтаксис этих методов:
[Link](func, thisArg);
[Link](func, thisArg);
[Link](func, thisArg);
// ...
// thisArg -- необязательный последний аргумент
Например, тут мы используем метод объекта army как фильтр, и thisArg передаёт ему контекст:
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return [Link] >= [Link] && [Link] < [Link];
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
alert([Link]); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
205/597
Итого
Пожалуйста, обратите внимание, что методы push , pop , shift , unshift , sort , reverse и splice изменяют
исходный массив.
Эти методы – самые используемые, их достаточно в 99% случаев. Но существуют и другие:
● [Link](fn) /[Link](fn) проверяет массив.
Функция fn вызывается для каждого элемента массива аналогично map . Если какие-либо/все результаты
вызовов являются true , то метод возвращает true , иначе false .
Эти методы ведут себя примерно так же, как операторы || и && : если fn возвращает истинное значение,
[Link]() немедленно возвращает true и останавливает перебор остальных элементов; если fn
возвращает ложное значение, [Link]() немедленно возвращает false и также прекращает перебор
остальных элементов.
Мы можем использовать every для сравнения массивов:
● [Link](value, start, end) – заполняет массив повторяющимися value , начиная с индекса start до end .
206/597
● [Link](target, start, end) – копирует свои элементы, начиная с позиции start и заканчивая end , в себя,
на позицию target (перезаписывая существующие).
● [Link](depth) /[Link](fn) создаёт новый плоский массив из многомерного массива.
На первый взгляд может показаться, что существует очень много разных методов, которые довольно сложно
запомнить. Но это гораздо проще, чем кажется.
Внимательно изучите шпаргалку, представленную выше, а затем, чтобы попрактиковаться, решите задачи,
предложенные в данной главе. Так вы получите необходимый опыт в правильном использовании методов массива.
Всякий раз, когда вам будет необходимо что-то сделать с массивом, а вы не знаете, как это сделать – приходите
сюда, смотрите на таблицу и ищите правильный метод. Примеры помогут вам всё сделать правильно, и вскоре вы
быстро запомните методы без особых усилий.
Задачи
То есть дефисы удаляются, а все слова после них получают заглавную букву.
Примеры:
camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';
P.S. Подсказка: используйте split , чтобы разбить строку на массив символов, потом переделайте всё как нужно и
методом join соедините обратно.
К решению
Фильтрация по диапазону
важность: 4
Напишите функцию filterRange(arr, a, b) , которая принимает массив arr , ищет элементы со значениями
больше или равными a и меньше или равными b и возвращает результат в виде массива.
Например:
К решению
207/597
Напишите функцию filterRangeInPlace(arr, a, b) , которая принимает массив arr и удаляет из него все
значения кроме тех, которые находятся между a и b . То есть, проверка имеет вид a ≤ arr[i] ≤ b .
Например:
К решению
К решению
У нас есть массив строк arr . Нужно получить отсортированную копию, но оставить arr неизменённым.
К решению
1.
Во-первых, реализуйте метод calculate(str) , который принимает строку типа "1 + 2" в формате «ЧИСЛО
оператор ЧИСЛО» (разделено пробелами) и возвращает результат. Метод должен понимать плюс + и минус - .
Пример использования:
2.
208/597
Затем добавьте метод addMethod(name, func) , который добавляет в калькулятор новые операции. Он
принимает оператор name и функцию с двумя аргументами func(a,b) , которая описывает его.
К решению
У вас есть массив объектов user , и в каждом из них есть [Link] . Напишите код, который преобразует их в
массив имён.
Например:
К решению
Трансформировать в объекты
важность: 5
У вас есть массив объектов user , и у каждого из объектов есть name , surname и id .
Напишите код, который создаст ещё один массив объектов с параметрами id и fullName , где fullName – состоит
из name и surname .
Например:
/*
usersMapped = [
{ fullName: "Вася Пупкин", id: 1 },
{ fullName: "Петя Иванов", id: 2 },
{ fullName: "Маша Петрова", id: 3 }
]
*/
209/597
alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // Вася Пупкин
Итак, на самом деле вам нужно трансформировать один массив объектов в другой. Попробуйте использовать => .
Это небольшая уловка.
К решению
Напишите функцию sortByAge(users) , которая принимает массив объектов со свойством age и сортирует их по
нему.
Например:
sortByAge(arr);
К решению
Перемешайте массив
важность: 3
Многократные прогоны через shuffle могут привести к разным последовательностям элементов. Например:
shuffle(arr);
// arr = [3, 2, 1]
shuffle(arr);
// arr = [2, 1, 3]
shuffle(arr);
// arr = [3, 1, 2]
// ...
Все последовательности элементов должны иметь одинаковую вероятность. Например, [1,2,3] может быть
переупорядочено как [1,2,3] или [1,3,2] , или [3,1,2] и т.д., с равной вероятностью каждого случая.
К решению
Напишите функцию getAverageAge(users) , которая принимает массив объектов со свойством age и возвращает
средний возраст.
210/597
Например:
К решению
Напишите функцию unique(arr) , которая возвращает массив, содержащий только уникальные элементы arr .
Например:
function unique(arr) {
/* ваш код */
}
К решению
Создайте функцию groupById(arr) , которая создаст из него объект с id в качестве ключа и элементами массива
в качестве значений.
Например:
let users = [
{id: 'john', name: "John Smith", age: 20},
{id: 'ann', name: "Ann Smith", age: 24},
{id: 'pete', name: "Pete Peterson", age: 31},
];
/*
после вызова у нас должно получиться:
usersById = {
john: {id: 'john', name: "John Smith", age: 20},
ann: {id: 'ann', name: "Ann Smith", age: 24},
pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/
Такая функция очень удобна при работе с данными, которые приходят с сервера.
В этой задаче мы предполагаем, что id уникален. Не может быть двух элементов массива с одинаковым id .
211/597
Используйте метод .reduce в решении.
К решению
Перебираемые объекты
Перебираемые (или итерируемые) объекты – это обобщение массивов. Концепция, которая позволяет использовать
любой объект в цикле for..of .
Конечно же, сами массивы являются перебираемыми объектами. Но есть и много других встроенных перебираемых
объектов, например, строки.
Если объект не является массивом, но представляет собой коллекцию каких-то элементов (список, набор), то удобно
использовать цикл for..of для их перебора, так что давайте посмотрим, как это сделать.
[Link]
let range = {
from: 1,
to: 5
};
Чтобы сделать range итерируемым (и позволить for..of работать с ним), нам нужно добавить в объект метод с
именем [Link] (специальный встроенный Symbol , созданный как раз для этого).
1. Когда цикл for..of запускается, он вызывает этот метод один раз (или выдаёт ошибку, если метод не найден).
Этот метод должен вернуть итератор – объект с методом next .
2. Дальше for..of работает только с этим возвращённым объектом.
3. Когда for..of хочет получить следующее значение, он вызывает метод next() этого объекта.
4. Результат вызова next() должен иметь вид {done: Boolean, value: any} , где done=true означает, что
цикл завершён, в противном случае value содержит очередное значение.
let range = {
from: 1,
to: 5
};
212/597
}
}
};
};
// теперь работает!
for (let num of range) {
alert(num); // 1, затем 2, 3, 4, 5
}
let range = {
from: 1,
to: 5,
[[Link]]() {
[Link] = [Link];
return this;
},
next() {
if ([Link] <= [Link]) {
return { done: false, value: [Link]++ };
} else {
return { done: true };
}
}
};
Теперь range[[Link]]() возвращает сам объект range : у него есть необходимый метод next() , и
он запоминает текущее состояние итерации в [Link] . Короче? Да. И иногда такой способ тоже хорош.
Недостаток такого подхода в том, что теперь мы не можем использовать этот объект в двух параллельных циклах
for..of : у них будет общее текущее состояние итерации, потому что теперь существует лишь один итератор – сам
объект. Но необходимость в двух циклах for..of , выполняемых одновременно, возникает редко, даже при наличии
асинхронных операций.
Бесконечные итераторы
Можно сделать бесконечный итератор. Например, range будет бесконечным при [Link] = Infinity . Или
мы можем создать итерируемый объект, который генерирует бесконечную последовательность псевдослучайных
чисел. Это бывает полезно.
Метод next не имеет ограничений, он может возвращать всё новые и новые значения, это нормально.
Конечно же, цикл for..of с таким итерируемым объектом будет бесконечным. Но мы всегда можем прервать
его, используя break .
213/597
for (let char of "test") {
// срабатывает 4 раза: по одному для каждого символа
alert( char ); // t, затем e, затем s, затем t
}
Чтобы понять устройство итераторов чуть глубже, давайте посмотрим, как их использовать явно.
Мы будем перебирать строку точно так же, как цикл for..of , но вручную, прямыми вызовами. Нижеприведённый
код получает строковый итератор и берёт из него значения:
while (true) {
let result = [Link]();
if ([Link]) break;
alert([Link]); // выводит символы один за другим
}
Такое редко бывает необходимо, но это даёт нам больше контроля над процессом, чем for..of . Например, мы
можем разбить процесс итерации на части: перебрать немного элементов, затем остановиться, сделать что-то ещё и
потом продолжить.
Есть два официальных термина, которые очень похожи, но в то же время сильно различаются. Поэтому убедитесь,
что вы как следует поняли их, чтобы избежать путаницы.
● Итерируемые объекты – это объекты, которые реализуют метод [Link] , как было описано выше.
● Псевдомассивы – это объекты, у которых есть индексы и свойство length , то есть, они выглядят как массивы.
При использовании JavaScript в браузере или других окружениях мы можем встретить объекты, которые являются
итерируемыми или псевдомассивами, или и тем, и другим.
Например, строки итерируемы (для них работает for..of ) и являются псевдомассивами (они индексированы и есть
length ).
Но итерируемый объект может не быть псевдомассивом. И наоборот: псевдомассив может не быть итерируемым.
Например, объект range из примера выше – итерируемый, но не является псевдомассивом, потому что у него нет
индексированных свойств и length .
А вот объект, который является псевдомассивом, но его нельзя итерировать:
214/597
Что у них общего? И итерируемые объекты, и псевдомассивы – это обычно не массивы, у них нет методов push ,
pop и т.д. Довольно неудобно, если у нас есть такой объект и мы хотим работать с ним как с массивом. Например,
мы хотели бы работать с range , используя методы массивов. Как этого достичь?
[Link]
Есть универсальный метод [Link] , который принимает итерируемый объект или псевдомассив и делает из него
«настоящий» Array . После этого мы уже можем использовать методы массивов.
Например:
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
[Link] в строке (*) принимает объект, проверяет, является ли он итерируемым объектом или
псевдомассивом, затем создаёт новый массив и копирует туда все элементы.
То же самое происходит с итерируемым объектом:
Необязательный второй аргумент может быть функцией, которая будет применена к каждому элементу перед
добавлением в массив, а thisArg позволяет установить this для этой функции.
Например:
alert(arr); // 1,4,9,16,25
alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert([Link]); // 2
В отличие от [Link] , этот метод в работе опирается на итерируемость строки, и поэтому, как и for..of , он
корректно работает с суррогатными парами.
Технически это то же самое, что и:
215/597
let chars = []; // [Link] внутри себя выполняет тот же цикл
for (let char of str) {
[Link](char);
}
alert(chars);
alert( slice(str, 1, 3) ); // 😂𩷶
Итого
Объекты, имеющие индексированные свойства и length , называются псевдомассивами. Они также могут иметь
другие свойства и методы, но у них нет встроенных методов массивов.
Если мы заглянем в спецификацию, мы увидим, что большинство встроенных методов рассчитывают на то, что они
будут работать с итерируемыми объектами или псевдомассивами вместо «настоящих» массивов, потому что эти
объекты более абстрактны.
[Link](obj[, mapFn, thisArg]) создаёт настоящий Array из итерируемого объекта или псевдомассива
obj , и затем мы можем применять к нему методы массивов. Необязательные аргументы mapFn и thisArg
позволяют применять функцию с задаваемым контекстом к каждому элементу.
Map и Set
Сейчас мы знаем о следующих сложных структурах данных:
● Объекты для хранения именованных коллекций.
●
Массивы для хранения упорядоченных коллекций.
Но этого не всегда достаточно для решения повседневных задач. Поэтому также существуют Map и Set .
Map
Map – это коллекция ключ/значение, как и Object . Но основное отличие в том, что Map позволяет использовать
ключи любого типа.
Методы и свойства:
● new Map() – создаёт коллекцию.
● [Link](key, value) – записывает по ключу key значение value .
●
[Link](key) – возвращает значение по ключу или undefined , если ключ key отсутствует.
216/597
● [Link](key) – возвращает true , если ключ key присутствует в коллекции, иначе false .
●
[Link](key) – удаляет элемент (пару «ключ/значение») по ключу key .
●
[Link]() – очищает коллекцию от всех элементов.
● [Link] – возвращает текущее количество элементов.
Например:
alert([Link]); // 3
Как мы видим, в отличие от объектов, ключи не были приведены к строкам. Можно использовать любые типы данных
для ключей.
alert([Link](john)); // 123
Использование объектов в качестве ключей – одна из наиболее заметных и важных функций Map . Это то что
невозможно для Object . Строка в качестве ключа в Object – это нормально, но мы не можем использовать другой
Object в качестве ключа в Object .
Давайте попробуем заменить Map на Object :
Так как visitsCountObj является объектом, он преобразует все ключи Object , такие как john и ben , в одну и
ту же строку "[object Object]" . Это определенно не то, чего мы хотим.
217/597
Как объект Map сравнивает ключи
Чтобы сравнивать ключи, объект Map использует алгоритм SameValueZero . Это почти такое же сравнение, что
и === , с той лишь разницей, что NaN считается равным NaN . Так что NaN также может использоваться в
качестве ключа.
Этот алгоритм не может быть заменён или модифицирован.
Цепочка вызовов
Каждый вызов [Link] возвращает объект map, так что мы можем объединить вызовы в цепочку:
[Link]("1", "str1")
.set(1, "num1")
.set(true, "bool1");
Перебор Map
Например:
Кроме этого, Map имеет встроенный метод forEach , схожий со встроенным методом массивов Array :
218/597
[Link]: Map из Object
При создании Map мы можем указать массив (или другой итерируемый объект) с парами ключ-значение для
инициализации, как здесь:
Если у нас уже есть обычный объект, и мы хотели бы создать Map из него, то поможет встроенный метод
[Link](obj) , который получает объект и возвращает массив пар ключ-значение для него, как раз в этом
формате.
let obj = {
name: "John",
age: 30
};
Здесь [Link] возвращает массив пар ключ-значение: [ ["name","John"], ["age", 30] ] . Это
именно то, что нужно для создания Map .
Мы только что видели, как создать Map из обычного объекта при помощи [Link](obj) .
Есть метод [Link] , который делает противоположное: получив массив пар вида [ключ,
значение] , он создаёт из них объект:
alert([Link]); // 2
// готово!
// obj = { banana: 1, orange: 2, meat: 4 }
alert([Link]); // 2
219/597
Вызов [Link]() возвращает итерируемый объект пар ключ/значение, как раз в нужном формате для
[Link] .
Мы могли бы написать строку (*) ещё короче:
Это то же самое, так как [Link] ожидает перебираемый объект в качестве аргумента, не
обязательно массив. А перебор map как раз возвращает пары ключ/значение, так же, как и [Link]() . Так что
в итоге у нас будет обычный объект с теми же ключами/значениями, что и в map .
Set
Объект Set – это особый вид коллекции: «множество» значений (без ключей), где каждое значение может
появляться только один раз.
Его основные методы это:
● new Set(iterable) – создаёт Set , и если в качестве аргумента был предоставлен итерируемый объект
(обычно это массив), то копирует его значения в новый Set .
● [Link](value) – добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект
set .
● [Link](value) – удаляет значение, возвращает true , если value было в множестве на момент
вызова, иначе false .
●
[Link](value) – возвращает true , если значение присутствует в множестве, иначе false .
● [Link]() – удаляет все имеющиеся значения.
● [Link] – возвращает количество элементов в множестве.
Основная «изюминка» – это то, что при повторных вызовах [Link]() с одним и тем же значением ничего не
происходит, за счёт этого как раз и получается, что каждое значение появляется один раз.
Например, мы ожидаем посетителей, и нам необходимо составить их список. Но повторные визиты не должны
приводить к дубликатам. Каждый посетитель должен появиться в списке только один раз.
Множество Set – как раз то, что нужно для этого:
Альтернативой множеству Set может выступать массив для хранения гостей и дополнительный код для проверки
уже имеющегося элемента с помощью [Link] . Но в этом случае будет хуже производительность, потому что
[Link] проходит весь массив для проверки наличия элемента. Множество Set лучше оптимизировано для
добавлений, оно автоматически проверяет на уникальность.
Мы можем перебрать содержимое объекта set как с помощью метода for..of , так и используя forEach :
220/597
let set = new Set(["апельсин", "яблоко", "банан"]);
// то же самое с forEach:
[Link]((value, valueAgain, set) => {
alert(value);
});
Заметим забавную вещь. Функция в forEach у Set имеет 3 аргумента: значение value , потом снова то же самое
значение valueAgain , и только потом целевой объект. Это действительно так, значение появляется в списке
аргументов дважды.
Это сделано для совместимости с объектом Map , в котором колбэк forEach имеет 3 аргумента. Выглядит немного
странно, но в некоторых случаях может помочь легко заменить Map на Set и наоборот.
Set имеет те же встроенные методы, что и Map :
●
[Link]() – возвращает перебираемый объект для значений,
● [Link]() – то же самое, что и [Link]() , присутствует для обратной совместимости с Map ,
● [Link]() – возвращает перебираемый объект для пар вида [значение, значение] , присутствует
для обратной совместимости с Map .
Итого
Методы и свойства:
● new Map([iterable]) – создаёт коллекцию, можно указать перебираемый объект (обычно массив) из пар
[ключ,значение] для инициализации.
● [Link](key, value) – записывает по ключу key значение value .
● [Link](key) – возвращает значение по ключу или undefined , если ключ key отсутствует.
●
[Link](key) – возвращает true , если ключ key присутствует в коллекции, иначе false .
● [Link](key) – удаляет элемент по ключу key .
●
[Link]() – очищает коллекцию от всех элементов.
●
[Link] – возвращает текущее количество элементов.
Методы и свойства:
● new Set(iterable) – создаёт Set , можно указать перебираемый объект со значениями для инициализации.
● [Link](value) – добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект
set .
● [Link](value) – удаляет значение, возвращает true если value было в множестве на момент
вызова, иначе false .
● [Link](value) – возвращает true , если значение присутствует в множестве, иначе false .
● [Link]() – удаляет все имеющиеся значения.
● [Link] – возвращает количество элементов в множестве.
Перебор Map и Set всегда осуществляется в порядке добавления элементов, так что нельзя сказать, что это –
неупорядоченные коллекции, но поменять порядок элементов или получить элемент напрямую по его номеру нельзя.
Задачи
221/597
важность: 5
Создайте функцию unique(arr) , которая вернёт массив уникальных, не повторяющихся значений массива arr .
Например:
function unique(arr) {
/* ваш код */
}
К решению
Отфильтруйте анаграммы
важность: 4
Анаграммы – это слова, у которых те же буквы в том же количестве, но они располагаются в другом порядке.
Например:
nap - pan
ear - are - era
cheaters - hectares - teachers
Например:
Из каждой группы анаграмм должно остаться только одно слово, не важно какое.
К решению
Перебираемые ключи
важность: 5
Мы хотели бы получить массив ключей [Link]() в переменную и далее работать с ними, например, применить
метод .push .
Но это не выходит:
[Link]("name", "John");
222/597
// Error: [Link] is not a function
// Ошибка: [Link] -- это не функция
[Link]("more");
К решению
WeakMap и WeakSet
Как мы знаем из главы Сборка мусора, движок JavaScript хранит значения в памяти до тех пор, пока они достижимы
(то есть, эти значения могут быть использованы).
Например:
// перепишем ссылку
john = null;
Обычно свойства объекта, элементы массива или другой структуры данных считаются достижимыми и сохраняются в
памяти до тех пор, пока эта структура данных содержится в памяти.
Например, если мы поместим объект в массив, то до тех пор, пока массив существует, объект также будет
существовать в памяти, несмотря на то, что других ссылок на него нет.
Например:
Аналогично, если мы используем объект как ключ в Map , то до тех пор, пока существует Map , также будет
существовать и этот объект. Он занимает место в памяти и не может быть удалён сборщиком мусора.
Например:
WeakMap – принципиально другая структура в этом аспекте. Она не предотвращает удаление объектов сборщиком
мусора, когда эти объекты выступают в качестве ключей.
Давайте посмотрим, что это означает, на примерах.
WeakMap
Первое его отличие от Map в том, что ключи в WeakMap должны быть объектами, а не примитивными значениями:
223/597
let weakMap = new WeakMap();
Теперь, если мы используем объект в качестве ключа и если больше нет ссылок на этот объект, то он будет удалён из
памяти (и из объекта WeakMap ) автоматически.
Сравните это поведение с поведением обычного Map , пример которого был приведён ранее. Теперь john
существует только как ключ в WeakMap и может быть удалён оттуда автоматически.
WeakMap не поддерживает перебор и методы keys() , values() , entries() , так что нет способа взять все
ключи или значения из неё.
В WeakMap присутствуют только следующие методы:
●
[Link](key)
● [Link](key, value)
●
[Link](key)
● [Link](key)
К чему такие ограничения? Из-за особенностей технической реализации. Если объект станет недостижим (как объект
john в примере выше), то он будет автоматически удалён сборщиком мусора. Но нет информации, в какой момент
произойдёт эта очистка.
Решение о том, когда делать сборку мусора, принимает движок JavaScript. Он может посчитать необходимым как
удалить объект прямо сейчас, так и отложить эту операцию, чтобы удалить большее количество объектов за раз
позже. Так что технически количество элементов в коллекции WeakMap неизвестно. Движок может произвести
очистку сразу или потом, или сделать это частично. По этой причине методы для доступа ко всем сразу ключам/
значениям недоступны.
Но для чего же нам нужна такая структура данных?
Если мы работаем с объектом, который «принадлежит» другому коду, может быть даже сторонней библиотеке, и
хотим сохранить у себя какие-то данные для него, которые должны существовать лишь пока существует этот объект,
то WeakMap – как раз то, что нужно.
Мы кладём эти данные в WeakMap , используя объект как ключ, и когда сборщик мусора удалит объекты из памяти,
ассоциированные с ними данные тоже автоматически исчезнут.
Предположим, у нас есть код, который ведёт учёт посещений для пользователей. Информация хранится в коллекции
Map : объект, представляющий пользователя, является ключом, а количество визитов – значением. Когда
пользователь нас покидает (его объект удаляется сборщиком мусора), то больше нет смысла хранить
соответствующий счётчик посещений.
224/597
Вот пример реализации счётчика посещений с использованием Map :
// 📁 [Link]
let visitsCountMap = new Map(); // map: пользователь => число визитов
// увеличиваем счётчик
function countUser(user) {
let count = [Link](user) || 0;
[Link](user, count + 1);
}
А вот другая часть кода, возможно, в другом файле, которая использует countUser :
// 📁 [Link]
let john = { name: "John" };
Теперь объект john должен быть удалён сборщиком мусора, но он продолжает оставаться в памяти, так как
является ключом в visitsCountMap .
Нам нужно очищать visitsCountMap при удалении объекта пользователя, иначе коллекция будет бесконечно
расти. Подобная очистка может быть неудобна в реализации при сложной архитектуре приложения.
Проблемы можно избежать, если использовать WeakMap :
// 📁 [Link]
let visitsCountMap = new WeakMap(); // map: пользователь => число визитов
// увеличиваем счётчик
function countUser(user) {
let count = [Link](user) || 0;
[Link](user, count + 1);
}
Теперь нет необходимости вручную очищать visitsCountMap . После того, как объект john стал недостижим
другими способами, кроме как через WeakMap , он удаляется из памяти вместе с информацией по такому ключу из
WeakMap .
Другая частая сфера применения – это кеширование, когда результат вызова функции должен где-то запоминаться
(«кешироваться») для того, чтобы дальнейшие её вызовы на том же объекте могли просто брать уже готовый
результат, повторно используя его.
Для хранения результатов мы можем использовать Map , вот так:
// 📁 [Link]
let cache = new Map();
[Link](obj, result);
}
return [Link](obj);
}
// 📁 [Link]
let obj = {/* допустим, у нас есть какой-то объект */};
225/597
let result1 = process(obj); // вычислен результат
Многократные вызовы process(obj) с тем же самым объектом в качестве аргумента ведут к тому, что результат
вычисляется только в первый раз, а затем последующие вызовы берут его из кеша. Недостатком является то, что
необходимо вручную очищать cache от ставших ненужными объектов.
Но если мы будем использовать WeakMap вместо Map , то эта проблема исчезнет: закешированные результаты будут
автоматически удалены из памяти сборщиком мусора.
// 📁 [Link]
let cache = new WeakMap();
[Link](obj, result);
}
return [Link](obj);
}
// 📁 [Link]
let obj = {/* какой-то объект */};
WeakSet
Будучи «слабой» версией оригинальной структуры данных, она тоже служит в качестве дополнительного хранилища.
Но не для произвольных данных, а скорее для значений типа «да/нет». Присутствие во множестве WeakSet может
что-то сказать нам об объекте.
Например, мы можем добавлять пользователей в WeakSet для учёта тех, кто посещал наш сайт:
226/597
// проверим, заходил ли John?
alert([Link](john)); // true
john = null;
// структура данных visitedSet будет очищена автоматически (объект john будет удалён из visitedSet)
Наиболее значительным ограничением WeakMap и WeakSet является то, что их нельзя перебрать или взять всё
содержимое. Это может доставлять неудобства, но не мешает WeakMap/WeakSet выполнять их главную задачу –
быть дополнительным хранилищем данных для объектов, управляемых из каких-то других мест в коде.
Итого
WeakMap – это Map -подобная коллекция, позволяющая использовать в качестве ключей только объекты, и
автоматически удаляющая их вместе с соответствующими значениями, как только они становятся недостижимыми
иными путями.
WeakSet – это Set -подобная коллекция, которая хранит только объекты и удаляет их, как только они становятся
недостижимыми иными путями.
Обе этих структуры данных не поддерживают методы и свойства, работающие со всем содержимым сразу или
возвращающие информацию о размере коллекции. Возможны только операции на отдельном элементе коллекции.
WeakMap и WeakSet используются как вспомогательные структуры данных в дополнение к «основному» месту
хранения объекта. Если объект удаляется из основного хранилища и нигде не используется, кроме как в качестве
ключа в WeakMap или в WeakSet , то он будет удалён автоматически.
Задачи
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
У вас есть к ним доступ, но управление этим массивом происходит где-то ещё. Добавляются новые сообщения и
удаляются старые, и вы не знаете в какой момент это может произойти.
Имея такую вводную информацию, решите, какую структуру данных вы могли бы использовать для ответа на вопрос
«было ли сообщение прочитано?». Структура должна быть подходящей, чтобы можно было однозначно сказать, было
ли прочитано это сообщение для каждого объекта сообщения.
P.S. Когда сообщение удаляется из массива messages , оно должно также исчезать из структуры данных.
P.P.S. Нам не следует модифицировать сами объекты сообщений, добавлять туда свойства. Если сообщения
принадлежат какому-то другому коду, то это может привести к плохим последствиям.
К решению
let messages = [
{ text: "Hello", from: "John" },
227/597
{ text: "How goes?", from: "John" },
{ text: "See you soon", from: "Alice" }
];
Теперь вопрос стоит так: какую структуру данных вы бы предложили использовать для хранения информации о том,
когда сообщение было прочитано?
В предыдущем задании нам нужно было сохранить только факт прочтения «да или нет». Теперь же нам нужно
сохранить дату, и она должна исчезнуть из памяти при удалении «сборщиком мусора» сообщения.
P.S. Даты в JavaScript можно хранить как объекты встроенного класса Date , которые мы разберём позднее.
К решению
Простые объекты также можно перебирать похожими методами, но синтаксис немного отличается.
Map Object
Почему так? Основная причина – гибкость. Помните, что объекты являются основой всех сложных структур в
JavaScript. У нас может быть объект data , который реализует свой собственный метод [Link]() . И мы всё
ещё можем применять к нему стандартный метод [Link](data) .
Второе отличие в том, что методы вида Object.* возвращают «реальные» массивы, а не просто итерируемые
объекты. Это в основном по историческим причинам.
Например:
let user = {
name: "John",
age: 30
};
228/597
●
[Link](user) = [ ["name","John"], ["age",30] ]
let user = {
name: "John",
age: 30
};
// перебор значений
for (let value of [Link](user)) {
alert(value); // John, затем 30
}
Обычно это удобно. Но если требуется учитывать и символьные ключи, то для этого существует отдельный метод
[Link] , возвращающий массив только символьных ключей. Также, существует метод
[Link](obj) , который возвращает все ключи.
Трансформации объекта
У объектов нет множества методов, которые есть в массивах, например map , filter и других.
Если мы хотели бы их применить, то можно использовать [Link] с последующим вызовом
[Link] :
1. Вызов [Link](obj) возвращает массив пар ключ/значение для obj .
2. На нём вызываем методы массива, например, map .
3. Используем [Link](array) на результате, чтобы преобразовать его обратно в объект.
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
alert([Link]); // 8
Это может выглядеть сложным на первый взгляд, но становится лёгким для понимания после нескольких раз
использования.
Можно делать и более сложные «однострочные» преобразования таким путём. Важно только сохранять баланс,
чтобы код при этом был достаточно простым для понимания.
Задачи
Напишите функцию sumSalaries(salaries) , которая возвращает сумму всех зарплат с помощью метода
[Link] и цикла for..of .
229/597
Например:
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
К решению
let user = {
name: 'John',
age: 30
};
alert( count(user) ); // 2
К решению
Деструктурирующее присваивание
В JavaScript есть две чаще всего используемые структуры данных – это Object и Array .
●
Объекты позволяют нам создавать одну сущность, которая хранит элементы данных по ключам.
● Массивы позволяют нам собирать элементы данных в упорядоченный список.
Деструктуризация массива
// деструктурирующее присваивание
// записывает firstName = arr[0]
// и surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // Ilya
alert(surname); // Kantor
230/597
Теперь мы можем использовать переменные вместо элементов массива.
Отлично смотрится в сочетании со split или другими методами, возвращающими массив:
Как вы можете видеть, синтаксис прост. Однако есть несколько странных моментов. Давайте посмотрим больше
примеров, чтобы лучше понять это.
В примере выше второй элемент массива пропускается, а третий присваивается переменной title , оставшиеся
элементы массива также пропускаются (так как для них нет переменных).
alert([Link]); // Ilya
alert([Link]); // Kantor
231/597
Цикл с .entries()
В предыдущей главе мы видели метод [Link](obj) .
Мы можем использовать его с деструктуризацией для цикличного перебора ключей и значений объекта:
let user = {
name: "John",
age: 30
};
// Map перебирает как пары [ключ, значение], что очень удобно для деструктурирования
for (let [key, value] of user) {
alert(`${key}:${value}`); // name:John, затем age:30
}
Здесь мы создаём временный массив из двух переменных и немедленно деструктурируем его в порядке замены.
Таким образом, мы можем поменять местами даже более двух переменных.
let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// Дальнейшие элементы нигде не присваиваются
Если мы хотим не просто получить первые значения, но и собрать все остальные, то мы можем добавить ещё один
параметр, который получает остальные значения, используя оператор «остаточные параметры» – троеточие
( "..." ):
let [name1, name2, ... rest] = ["Julius", "Caesar", "Consul" , "of the Roman Republic"];
232/597
// rest это массив элементов, начиная с 3-го
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert([Link]); // 2
let [name1, name2, ... titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// теперь titles = ["Consul", "of the Roman Republic"]
Значения по умолчанию
Если в массиве меньше значений, чем в присваивании, то ошибки не будет. Отсутствующие значения считаются
неопределёнными:
alert(firstName); // undefined
alert(surname); // undefined
Если мы хотим, чтобы значение «по умолчанию» заменило отсутствующее, мы можем указать его с помощью = :
// значения по умолчанию
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
Значения по умолчанию могут быть гораздо более сложными выражениями или даже функциями. Они выполняются,
только если значения отсутствуют.
Например, здесь мы используем функцию prompt для указания двух значений по умолчанию.
Обратите внимание, prompt будет запущен только для пропущенного значения ( surname ).
Деструктуризация объекта
Синтаксис:
У нас есть существующий объект с правой стороны, который мы хотим разделить на переменные. Левая сторона
содержит «шаблон» для соответствующих свойств. В простом случае это список названий переменных в {...} .
Например:
let options = {
title: "Menu",
width: 100,
height: 200
};
233/597
alert(title); // Menu
alert(width); // 100
alert(height); // 200
Шаблон с левой стороны может быть более сложным и определять соответствие между свойствами и переменными.
Если мы хотим присвоить свойство объекта переменной с другим названием, например, свойство [Link]
присвоить переменной w , то мы можем использовать двоеточие:
let options = {
title: "Menu",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;
// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
Двоеточие показывает «что : куда идёт». В примере выше свойство width сохраняется в переменную w , свойство
height сохраняется в h , а title присваивается одноимённой переменной.
Для потенциально отсутствующих свойств мы можем установить значения по умолчанию, используя "=" , как здесь:
let options = {
title: "Menu"
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
Как и в случае с массивами, значениями по умолчанию могут быть любые выражения или даже функции. Они
выполнятся, если значения отсутствуют.
В коде ниже prompt запросит width , но не title :
let options = {
title: "Menu"
};
alert(title); // Menu
alert(width); // (результат prompt)
let options = {
title: "Menu"
234/597
};
alert(title); // Menu
alert(w); // 100
alert(h); // 200
Если у нас есть большой объект с множеством свойств, можно взять только то, что нужно:
let options = {
title: "Menu",
width: 100,
height: 200
};
alert(title); // Menu
Выглядит так:
let options = {
title: "Menu",
height: 200,
width: 100
};
235/597
Обратите внимание на let
В примерах выше переменные были объявлены в присваивании: let {…} = {…} . Конечно, мы могли бы
использовать существующие переменные и не указывать let , но тут есть подвох.
Вот так не будет работать:
Проблема в том, что JavaScript обрабатывает {...} в основном потоке кода (не внутри другого выражения) как
блок кода. Такие блоки кода могут быть использованы для группировки операторов, например:
{
// блок кода
let message = "Hello";
// ...
alert( message );
}
Так что здесь JavaScript считает, что видит блок кода, отсюда и ошибка. На самом-то деле у нас деструктуризация.
Чтобы показать JavaScript, что это не блок кода, мы можем заключить выражение в скобки (...) :
Вложенная деструктуризация
Если объект или массив содержит другие вложенные объекты или массивы, то мы можем использовать более
сложные шаблоны с левой стороны, чтобы извлечь более глубокие свойства.
В приведённом ниже коде options хранит другой объект в свойстве size и массив в свойстве items . Шаблон в
левой части присваивания имеет такую же структуру, чтобы извлечь данные из них:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
236/597
Весь объект options , кроме свойства extra , которое в левой части отсутствует, присваивается в соответствующие
переменные:
В итоге у нас есть width , height , item1 , item2 и title со значением по умолчанию.
Заметим, что переменные для size и items отсутствуют, так как мы взяли сразу их содержимое.
Есть ситуации, когда функция имеет много параметров, большинство из которых не обязательны. Это особенно верно
для пользовательских интерфейсов. Представьте себе функцию, которая создаёт меню. Она может иметь ширину,
высоту, заголовок, список элементов и так далее.
Вот так – плохой способ писать подобные функции:
В реальной жизни проблема заключается в том, как запомнить порядок всех аргументов. Обычно IDE пытаются
помочь нам, особенно если код хорошо документирован, но всё же… Другая проблема заключается в том, как
вызвать функцию, когда большинство параметров передавать не надо, и значения по умолчанию вполне подходят.
Это выглядит ужасно. И становится нечитаемым, когда мы имеем дело с большим количеством параметров.
На помощь приходит деструктуризация!
Мы можем передать параметры как объект, и функция немедленно деструктурирует его в переменные:
showMenu(options);
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
237/597
function showMenu({
title = "Untitled",
width: w = 100, // width присваиваем в w
height: h = 200, // height присваиваем в h
items: [item1, item2] // первый элемент items присваивается в item1, второй в item2
}) {
alert( `${title} ${w} ${h}` ); // My Menu 100 200
alert( item1 ); // Item1
alert( item2 ); // Item2
}
showMenu(options);
function({
incomingProperty: varName = defaultValue
...
})
Тогда для объекта с параметрами будет создана переменная varName для свойства с именем incomingProperty
по умолчанию равная defaultValue .
Пожалуйста, обратите внимание, что такое деструктурирование подразумевает, что в showMenu() будет
обязательно передан аргумент. Если нам нужны все значения по умолчанию, то нам следует передать пустой объект:
Мы можем исправить это, сделав {} значением по умолчанию для всего объекта параметров:
В приведённом выше коде весь объект аргументов по умолчанию равен {} , поэтому всегда есть что-то, что можно
деструктурировать.
Итого
●
Деструктуризация позволяет разбивать объект или массив на переменные при присвоении.
● Полный синтаксис для объекта:
Cвойство prop объекта object здесь должно быть присвоено переменной varName . Если в объекте
отсутствует такое свойство, переменной varName присваивается значение по умолчанию.
Свойства, которые не были упомянуты, копируются в объект rest .
●
Полный синтаксис для массива:
Первый элемент отправляется в item1 ; второй отправляется в item2 , все остальные элементы попадают в
массив rest .
● Можно извлекать данные из вложенных объектов и массивов, для этого левая сторона должна иметь ту же
структуру, что и правая.
238/597
Задачи
Деструктурирующее присваивание
важность: 5
let user = {
name: "John",
years: 30
};
К решению
Максимальная зарплата
важность: 5
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
К решению
Дата и время
Встречайте новый встроенный объект: Date . Он содержит дату и время, а также предоставляет методы управления
ими.
Например, его можно использовать для хранения времени создания/изменения, для измерения времени или просто
для вывода текущей даты.
239/597
Создание
Для создания нового объекта Date нужно вызвать конструктор new Date() с одним из следующих аргументов:
new Date()
Без аргументов – создать объект Date с текущими датой и временем:
new Date(milliseconds)
Создать объект Date с временем, равным количеству миллисекунд (тысячная доля секунды), прошедших с 1 января
1970 года UTC+0.
Целое число, представляющее собой количество миллисекунд, прошедших с начала 1970 года, называется
таймстамп (англ. timestamp).
Это – легковесное численное представление даты. Из таймстампа всегда можно получить дату с помощью new
Date(timestamp) и преобразовать существующий объект Date в таймстамп, используя метод [Link]()
(см. ниже).
Датам до 1 января 1970 будут соответствовать отрицательные таймстампы, например:
new Date(datestring)
Если аргумент всего один, и это строка, то из неё «прочитывается» дата. Алгоритм разбора – такой же, как в
[Link] , который мы рассмотрим позже.
Например:
240/597
new Date(2011, 0, 1, 0, 0, 0, 0); // // 1 Jan 2011, [Link]
new Date(2011, 0, 1); // то же самое, так как часы и проч. равны 0
getFullYear()
Получить год (4 цифры)
getMonth()
Получить месяц, от 0 до 11.
getDate()
Получить день месяца, от 1 до 31, что несколько противоречит названию метода.
getDay()
Вернуть день недели от 0 (воскресенье) до 6 (суббота). Несмотря на то, что в ряде стран за первый день недели
принят понедельник, в JavaScript начало недели приходится на воскресенье.
// текущая дата
let date = new Date();
// час в часовом поясе UTC+0 (лондонское время без перехода на летнее время)
alert( [Link]() );
getTime()
Для заданной даты возвращает таймстамп – количество миллисекунд, прошедших с 1 января 1970 года UTC+0.
getTimezoneOffset()
Возвращает разницу в минутах между UTC и местным часовым поясом:
241/597
// если вы в часовом поясе UTC-1, то выводится 60
// если вы в часовом поясе UTC+3, выводится -180
alert( new Date().getTimezoneOffset() );
● setMonth(month, [date])
● setDate(date)
●
setSeconds(sec, [ms])
● setMilliseconds(ms)
● setTime(milliseconds) (устанавливает дату в виде целого количества миллисекунд, прошедших с
01.01.1970 UTC)
[Link](0);
alert(today); // выводится сегодняшняя дата, но значение часа будет 0
[Link](0, 0, 0, 0);
alert(today); // всё ещё выводится сегодняшняя дата, но время будет ровно [Link].
Автоисправление даты
Автоисправление – это очень полезная особенность объектов Date . Можно устанавливать компоненты даты вне
обычного диапазона значений, а объект сам себя исправит.
Пример:
Эту возможность часто используют, чтобы получить дату по прошествии заданного отрезка времени. Например,
получим дату «спустя 70 секунд с текущего момента»:
242/597
Также можно установить нулевые или даже отрицательные значения. Например:
[Link](0); // первый день месяца -- это 1, так что выводится последнее число предыдущего месяца
alert( date ); // 31 Dec 2015
Важный побочный эффект: даты можно вычитать, в результате получаем разность в миллисекундах.
Этот приём можно использовать для измерения времени:
[Link]()
Семантически он эквивалентен new Date().getTime() , однако метод не создаёт промежуточный объект Date .
Так что этот способ работает быстрее и не нагружает сборщик мусора.
Данный метод используется из соображений удобства или когда важно быстродействие, например, при разработке
игр на JavaScript или других специализированных приложений.
Вероятно, предыдущий пример лучше переписать так:
Бенчмаркинг
Будьте внимательны, если хотите точно протестировать производительность функции, которая зависит от
процессора.
Например, сравним две функции, вычисляющие разницу между двумя датами: какая сработает быстрее?
243/597
// есть date1 и date2, какая функция быстрее вернёт разницу между ними в миллисекундах?
function diffSubtract(date1, date2) {
return date2 - date1;
}
// или
function diffGetTime(date1, date2) {
return [Link]() - [Link]();
}
Обе функции делают буквально одно и то же, только одна использует явный метод [Link]() для получения
даты в миллисекундах, а другая полагается на преобразование даты в число. Результат их работы всегда один и тот
же.
Проведём измерения:
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
Вот это да! Метод getTime() работает ощутимо быстрее! Всё потому, что не производится преобразование типов, и
интерпретаторам такое намного легче оптимизировать.
Замечательно, это уже что-то. Но до хорошего бенчмарка нам ещё далеко.
Представьте, что при выполнении bench(diffSubtract) процессор параллельно делал что-то ещё, также
потребляющее ресурсы. А к началу выполнения bench(diffGetTime) он это уже завершил.
Достаточно реалистичный сценарий в современных многопроцессорных операционных системах.
В итоге у первого бенчмарка окажется меньше ресурсов процессора, чем у второго. Это может исказить результаты.
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
244/597
}
let time1 = 0;
let time2 = 0;
Современные интерпретаторы JavaScript начинают применять продвинутые оптимизации только к «горячему коду»,
выполняющемуся несколько раз (незачем оптимизировать то, что редко выполняется). Так что в примере выше
первые запуски не оптимизированы должным образом. Нелишним будет добавить предварительный запуск для
«разогрева»:
Возможны и более короткие варианты, например, YYYY-MM-DD или YYYY-MM , или даже YYYY .
let ms = [Link]('2012-01-26T[Link].417-07:00');
245/597
alert(date);
Итого
● Дата и время в JavaScript представлены объектом Date . Нельзя создать «только дату» или «только время»:
объекты Date всегда содержат и то, и другое.
● Счёт месяцев начинается с нуля (да, январь – это нулевой месяц).
●
Дни недели в getDay() также отсчитываются с нуля, что соответствует воскресенью.
● Объект Date самостоятельно корректируется при введении значений, выходящих за рамки допустимых. Это
полезно для сложения/вычитания дней/месяцев/недель.
● Даты можно вычитать, и разность возвращается в миллисекундах. Так происходит, потому что при преобразовании
в число объект Date становится таймстампом.
●
Используйте [Link]() для быстрого получения текущего времени в формате таймстампа.
Учтите, что, в отличие от некоторых других систем, в JavaScript таймстамп в миллисекундах, а не в секундах.
Порой нам нужно измерить время с большей точностью. Собственными средствами JavaScript измерять время в
микросекундах (одна миллионная секунды) нельзя, но в большинстве сред такая возможность есть. К примеру, в
браузерах есть метод [Link]() , возвращающий количество миллисекунд с начала загрузки страницы с
точностью до микросекунд (3 цифры после точки):
В [Link] для этого предусмотрен модуль microtime и ряд других способов. Технически почти любое устройство
или среда позволяет добиться большей точности, просто её нет в объекте Date .
Задачи
Создайте дату
важность: 5
Создайте объект Date для даты: 20 февраля 2012 года, 3 часа 12 минут. Временная зона – местная.
К решению
Напишите функцию getWeekDay(date) , показывающую день недели в коротком формате: «ПН», «ВТ», «СР», «ЧТ»,
«ПТ», «СБ», «ВС».
Например:
К решению
В Европейских странах неделя начинается с понедельника (день номер 1), затем идёт вторник (номер 2) и так до
воскресенья (номер 7). Напишите функцию getLocalDay(date) , которая возвращает «европейский» день недели
246/597
для даты date .
К решению
Создайте функцию getDateAgo(date, days) , возвращающую число, которое было days дней назад от даты
date .
К решению
Напишите функцию getLastDayOfMonth(year, month) , возвращающую последнее число месяца. Иногда это 30,
31 или даже февральские 28/29.
Параметры:
●
year – год из четырёх цифр, например, 2012.
●
month – месяц от 0 до 11.
К решению
Функция должна работать в любой день, т.е. в ней не должно быть конкретного значения сегодняшней даты.
К решению
247/597
Сколько секунд осталось до завтра?
важность: 5
getSecondsToTomorrow() == 3600
P.S. Функция должна работать в любой день, т.е. в ней не должно быть конкретного значения сегодняшней даты.
К решению
Например:
К решению
Допустим, у нас есть сложный объект, и мы хотели бы преобразовать его в строку, чтобы отправить по сети или
просто вывести для логирования.
Естественно, такая строка должна включать в себя все важные свойства.
Мы могли бы реализовать преобразование следующим образом:
let user = {
name: "John",
age: 30,
toString() {
return `{name: "${[Link]}", age: ${[Link]}}`;
}
};
…Но в процессе разработки добавляются новые свойства, старые свойства переименовываются и удаляются.
Обновление такого toString каждый раз может стать проблемой. Мы могли бы попытаться перебрать свойства в
248/597
нём, но что, если объект сложный, и в его свойствах имеются вложенные объекты? Мы должны были бы осуществить
их преобразование тоже.
К счастью, нет необходимости писать код для обработки всего этого. У задачи есть простое решение.
[Link]
JSON (JavaScript Object Notation) – это общий формат для представления значений и объектов. Его описание
задокументировано в стандарте RFC 4627 . Первоначально он был создан для JavaScript, но многие другие языки
также имеют библиотеки, которые могут работать с ним. Таким образом, JSON легко использовать для обмена
данными, когда клиент использует JavaScript, а сервер написан на Ruby/PHP/Java или любом другом языке.
JavaScript предоставляет методы:
● [Link] для преобразования объектов в JSON.
● [Link] для преобразования JSON обратно в объект.
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
wife: null
};
alert(json);
/* выведет объект в формате JSON:
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"wife": null
}
*/
Например:
249/597
// число в JSON остаётся числом
alert( [Link](1) ) // 1
JSON является независимой от языка спецификацией для данных, поэтому [Link] пропускает некоторые
специфические свойства объектов JavaScript.
А именно:
● Свойства-функции (методы).
● Символьные ключи и значения.
● Свойства, содержащие undefined .
let user = {
sayHi() { // будет пропущено
alert("Hello");
},
[Symbol("id")]: 123, // также будет пропущено
something: undefined // как и это - пропущено
};
Обычно это нормально. Если это не то, чего мы хотим, то скоро мы увидим, как можно настроить этот процесс.
Самое замечательное, что вложенные объекты поддерживаются и конвертируются автоматически.
Например:
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
alert( [Link](meetup) );
/* вся структура преобразована в строку:
{
"title":"Conference",
"room":{"number":23,"participants":["john","ann"]},
}
*/
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
250/597
Здесь преобразование завершается неудачно из-за циклической ссылки: [Link] ссылается на meetup ,
и [Link] ссылается на room :
number: 23
occupiedBy place
title: "Conference"
participants
...
value
Значение для кодирования.
replacer
Массив свойств для кодирования или функция соответствия function(key, value) .
space
Дополнительное пространство (отступы), используемое для форматирования.
В большинстве случаев [Link] используется только с первым аргументом. Но если нам нужно настроить
процесс замены, например, отфильтровать циклические ссылки, то можно использовать второй аргумент
[Link] .
Если мы передадим ему массив свойств, будут закодированы только эти свойства.
Например:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup ссылается на room
};
Здесь мы, наверное, слишком строги. Список свойств применяется ко всей структуре объекта. Так что внутри
participants – пустые объекты, потому что name нет в списке.
Давайте включим в список все свойства, кроме [Link] , из-за которого появляется цикличная ссылка:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup ссылается на room
};
251/597
[Link] = meetup; // room ссылается на meetup
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup ссылается на room
};
Обратите внимание, что функция replacer получает каждую пару ключ/значение, включая вложенные объекты и
элементы массива. И она применяется рекурсивно. Значение this внутри replacer – это объект, который
содержит текущее свойство.
Первый вызов – особенный. Ему передаётся специальный «объект-обёртка»: {"": meetup} . Другими словами,
первая (key, value) пара имеет пустой ключ, а значением является целевой объект в общем. Вот почему первая
строка из примера выше будет ":[object Object]" .
Идея состоит в том, чтобы дать как можно больше возможностей replacer – у него есть возможность
проанализировать и заменить/пропустить даже весь объект целиком, если это необходимо.
Форматирование: space
Третий аргумент в [Link](value, replacer, space) – это количество пробелов, используемых для
удобного форматирования.
Ранее все JSON-форматированные объекты не имели отступов и лишних пробелов. Это нормально, если мы хотим
отправить объект по сети. Аргумент space используется исключительно для вывода в удобочитаемом виде.
Ниже space = 2 указывает JavaScript отображать вложенные объекты в несколько строк с отступом в 2 пробела
внутри объекта:
252/597
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
Третьим аргументом также может быть строка. В этом случае строка будет использоваться для отступа вместо ряда
пробелов.
Параметр space применяется исключительно для логирования и красивого вывода.
Пользовательский «toJSON»
Как и toString для преобразования строк, объект может предоставлять метод toJSON для преобразования в
JSON. [Link] автоматически вызывает его, если он есть.
Например:
let room = {
number: 23
};
let meetup = {
title: "Conference",
date: new Date([Link](2017, 0, 1)),
room
};
alert( [Link](meetup) );
/*
{
"title":"Conference",
"date":"2017-01-01T[Link].000Z", // (1)
"room": {"number":23} // (2)
}
*/
Как видим, date (1) стал строкой. Это потому, что все объекты типа Date имеют встроенный метод toJSON ,
который возвращает такую строку.
Теперь давайте добавим собственную реализацию метода toJSON в наш объект room (2) :
let room = {
number: 23,
253/597
toJSON() {
return [Link];
}
};
let meetup = {
title: "Conference",
room
};
alert( [Link](room) ); // 23
alert( [Link](meetup) );
/*
{
"title":"Conference",
"room": 23
}
*/
Как видите, toJSON используется как при прямом вызове [Link](room) , так и когда room вложен в
другой сериализуемый объект.
[Link]
Синтаксис:
str
JSON для преобразования в объект.
reviver
Необязательная функция, которая будет вызываться для каждой пары (ключ, значение) и может
преобразовывать значение.
Например:
// строковый массив
let numbers = "[0, 1, 2, 3]";
numbers = [Link](numbers);
alert( numbers[1] ); // 1
let user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
user = [Link](user);
alert( [Link][1] ); // 1
JSON может быть настолько сложным, насколько это необходимо, объекты и массивы могут включать другие объекты
и массивы. Но они должны быть в том же JSON-формате.
Вот типичные ошибки в написанном от руки JSON (иногда приходится писать его для отладки):
let json = `{
name: "John", // Ошибка: имя свойства без кавычек
"surname": 'Smith', // Ошибка: одинарные кавычки в значении (должны быть двойными)
'isAdmin': false, // Ошибка: одинарные кавычки в ключе (должны быть двойными)
"birthday": new Date(2000, 2, 3), // Ошибка: не допускается конструктор "new", только значения
254/597
"gender": "male" // Ошибка: отсутствует запятая после непоследнего свойства
"friends": [0,1,2,3], // Ошибка: не должно быть запятой после последнего свойства
}`;
Кроме того, JSON не поддерживает комментарии. Добавление комментария в JSON делает его недействительным.
Существует ещё один формат JSON5 , который поддерживает ключи без кавычек, комментарии и т.д. Но это
самостоятельная библиотека, а не спецификация языка.
Обычный JSON настолько строг не потому, что его разработчики ленивы, а потому, что позволяет легко, надёжно и
очень быстро реализовывать алгоритм кодирования и чтения.
Использование reviver
…А теперь нам нужно десериализовать её, т.е. снова превратить в объект JavaScript.
Давайте сделаем это, вызвав [Link] :
Ой, ошибка!
Значением [Link] является строка, а не Date объект. Как [Link] мог знать, что он должен был
преобразовать эту строку в Date ?
Давайте передадим [Link] функцию восстановления вторым аргументом, которая возвращает все значения
«как есть», но date станет Date :
let schedule = `{
"meetups": [
{"title":"Conference","date":"2017-11-30T[Link].000Z"},
{"title":"Birthday","date":"2017-04-18T[Link].000Z"}
]
}`;
255/597
Итого
●
JSON – это формат данных, который имеет собственный независимый стандарт и библиотеки для большинства
языков программирования.
● JSON поддерживает простые объекты, массивы, строки, числа, логические значения и null .
●
JavaScript предоставляет методы [Link] для сериализации в JSON и [Link] для чтения из JSON.
●
Оба метода поддерживают функции преобразования для интеллектуального чтения/записи.
● Если объект имеет метод toJSON , то он вызывается через [Link] .
Задачи
let user = {
name: "Василий Иванович",
age: 35
};
К решению
В простых случаях циклических ссылок мы можем исключить свойство, из-за которого они возникают, из
сериализации по его имени.
Но иногда мы не можем использовать имя, так как могут быть и другие, нужные, свойства с этим именем во
вложенных объектах. Поэтому можно проверять свойство по значению.
Напишите функцию replacer для JSON-преобразования, которая удалит свойства, ссылающиеся на meetup :
let room = {
number: 23
};
let meetup = {
title: "Совещание",
occupiedBy: [{name: "Иванов"}, {name: "Петров"}],
place: room
};
// цикличные ссылки
[Link] = meetup;
[Link] = meetup;
К решению
256/597
Продвинутая работа с функциями
Рекурсия и стек
Если вы не новичок в программировании, то, возможно, уже знакомы с рекурсией и можете пропустить эту главу.
Рекурсия – это приём программирования, полезный в ситуациях, когда задача может быть естественно разделена на
несколько аналогичных, но более простых задач. Или когда задача может быть упрощена до несложных действий
плюс простой вариант той же задачи. Или, как мы скоро увидим, для работы с определёнными структурами данных.
В процессе выполнения задачи в теле функции могут быть вызваны другие функции для выполнения подзадач.
Частный случай подвызова – когда функция вызывает сама себя. Это как раз и называется рекурсией.
В качестве первого примера напишем функцию pow(x, n) , которая возводит x в натуральную степень n . Иначе
говоря, умножает x на само себя n раз.
pow(2, 2) = 4
pow(2, 3) = 8
pow(2, 4) = 16
function pow(x, n) {
let result = 1;
return result;
}
alert( pow(2, 3) ); // 8
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
alert( pow(2, 3) ); // 8
if n==1 = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)
1. Если n == 1 , тогда всё просто. Эта ветвь называется базой рекурсии, потому что сразу же приводит к очевидному
результату: pow(x, 1) равно x .
257/597
2. Мы можем представить pow(x, n) в виде: x * pow(x, n - 1) . Что в математике записывается как: xn = x
* xn-1 . Эта ветвь – шаг рекурсии: мы сводим задачу к более простому действию (умножение на x ) и более
простой аналогичной задаче ( pow с меньшим n ). Последующие шаги упрощают задачу всё больше и больше,
пока n не достигает 1 .
pow(x,n)
рекурсивный вызов до n==1
Нет
n == 1 ? x * pow(x, n-1)
Да
1. pow(2, 4) = 2 * pow(2, 3)
2. pow(2, 3) = 2 * pow(2, 2)
3. pow(2, 2) = 2 * pow(2, 1)
4. pow(2, 1) = 2
Итак, рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его – к ещё более
простому и так далее, пока значение не станет очевидно.
function pow(x, n) {
return (n == 1) ? x : (x * pow(x, n - 1));
}
Общее количество вложенных вызовов (включая первый) называют глубиной рекурсии. В нашем случае она будет
равна n .
Максимальная глубина рекурсии ограничена движком JavaScript. Точно можно рассчитывать на 10000 вложенных
вызовов, некоторые интерпретаторы допускают и больше, но для большинства из них 100000 вызовов – за пределами
возможностей. Существуют автоматические оптимизации, помогающие избежать переполнения стека вызовов
(«оптимизация хвостовой рекурсии»), но они ещё не поддерживаются везде и работают только для простых случаев.
Это ограничивает применение рекурсии, но она всё равно широко распространена: для решения большого числа
задач рекурсивный способ решения даёт более простой код, который легче поддерживать.
Теперь мы посмотрим, как работают рекурсивные вызовы. Для этого заглянем «под капот» функций.
Информация о процессе выполнения запущенной функции хранится в её контексте выполнения (execution context).
Контекст выполнения – специальная внутренняя структура данных, которая содержит информацию о вызове
функции. Она включает в себя конкретное место в коде, на котором находится интерпретатор, локальные
переменные функции, значение this (мы не используем его в данном примере) и прочую служебную информацию.
Один вызов функции имеет ровно один контекст выполнения, связанный с ним.
Когда функция производит вложенный вызов, происходит следующее:
● Выполнение текущей функции приостанавливается.
● Контекст выполнения, связанный с ней, запоминается в специальной структуре данных – стеке контекстов
выполнения.
258/597
● Выполняются вложенные вызовы, для каждого из которых создаётся свой контекст выполнения.
● После их завершения старый контекст достаётся из стека, и выполнение внешней функции возобновляется с того
места, где она была остановлена.
pow(2, 3)
В начале вызова pow(2, 3) контекст выполнения будет хранить переменные: x = 2, n = 3 , выполнение
находится на первой строке функции.
●
Контекст: { x: 2, n: 3, строка 1 } вызов: pow(2, 3)
Это только начало выполнения функции. Условие n == 1 ложно, поэтому выполнение идёт во вторую ветку if :
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
alert( pow(2, 3) );
Значения переменных те же самые, но выполнение функции перешло к другой строке, актуальный контекст:
●
Контекст: { x: 2, n: 3, строка 5 } вызов: pow(2, 3)
Чтобы вычислить выражение x * pow(x, n - 1) , требуется произвести запуск pow с новыми аргументами
pow(2, 2) .
pow(2, 2)
Для выполнения вложенного вызова JavaScript запоминает текущий контекст выполнения в стеке контекстов
выполнения.
Здесь мы вызываем ту же функцию pow , однако это абсолютно неважно. Для любых функций процесс одинаков:
1. Текущий контекст «запоминается» на вершине стека.
2. Создаётся новый контекст для вложенного вызова.
3. Когда выполнение вложенного вызова заканчивается – контекст предыдущего вызова восстанавливается, и
выполнение соответствующей функции продолжается.
●
Контекст: { x: 2, n: 2, строка 1 } вызов: pow(2, 2)
●
Контекст: { x: 2, n: 3, строка 5 } вызов: pow(2, 3)
Новый контекст выполнения находится на вершине стека (и выделен жирным), а предыдущие запомненные
контексты – под ним.
Когда выполнение подвызова закончится, можно будет легко вернуться назад, потому что контекст сохраняет как
переменные, так и точное место кода, в котором он остановился. Слово «строка» на рисунках условно, на самом
деле запоминается более точное место в цепочке команд.
pow(2, 1)
Процесс повторяется: производится новый вызов в строке 5 , теперь с аргументами x=2 , n=1 .
Создаётся новый контекст выполнения, предыдущий контекст добавляется в стек:
●
Контекст: { x: 2, n: 1, строка 1 } вызов: pow(2, 1)
●
Контекст: { x: 2, n: 2, строка 5 } вызов: pow(2, 2)
●
Контекст: { x: 2, n: 3, строка 5 } вызов: pow(2, 3)
259/597
Теперь в стеке два старых контекста и один текущий для pow(2, 1) .
Выход
При выполнении pow(2, 1) , в отличие от предыдущих запусков, условие n == 1 истинно, поэтому выполняется
первая ветка условия if :
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
●
Контекст: { x: 2, n: 2, строка 5 } вызов: pow(2, 2)
●
Контекст: { x: 2, n: 3, строка 5 } вызов: pow(2, 3)
Возобновляется обработка вызова pow(2, 2) . Имея результат pow(2, 1) , он может закончить свою работу x *
pow(x, n - 1) , вернув 4 .
Восстанавливается контекст предыдущего вызова:
●
Контекст: { x: 2, n: 3, строка 5 } вызов: pow(2, 3)
Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых
в стеке.
Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних
вызовов в стеке, и в данном случае это приводит к тому, что возведение в степень n хранит в памяти n различных
контекстов.
Реализация возведения в степень через цикл гораздо более экономна:
function pow(x, n) {
let result = 1;
return result;
}
Итеративный вариант функции pow использует один контекст, в котором будут последовательно меняться значения
i и result . При этом объём затрачиваемой памяти небольшой, фиксированный и не зависит от n .
Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.
Но переделка рекурсии в цикл может быть нетривиальной, особенно когда в функции в зависимости от условий
используются различные рекурсивные подвызовы, результаты которых объединяются, или когда ветвление более
сложное. Оптимизация может быть ненужной и совершенно нестоящей усилий.
Часто код с использованием рекурсии более короткий, лёгкий для понимания и поддержки. Оптимизация требуется не
везде, как правило, нам важен хороший код, поэтому она и используется.
Рекурсивные обходы
260/597
Представьте, у нас есть компания. Структура персонала может быть представлена как объект:
let company = {
sales: [{
name: 'John',
salary: 1000
}, {
name: 'Alice',
salary: 600
}],
development: {
sites: [{
name: 'Peter',
salary: 2000
}, {
name: 'Alex',
salary: 1800
}],
internals: [{
name: 'Jack',
salary: 1300
}]
}
};
Теперь, допустим, нам нужна функция для получения суммы всех зарплат. Как мы можем это сделать?
Итеративный подход не прост, потому что структура довольно сложная. Первая идея заключается в том, чтобы
сделать цикл for поверх объекта company с вложенным циклом над отделами 1-го уровня вложенности. Но затем
нам нужно больше вложенных циклов для итераций над сотрудниками отделов второго уровня, таких как sites … А
затем ещё один цикл по отделам 3-го уровня, которые могут появиться в будущем? Если мы поместим в код 3-4
вложенных цикла для обхода одного объекта, то это будет довольно некрасиво.
Случай (1), когда мы получили массив, является базой рекурсии, тривиальным случаем.
Случай (2), при получении объекта, является шагом рекурсии. Сложная задача разделяется на подзадачи для
подотделов. Они могут, в свою очередь, снова разделиться на подотделы, но рано или поздно это разделение
закончится, и решение сведётся к случаю (1).
Алгоритм даже проще читается в виде кода:
261/597
function sumSalaries(department) {
if ([Link](department)) { // случай (1)
return [Link]((prev, current) => prev + [Link], 0); // сумма элементов массива
} else { // случай (2)
let sum = 0;
for (let subdep of [Link](department)) {
sum += sumSalaries(subdep); // рекурсивно вызывается для подотделов, суммируя результаты
}
return sum;
}
}
alert(sumSalaries(company)); // 6700
Код краток и прост для понимания (надеюсь?). В этом сила рекурсии. Она работает на любом уровне вложенности
отделов.
Схема вызовов:
Принцип прост: для объекта {...} используются рекурсивные вызовы, а массивы [...] являются «листьями»
дерева рекурсии, они сразу дают результат.
Обратите внимание, что в коде используются возможности, о которых мы говорили ранее:
●
Метод [Link] из главы Методы массивов для получения суммы элементов массива.
● Цикл for(val of [Link](obj)) для итерации по значениям объекта: [Link] возвращает
массив значений.
Рекурсивные структуры
Рекурсивная (рекурсивно определяемая) структура данных – это структура, которая повторяет саму себя в своих
частях.
Мы только что видели это на примере структуры компании выше.
262/597
Для лучшего понимания мы рассмотрим ещё одну рекурсивную структуру под названием «связанный список»,
которая в некоторых случаях может использоваться в качестве альтернативы массиву.
Связанный список
Представьте себе, что мы хотим хранить упорядоченный список объектов.
Естественным выбором будет массив:
…Но у массивов есть недостатки. Операции «удалить элемент» и «вставить элемент» являются дорогостоящими.
Например, операция [Link](obj) должна переиндексировать все элементы, чтобы освободить место для
нового obj , и, если массив большой, на это потребуется время. То же самое с [Link]() .
Пример:
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
Здесь мы можем ещё лучше увидеть, что есть несколько объектов, каждый из которых имеет value и next ,
указывающий на соседа. Переменная list является первым объектом в цепочке, поэтому, следуя по указателям
next из неё, мы можем попасть в любой элемент.
Список можно легко разделить на несколько частей и впоследствии объединить обратно:
263/597
value value
next next
list 1 2 null
value value
next next
secondList 3 4 null
Для объединения:
[Link] = secondList;
Чтобы удалить элемент из середины списка, нужно изменить значение next предыдущего элемента:
[Link] = [Link];
value
next
1
[Link] перепрыгнуло с 1 на значение 2 . Значение 1 теперь исключено из цепочки. Если оно не хранится где-
нибудь ещё, оно будет автоматически удалено из памяти.
264/597
Итого
Термины:
● Рекурсия – это термин в программировании, означающий вызов функцией самой себя. Рекурсивные функции могут
быть использованы для элегантного решения определённых задач.
Когда функция вызывает саму себя, это называется шагом рекурсии. База рекурсии – это такие аргументы
функции, которые делают задачу настолько простой, что решение не требует дальнейших вложенных вызовов.
● Рекурсивно определяемая структура данных – это структура данных, которая может быть определена с
использованием самой себя.
Например, связанный список может быть определён как структура данных, состоящая из объекта, содержащего
ссылку на список (или null).
Деревья, такие как дерево HTML-элементов или дерево отделов из этой главы, также являются рекурсивными: у
них есть ветви, и каждая ветвь может содержать другие ветви.
Как мы видели в примере sumSalary , рекурсивные функции могут быть использованы для прохода по ним.
Любая рекурсивная функция может быть переписана в итеративную. И это иногда требуется для оптимизации
работы. Но для многих задач рекурсивное решение достаточно быстрое и простое в написании и поддержке.
Задачи
Например:
sumTo(1) = 1
sumTo(2) = 2 + 1 = 3
sumTo(3) = 3 + 2 + 1 = 6
sumTo(4) = 4 + 3 + 2 + 1 = 10
...
sumTo(100) = 100 + 99 + ... + 2 + 1 = 5050
1. С использованием цикла.
2. Через рекурсию, т.к. sumTo(n) = n + sumTo(n-1) for n > 1 .
3. С использованием формулы арифметической прогрессии .
К решению
Вычислить факториал
важность: 4
265/597
Факториал натурального числа – это число, умноженное на "себя минус один" , затем на "себя минус два" ,
и так далее до 1 . Факториал n обозначается как n!
n! = n * (n - 1) * (n - 2) * ...*1
1! = 1
2! = 2 * 1 = 2
3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120
К решению
Числа Фибоначчи
важность: 5
Последовательность чисел Фибоначчи определяется формулой Fn = Fn-1 + Fn-2 . То есть, следующее число
получается как сумма двух предыдущих.
Первые два числа равны 1 , затем 2(1+1) , затем 3(1+2) , 5(2+3) и так далее: 1, 1, 2, 3, 5, 8, 13,
21... .
Числа Фибоначчи тесно связаны с золотым сечением и множеством природных явлений вокруг нас.
Пример работы:
alert(fib(3)); // 2
alert(fib(7)); // 13
alert(fib(77)); // 5527939700884757
P.S. Все запуски функций из примера выше должны работать быстро. Вызов fib(77) должен занимать не более
доли секунды.
К решению
Допустим, у нас есть односвязный список (как описано в главе Рекурсия и стек):
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
266/597
}
}
}
};
К решению
Выведите односвязный список из предыдущего задания Вывод односвязного списка в обратном порядке.
К решению
В этой главе мы узнаем, как сделать то же самое с нашими собственными функциями и как передавать таким
функциям параметры в виде массива.
Вызывать функцию можно с любым количеством аргументов независимо от того, как она была определена.
Например:
function sum(a, b) {
return a + b;
}
alert( sum(1, 2, 3, 4, 5) );
Лишние аргументы не вызовут ошибку. Но, конечно, посчитаются только первые два.
Остаточные параметры могут быть обозначены через три точки ... . Буквально это значит: «собери оставшиеся
параметры и положи их в массив».
return sum;
}
alert( sumAll(1) ); // 1
267/597
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
Переменная "arguments"
Все аргументы функции находятся в псевдомассиве arguments под своими порядковыми номерами.
Например:
function showName() {
alert( [Link] );
alert( arguments[0] );
alert( arguments[1] );
Раньше в языке не было остаточных параметров, и получить все аргументы функции можно было только с помощью
arguments . Этот способ всё ещё работает, мы можем найти его в старом коде.
Но у него есть один недостаток. Хотя arguments похож на массив, и его тоже можно перебирать, это всё же не
массив. Он не поддерживает методы массивов, поэтому мы не можем, например, вызвать [Link](...) .
К тому же, arguments всегда содержит все аргументы функции — мы не можем получить их часть. А остаточные
параметры позволяют это сделать.
Соответственно, для более удобной работы с аргументами лучше использовать остаточные параметры.
268/597
Стрелочные функции не имеют "arguments"
Если мы обратимся к arguments из стрелочной функции, то получим аргументы внешней «нормальной»
функции.
Пример:
function f() {
let showArg = () => alert(arguments[0]);
showArg(2);
}
f(1); // 1
Как мы помним, у стрелочных функций нет собственного this . Теперь мы знаем, что нет и своего объекта
arguments .
Оператор расширения
alert( [Link](3, 5, 1) ); // 5
Допустим, у нас есть массив чисел [3, 5, 1] . Как вызвать для него [Link] ?
Просто так их не вставишь — [Link] ожидает получить список чисел, а не один массив.
Конечно, мы можем вводить числа вручную : [Link](arr[0], arr[1], arr[2]) . Но, во-первых, это плохо
выглядит, а, во-вторых, мы не всегда знаем, сколько будет аргументов. Их может быть как очень много, так и не быть
совсем.
И тут нам поможет оператор расширения. Он похож на остаточные параметры – тоже использует ... , но делает
совершенно противоположное.
Когда ...arr используется при вызове функции, он «расширяет» перебираемый объект arr в список аргументов.
Для [Link] :
269/597
Оператор расширения можно использовать и для слияния массивов:
Посмотрим, что происходит. Под капотом оператор расширения использует итераторы, чтобы перебирать элементы.
Так же, как это делает for..of .
Цикл for..of перебирает строку как последовательность символов, поэтому из ...str получается "П", "р",
"и", "в", "е", "т" . Получившиеся символы собираются в массив при помощи стандартного объявления
массива: [...str] .
Для этой задачи мы можем использовать и [Link] . Он тоже преобразует перебираемый объект (такой как
строка) в массив:
Выходит, что если нужно сделать из чего угодно массив, то [Link] — более универсальный метод.
Итого
Когда мы видим "..." в коде, это могут быть как остаточные параметры, так и оператор расширения.
Полезно запомнить:
● Остаточные параметры используются, чтобы создавать новые функции с неопределённым числом аргументов.
● С помощью оператора расширения можно вставить массив в функцию, которая по умолчанию работает с обычным
списком аргументов.
Вместе эти конструкции помогают легко преобразовывать наборы значений в массивы и обратно.
К аргументам функции можно обращаться и по-старому — через псевдомассив arguments .
270/597
Область видимости переменных, замыкание
JavaScript – язык с сильным функционально-ориентированным уклоном. Он даёт нам много свободы. Функция может
быть динамически создана, скопирована в другую переменную или передана как аргумент другой функции и позже
вызвана из совершенно другого места.
Мы знаем, что функция может получить доступ к переменным из внешнего окружения, эта возможность используется
очень часто.
Но что произойдёт, когда внешние переменные изменятся? Функция получит последнее значение или то, которое
существовало на момент создания функции?
И что произойдёт, когда функция переместится в другое место в коде и будет вызвана оттуда – получит ли она доступ
к внешним переменным своего нового местоположения?
Разные языки ведут себя по-разному в таких случаях, и в этой главе мы рассмотрим поведение JavaScript.
Блоки кода
Если переменная объявлена внутри блока кода {...} , то она видна только внутри этого блока.
Например:
{
// выполняем некоторые действия с локальной переменной, которые не должны быть видны снаружи
alert(message); // Hello
}
С помощью блоков {...} мы можем изолировать часть кода, выполняющую свою собственную задачу, с
переменными, принадлежащими только ей:
{
// показать сообщение
let message = "Hello";
alert(message);
}
{
// показать другое сообщение
let message = "Goodbye";
alert(message);
}
271/597
Без блоков была бы ошибка
Обратите внимание, что без отдельных блоков возникнет ошибка, если мы используем let с существующим
именем переменной:
// показать сообщение
let message = "Hello";
alert(message);
Для if , for , while и т.д. переменные, объявленные в блоке кода {...} , также видны только внутри:
if (true) {
let phrase = "Hello";
alert(phrase); // Hello
}
В этом случае после завершения работы if нижний alert не увидит phrase , что и приведет к ошибке.
И это замечательно, поскольку это позволяет нам создавать блочно-локальные переменные, относящиеся только к
ветви if .
Визуально let i = 0; находится вне блока кода {...} , однако здесь в случае с for есть особенность:
переменная, объявленная внутри (...) , считается частью блока.
Вложенные функции
Здесь вложенная функция getFullName() создана для удобства. Она может получить доступ к внешним
переменным и, значит, вывести полное имя. В JavaScript вложенные функции используются очень часто.
Что ещё интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если
внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте.
Не важно где, она всё так же будет иметь доступ к тем же внешним переменным.
272/597
Ниже, makeCounter создает функцию «счётчик», которая при каждом вызове возвращает следующее число:
function makeCounter() {
let count = 0;
return function() {
return count++; // есть доступ к внешней переменной "count"
};
}
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
Несмотря на простоту этого примера, немного модифицированные его варианты применяются на практике, например,
в генераторе псевдослучайных чисел и во многих других случаях.
Как это работает? Если мы создадим несколько таких счётчиков, будут ли они независимыми друг от друга? Что
происходит с переменными?
Понимание таких вещей полезно для повышения общего уровня владения JavaScript и для более сложных
сценариев. Так что давайте немного углубимся.
Лексическое окружение
Шаг 1. Переменные
В JavaScript у каждой выполняемой функции, блока кода {...} и скрипта есть связанный с ними внутренний
(скрытый) объект, называемый лексическим окружением LexicalEnvironment .
Объект лексического окружения состоит из двух частей:
1. Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая
другая информация, такая как значение this ).
2. Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих
фигурных скобок).
«Переменная» – это просто свойство специального внутреннего объекта: Environment Record . «Получить
или изменить переменную», означает, «получить или изменить свойство этого объекта».
Например, в этом простом коде только одно лексическое окружение:
Лексическое Окружение
outer
phrase: "Hello" null
273/597
outer
начало выполнения phrase: <uninitialized> null
phrase: undefined
phrase: "Hello"
phrase: "Bye"
Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе
выполнения кода:
1. При запуске скрипта лексическое окружение предварительно заполняется всеми объявленными переменными.
● Изначально они находятся в состоянии «Uninitialized». Это особое внутреннее состояние, которое означает, что
движок знает о переменной, но на нее нельзя ссылаться, пока она не будет объявлена с помощью let . Это
почти то же самое, как если бы переменная не существовала.
2. Появляется определение переменной let phrase . У неё ещё нет присвоенного значения, поэтому присваивается
undefined . С этого момента мы можем использовать переменную.
3. Переменной phrase присваивается значение.
4. Переменная phrase меняет значение.
Вот, к примеру, начальное состояние глобального лексического окружения при добавлении функции:
outer
phrase: <uninitialized> null
начало выполнения
say: function
...
Конечно, такое поведение касается только Function Declaration, а не Function Expression, в которых мы присваиваем
функцию переменной, например, let say = function(name) {...} .
Например, для say("John") это выглядит так (выполнение находится на строке, отмеченной стрелкой):
274/597
Лексическое Окружение вызова
В процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее
(глобальное):
● Внутреннее лексическое окружение соответствует текущему выполнению say .
В нём находится одна переменная name , аргумент функции. Мы вызываем say("John") , так что значение
переменной name равно "John" .
● Внешнее лексическое окружение – это глобальное лексическое окружение.
В нём находятся переменная phrase и сама функция.
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
В начале каждого вызова makeCounter() создается новый объект лексического окружения, в котором хранятся
переменные для конкретного запуска makeCounter .
Таким образом, мы имеем два вложенных лексических окружения, как в примере выше:
outer outer
count: 0 makeCounter: function null
counter: undefined
275/597
Отличие заключается в том, что во время выполнения makeCounter() создается крошечная вложенная функция,
состоящая всего из одной строки: return count++ . Мы ее еще не запускаем, а только создаем.
Все функции помнят лексическое окружение, в котором они были созданы. Технически здесь нет никакой магии: все
функции имеют скрытое свойство [[Environment]] , которое хранит ссылку на лексическое окружение, в котором
была создана функция:
outer outer
[[Environment]] count: 0 makeCounter: function null
counter: undefined
Таким образом, counter.[[Environment]] имеет ссылку на {count: 0} лексического окружения. Так функция
запоминает, где она была создана, независимо от того, где она вызывается. Ссылка на [[Environment]]
устанавливается один раз и навсегда при создании функции.
Впоследствии, при вызове counter() , для этого вызова создается новое лексическое окружение, а его внешняя
ссылка на лексическое окружение берется из counter.[[Environment]] :
Теперь, когда код внутри counter() ищет переменную count , он сначала ищет ее в собственном лексическом
окружении (пустом, так как там нет локальных переменных), а затем в лексическом окружении внешнего вызова
makeCounter() , где находит count и изменяет ее.
Переменная обновляется в том лексическом окружении, в котором она существует.
Вот состояние после выполнения:
изменено здесь
outer outer makeCounter: function outer
<пусто> count: 1 counter: function null
Если мы вызовем counter() несколько раз, то в одном и том же месте переменная count будет увеличена до 2 ,
3 и т.д.
276/597
Замыкания
В программировании есть общий термин: «замыкание», – который должен знать каждый разработчик.
Замыкание – это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В
некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось
замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями (есть
только одно исключение, про которое будет рассказано в Синтаксис "new Function").
То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства [[Environment]] , и
все они могут получить доступ к внешним переменным.
Когда на собеседовании фронтенд-разработчику задают вопрос: «что такое замыкание?», – правильным ответом
будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и,
может быть, несколько слов о технических деталях: свойстве [[Environment]] и о том, как работает
лексическое окружение.
Сборка мусора
Обычно лексическое окружение удаляется из памяти вместе со всеми переменными после завершения вызова
функции. Это связано с тем, что на него нет ссылок. Как и любой объект JavaScript, оно хранится в памяти только до
тех пор, пока к нему можно обратиться.
Однако если существует вложенная функция, которая все еще доступна после завершения функции, то она имеет
свойство [[Environment]] , ссылающееся на лексическое окружение.
В этом случае лексическое окружение остается доступным даже после завершения работы функции.
Например:
function f() {
let value = 123;
return function() {
alert(value);
}
}
Обратите внимание, что если f() вызывается много раз и результирующие функции сохраняются, то все
соответствующие объекты лексического окружения также будут сохранены в памяти. В приведенном ниже коде – все
три:
function f() {
let value = [Link]();
Объект лексического окружения исчезает, когда становится недоступным (как и любой другой объект). Другими
словами, он существует только до тех пор, пока на него ссылается хотя бы одна вложенная функция.
В приведенном ниже коде после удаления вложенной функции ее окружающее лексическое окружение (а значит, и
value ) очищается из памяти:
function f() {
let value = 123;
return function() {
alert(value);
}
}
277/597
let g = f(); // пока существует функция g, value остается в памяти
Оптимизация на практике
Как мы видели, в теории, пока функция жива, все внешние переменные тоже сохраняются.
Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и,
если легко по коду понять, что внешняя переменная не используется – она удаляется.
Одним из важных побочных эффектов в V8 (Chrome, Edge, Opera) является то, что такая переменная
становится недоступной при отладке.
Попробуйте запустить следующий пример в Chrome с открытой Developer Tools.
Когда код будет поставлен на паузу, напишите в консоли alert(value) .
function f() {
let value = [Link]();
function g() {
debugger; // в консоли: напишите alert(value); Такой переменной нет!
}
return g;
}
let g = f();
g();
Как вы можете видеть – такой переменной не существует! В теории, она должна быть доступна, но попала под
оптимизацию движка.
Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них – мы можем
увидеть не ту внешнюю переменную при совпадающих названиях:
function f() {
let value = "ближайшее значение";
function g() {
debugger; // в консоли: напишите alert(value); Сюрприз!
}
return g;
}
let g = f();
g();
Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Edge/Opera, рано или поздно вы с ней
столкнётесь.
Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится. Вы всегда можете проверить
это, запустив примеры на этой странице.
Задачи
Функция sayHi использует имя внешней переменной. Какое значение будет использоваться при выполнении
функции?
278/597
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
Такие ситуации встречаются как при разработке для браузера, так и для сервера. Функция может быть назначена на
выполнение позже, чем она была создана, например, после действия пользователя или сетевого запроса.
К решению
Приведенная ниже функция makeWorker создает другую функцию и возвращает ее. Эта новая функция может быть
вызвана из другого места.
Будет ли она иметь доступ к внешним переменным из места своего создания, или из места вызова, или из обоих
мест?
function makeWorker() {
let name = "Pete";
return function() {
alert(name);
};
}
// создаём функцию
let work = makeWorker();
// вызываем её
work(); // что будет показано?
К решению
Независимы ли счётчики?
важность: 5
Здесь мы делаем два счётчика: counter и counter2 , используя одну и ту же функцию makeCounter .
Они независимы? Что покажет второй счётчик? 0,1 или 2,3 или что-то ещё?
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter2() ); // ?
alert( counter2() ); // ?
279/597
К решению
Объект счётчика
важность: 5
function Counter() {
let count = 0;
[Link] = function() {
return ++count;
};
[Link] = function() {
return --count;
};
}
alert( [Link]() ); // ?
alert( [Link]() ); // ?
alert( [Link]() ); // ?
К решению
Функция внутри if
важность: 5
Обратите внимание: результат зависит от режима выполнения кода. Здесь используется строгий режим "use
strict" .
if (true) {
let user = "John";
function sayHi() {
alert(`${phrase}, ${user}`);
}
}
sayHi();
К решению
Да, именно таким образом, используя двойные круглые скобки (не опечатка).
Например:
sum(1)(2) = 3
sum(5)(-1) = 4
К решению
280/597
Видна ли переменная?
важность: 4
let x = 1;
function func() {
[Link](x); // ?
let x = 2;
}
func();
К решению
У нас есть встроенный метод [Link](f) для массивов. Он фильтрует все элементы с помощью функции f .
Если она возвращает true , то элемент добавится в возвращаемый массив.
Например:
К решению
Сортировать по полю
важность: 5
let users = [
{ name: "Иван", age: 20, surname: "Иванов" },
{ name: "Пётр", age: 18, surname: "Петров" },
{ name: "Анна", age: 19, surname: "Каренина" }
];
281/597
// по возрасту (Пётр, Анна, Иван)
[Link]((a, b) => [Link] > [Link] ? 1 : -1);
[Link](byField('name'));
[Link](byField('age'));
К решению
Армия функций
важность: 5
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = function() { // функция shooter
alert( i ); // должна выводить порядковый номер
};
[Link](shooter); // и добавлять стрелка в массив
i++;
}
К решению
В самой первой главе про переменные мы ознакомились с тремя способами объявления переменных:
1. let
282/597
2. const
3. var
let и const ведут себя одинаково по отношению к лексическому окружению, области видимости.
Но var – это совершенно другой зверь, берущий своё начало с давних времён. Обычно var не используется в
современных скриптах, но всё ещё может скрываться в старых.
Если в данный момент вы не работаете с подобными скриптами, вы можете пропустить или отложить прочтение
данной главы, однако, есть шанс, что вы столкнётесь с var в будущем.
function sayHi() {
var phrase = "Привет"; // локальная переменная, "var" вместо "let"
alert(phrase); // Привет
}
sayHi();
Область видимости переменных var ограничивается либо функцией, либо, если переменная глобальная, то
скриптом. Такие переменные доступны за пределами блока.
Например:
if (true) {
var test = true; // используем var вместо let
}
if (true) {
let test = true; // используем let
}
Аналогично для циклов: var не может быть блочной или локальной внутри цикла:
alert(i); // 10, переменная i доступна вне цикла, т.к. является глобальной переменной
Если блок кода находится внутри функции, то var становится локальной переменной в этой функции:
function sayHi() {
if (true) {
var phrase = "Привет";
}
283/597
sayHi();
alert(phrase); // Ошибка: phrase не определена (видна в консоли разработчика)
Как мы видим, var выходит за пределы блоков if , for и подобных. Это происходит потому, что на заре развития
JavaScript блоки кода не имели лексического окружения. Поэтому можно сказать, что var – это пережиток прошлого.
Если в блоке кода дважды объявить одну и ту же переменную let , будет ошибка:
let user;
let user; // SyntaxError: 'user' has already been declared
Используя var , можно переобъявлять переменную сколько угодно раз. Повторные var игнорируются:
alert(user); // Пётр
alert(user); // Иван
Объявления переменных var обрабатываются в начале выполнения функции (или запуска скрипта, если
переменная является глобальной).
Другими словами, переменные var считаются объявленными с самого начала исполнения функции вне зависимости
от того, в каком месте функции реально находятся их объявления (при условии, что они не находятся во вложенной
функции).
Т.е. этот код:
function sayHi() {
phrase = "Привет";
alert(phrase);
var phrase;
}
sayHi();
…Технически полностью эквивалентен следующему (объявление переменной var phrase перемещено в начало
функции):
function sayHi() {
var phrase;
phrase = "Привет";
alert(phrase);
}
sayHi();
284/597
function sayHi() {
phrase = "Привет"; // (*)
if (false) {
var phrase;
}
alert(phrase);
}
sayHi();
Это поведение называется «hoisting» (всплытие, поднятие), потому что все объявления переменных var
«всплывают» в самый верх функции.
В примере выше if (false) условие никогда не выполнится. Но это никаким образом не препятствует созданию
переменной var phrase , которая находится внутри него, поскольку объявления var «всплывают» в начало
функции. Т.е. в момент присвоения значения (*) переменная уже существует.
Объявления переменных «всплывают», но присваивания значений – нет.
function sayHi() {
alert(phrase);
sayHi();
Объявление переменной обрабатывается в начале выполнения функции («всплывает»), однако присвоение значения
всегда происходит в той строке кода, где оно указано. Т.е. код выполняется по следующему сценарию:
function sayHi() {
var phrase; // объявление переменной срабатывает вначале...
alert(phrase); // undefined
sayHi();
Поскольку все объявления переменных var обрабатываются в начале функции, мы можем ссылаться на них в
любом месте. Однако, переменные имеют значение undefined до строки с присвоением значения.
В обоих примерах выше вызов alert происходил без ошибки, потому что переменная phrase уже существовала.
Но её значение ещё не было присвоено, поэтому мы получали undefined .
IIFE
В прошлом, поскольку существовал только var , а он не имел блочной области видимости, программисты придумали
способ её эмулировать. Этот способ получил название «Immediately-invoked function expressions» (сокращенно IIFE).
Это не то, что мы должны использовать сегодня, но, так как вы можете встретить это в старых скриптах, полезно
понимать принцип работы.
IIFE выглядит следующим образом:
(function() {
285/597
alert(message); // Привет
})();
Здесь создаётся и немедленно вызывается Function Expression. Так что код выполняется сразу же и у него есть свои
локальные переменные.
Function Expression обёрнуто в скобки (function {...}) , потому что, когда JavaScript встречает "function" в
основном потоке кода, он воспринимает это как начало Function Declaration. Но у Function Declaration должно быть
имя, так что такой код вызовет ошибку:
alert(message); // Привет
}();
Даже если мы скажем: «хорошо, давайте добавим имя», – это не сработает, потому что JavaScript не позволяет
вызывать Function Declaration немедленно.
Так что скобки вокруг функции – это трюк, который позволяет объяснить JavaScript, что функция была создана в
контексте другого выражения, а значит, что это Function Expression: ей не нужно имя и её можно вызвать немедленно.
Помимо круглых скобок существуют и другие способы сообщить JavaScript, что мы имеем в виду Function Expression:
(function() {
alert("Круглые скобки вокруг функции");
})();
(function() {
alert("Круглые скобки вокруг всего выражения");
}());
!function() {
alert("Выражение начинается с логического оператора НЕ");
}();
+function() {
alert("Выражение начинается с унарного плюса");
}();
Во всех перечисленных случаях мы объявляем Function Expression и немедленно запускаем его. Ещё раз отметим: в
настоящее время необходимости писать подобный код нет.
Итого
Есть ещё одно небольшое отличие, относящееся к глобальному объекту, мы рассмотрим его в следующей главе.
Эти особенности, как правило, не очень хорошо влияют на код. Блочная область видимости – это удобно. Поэтому
много лет назад let и const были введены в стандарт и сейчас являются основным способом объявления
286/597
переменных.
Глобальный объект
Глобальный объект предоставляет переменные и функции, доступные в любом месте программы. По умолчанию это
те, что встроены в язык или среду исполнения.
В браузере он называется window , в [Link] — global , в другой среде исполнения может называться иначе.
Недавно globalThis был добавлен в язык как стандартизированное имя для глобального объекта, которое должно
поддерживаться в любом окружении. Он поддерживается во всех основных браузерах.
Далее мы будем использовать window , полагая, что наша среда – браузер. Если скрипт может выполняться и в
другом окружении, лучше будет globalThis .
Ко всем свойствам глобального объекта можно обращаться напрямую:
alert("Привет");
// это то же самое, что и
[Link]("Привет");
В браузере глобальные функции и переменные, объявленные с помощью var (не let/const !), становятся
свойствами глобального объекта:
var gVar = 5;
То же самое касается функций, объявленных с помощью синтаксиса Function Declaration (выражения с ключевым
словом function в основном потоке кода, не Function Expression)
Пожалуйста, не полагайтесь на это. Такое поведение поддерживается для совместимости. В современных проектах,
использующих JavaScript-модули, такого не происходит.
let gLet = 5;
Если свойство настолько важное, что вы хотите сделать его доступным для всей программы, запишите его в
глобальный объект напрямую:
// сделать информацию о текущем пользователе глобальной, для предоставления доступа всем скриптам
[Link] = {
name: "John"
};
При этом обычно не рекомендуется использовать глобальные переменные. Следует применять их как можно реже.
Дизайн кода, при котором функция получает входные параметры и выдаёт определённый результат, чище, надёжнее
и удобнее для тестирования, чем когда используются внешние, а тем более глобальные переменные.
Глобальный объект можно использовать, чтобы проверить поддержку современных возможностей языка.
Например, проверить наличие встроенного объекта Promise (такая поддержка отсутствует в очень старых
браузерах):
287/597
if (![Link]) {
alert("Ваш браузер очень старый!");
}
Если нет (скажем, используется старый браузер), мы можем создать полифил: добавить функции, которые не
поддерживаются окружением, но существуют в современном стандарте.
if (![Link]) {
[Link] = ... // собственная реализация современной возможности языка
}
Итого
● Глобальный объект хранит переменные, которые должны быть доступны в любом месте программы.
Это включает в себя как встроенные объекты, например, Array , так и характерные для окружения свойства,
например, [Link] – высота окна браузера.
● Глобальный объект имеет универсальное имя – globalThis .
…Но чаще на него ссылаются по-старому, используя имя, характерное для данного окружения, такое как window
(браузер) и global ([Link]).
● Следует хранить значения в глобальном объекте, только если они действительно глобальны для нашего проекта. И
стараться свести их количество к минимуму.
● В браузерах, если только мы не используем модули, глобальные функции и переменные, объявленные с помощью
var , становятся свойствами глобального объекта.
●
Для того, чтобы код был проще и в будущем его легче было поддерживать, следует обращаться к свойствам
глобального объекта напрямую, как window.x .
Можно представить функцию как «объект, который может делать какое-то действие». Функции можно не только
вызывать, но и использовать их как обычные объекты: добавлять/удалять свойства, передавать их по ссылке и т.д.
Свойство «name»
function sayHi() {
alert("Hi");
}
alert([Link]); // sayHi
Что довольно забавно, логика назначения name весьма умная. Она присваивает корректное имя даже в случае,
когда функция создаётся без имени и тут же присваивается, вот так:
288/597
function f(sayHi = function() {}) {
alert([Link]); // sayHi (работает!)
}
f();
В спецификации это называется «контекстное имя»: если функция не имеет name, то JavaScript пытается определить
его из контекста.
Также имена имеют и методы объекта:
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
alert([Link]); // sayHi
alert([Link]); // sayBye
В этом нет никакой магии. Бывает, что корректное имя определить невозможно. В таких случаях свойство name имеет
пустое значение. Например:
Свойство «length»
Ещё одно встроенное свойство «length» содержит количество параметров функции в её объявлении. Например:
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert([Link]); // 1
alert([Link]); // 2
alert([Link]); // 2
Как мы видим, троеточие, обозначающее «остаточные параметры», здесь как бы «не считается»
Свойство length иногда используется для интроспекций в функциях, которые работают с другими функциями.
Например, в коде ниже функция ask принимает в качестве параметров вопрос question и произвольное
количество функций-обработчиков ответа handler .
Когда пользователь отвечает на вопрос, функция вызывает обработчики. Мы можем передать два типа обработчиков:
● Функцию без аргументов, которая будет вызываться только в случае положительного ответа.
● Функцию с аргументами, которая будет вызываться в обоих случаях и возвращать ответ.
289/597
function ask(question, ...handlers) {
let isYes = confirm(question);
Это частный случай так называемого Ad-hoc-полиморфизма – обработка аргументов в зависимости от их типа или,
как в нашем случае – от значения length . Эта идея имеет применение в библиотеках JavaScript.
Пользовательские свойства
function sayHi() {
alert("Hi");
sayHi(); // Hi
sayHi(); // Hi
Иногда свойства функции могут использоваться вместо замыканий. Например, мы можем переписать функцию-
счётчик из главы Область видимости переменных, замыкание, используя её свойство:
function makeCounter() {
// вместо
// let count = 0
function counter() {
return [Link]++;
};
[Link] = 0;
return counter;
}
290/597
Это хуже или лучше, чем использовать замыкание?
Основное отличие в том, что если значение count живёт во внешней переменной, то оно не доступно для внешнего
кода. Изменить его могут только вложенные функции. А если оно присвоено как свойство функции, то мы можем его
получить:
function makeCounter() {
function counter() {
return [Link]++;
};
[Link] = 0;
return counter;
}
[Link] = 10;
alert( counter() ); // 10
Named Function Expression или NFE – это термин для Function Expression, у которого есть имя.
Есть две важные особенности имени func , ради которого оно даётся:
1. Оно позволяет функции ссылаться на себя же.
2. Оно не доступно за пределами функции.
Например, ниже функция sayHi вызывает себя с "Guest" , если не передан параметр who :
291/597
func("Guest"); // использует func, чтобы снова вызвать себя же
}
};
Почему мы используем func ? Почему просто не использовать sayHi для вложенного вызова?
Вообще, обычно мы можем так поступить:
Однако, у этого кода есть проблема, которая заключается в том, что значение sayHi может быть изменено. Функция
может быть присвоена другой переменной, и тогда код начнёт выдавать ошибки:
Так происходит, потому что функция берёт sayHi из внешнего лексического окружения. Так как локальная
переменная sayHi отсутствует, используется внешняя. И на момент вызова эта внешняя sayHi равна null .
Необязательное имя, которое можно вставить в Function Expression, как раз и призвано решать такого рода
проблемы.
Давайте используем его, чтобы исправить наш код:
Теперь всё работает, потому что имя "func" локальное и находится внутри функции. Теперь оно взято не снаружи
(и недоступно оттуда). Спецификация гарантирует, что оно всегда будет ссылаться на текущую функцию.
Внешний код все ещё содержит переменные sayHi и welcome , но теперь func – это «внутреннее имя функции»,
таким образом она может вызвать себя изнутри.
292/597
Это не работает с Function Declaration
Трюк с «внутренним» именем, описанный выше, работает только для Function Expression и не работает для
Function Declaration. Для Function Declaration синтаксис не предусматривает возможность объявить
дополнительное «внутреннее» имя.
Зачастую, когда нам нужно надёжное «внутреннее» имя, стоит переписать Function Declaration на Named Function
Expression.
Итого
Если функция объявлена как Function Expression (вне основного потока кода) и имеет имя, тогда это называется
Named Function Expression (Именованным Функциональным Выражением). Это имя может быть использовано для
ссылки на себя же, для рекурсивных вызовов и т.п.
Также функции могут содержать дополнительные свойства. Многие известные JavaScript-библиотеки искусно
используют эту возможность.
Они создают «основную» функцию и добавляют множество «вспомогательных» функций внутрь первой. Например,
библиотека jQuery создаёт функцию с именем $ . Библиотека lodash создаёт функцию _ , а потом добавляет в
неё _.clone , _.keyBy и другие свойства (чтобы узнать о ней побольше см. документацию ). Они делают это,
чтобы уменьшить засорение глобального пространства имён посредством того, что одна библиотека предоставляет
только одну глобальную переменную, уменьшая вероятность конфликта имён.
Таким образом, функция может не только делать что-то сама по себе, но также и предоставлять полезную
функциональность через свои свойства.
Задачи
Измените код makeCounter() так, чтобы счётчик мог уменьшать и устанавливать значение:
P.S. Для того, чтобы сохранить текущее значение счётчика, можно воспользоваться как замыканием, так и свойством
функции. Или сделать два варианта решения: и так, и так.
К решению
sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
293/597
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15
P.S. Подсказка: возможно вам стоит сделать особый метод преобразования в примитив для функции.
К решению
Существует ещё один вариант объявления функции. Он используется крайне редко, но иногда другого решения не
найти.
Синтаксис
Это проще понять на конкретном примере. Здесь объявлена функция с двумя аргументами:
alert( sum(1, 2) ); // 3
А вот функция без аргументов, в этом случае достаточно указать только тело:
sayHi(); // Hello
Главное отличие от других способов объявления функции, которые были рассмотрены ранее, заключается в том, что
функция создаётся полностью «на лету» из строки, переданной во время выполнения.
Все предыдущие объявления требовали от нас, программистов, писать объявление функции в скрипте.
Но new Function позволяет превратить любую строку в функцию. Например, можно получить новую функцию с
сервера и затем выполнить её:
Это используется в очень специфических случаях, например, когда мы получаем код с сервера для динамической
компиляции функции из шаблона, в сложных веб-приложениях.
Замыкание
Обычно функция запоминает, где родилась, в специальном свойстве [[Environment]] . Это ссылка на лексическое
окружение (Lexical Environment), в котором она создана (мы разбирали это в главе Область видимости переменных,
замыкание).
Но когда функция создаётся с использованием new Function , в её [[Environment]] записывается ссылка не на
внешнее лексическое окружение, в котором она была создана, а на глобальное. Поэтому такая функция имеет доступ
только к глобальным переменным.
function getFunc() {
let value = "test";
294/597
let func = new Function('alert(value)');
return func;
}
function getFunc() {
let value = "test";
return func;
}
Эта особенность new Function выглядит странно, но оказывается очень полезной на практике.
Представьте, что нужно создать функцию из строки. Код этой функции неизвестен во время написания скрипта
(поэтому не используем обычные функции), а будет определён только в процессе выполнения. Мы можем получить
код с сервера или с другого ресурса.
Наша новая функция должна взаимодействовать с основным скриптом.
Что если бы она имела доступ к внешним переменным?
Проблема в том, что перед отправкой JavaScript-кода на реальные работающие проекты код сжимается с помощью
минификатора – специальной программы, которая уменьшает размер кода, удаляя комментарии, лишние пробелы,
и, что самое главное, локальным переменным даются укороченные имена.
Например, если в функции объявляется переменная let userName , то минификатор изменяет её на let a (или
другую букву, если она не занята) и изменяет её везде. Обычно так делать безопасно, потому что переменная
является локальной, и никто снаружи не имеет к ней доступ. И внутри функции минификатор заменяет каждое её
упоминание. Минификаторы достаточно умные. Они не просто осуществляют «тупой» поиск-замену, они анализируют
структуру кода, и поэтому ничего не ломается.
Так что если бы даже new Function и имела доступ к внешним переменным, она не смогла бы найти
переименованную userName .
Если бы new Function имела доступ к внешним переменным, при этом были бы проблемы с
минификаторами.
Кроме того, такой код был бы архитектурно хуже и более подвержен ошибкам.
Чтобы передать что-то в функцию, созданную как new Function , можно использовать её аргументы.
Итого
Синтаксис:
По историческим причинам аргументы также могут быть объявлены через запятую в одной строке.
Эти 3 объявления ниже эквивалентны:
Функции, объявленные через new Function , имеют [[Environment]] , ссылающийся на глобальное лексическое
окружение, а не на родительское. Поэтому они не могут использовать внешние локальные переменные. Но это очень
хорошо, потому что страхует нас от ошибок. Переданные явно параметры – гораздо лучшее архитектурное решение,
которое не вызывает проблем у минификаторов.
295/597
Планирование: setTimeout и setInterval
Мы можем вызвать функцию не в данный момент, а позже, через заданный интервал времени. Это называется
«планирование вызова».
Для этого существуют два метода:
● setTimeout позволяет вызвать функцию один раз через определённый интервал времени.
●
setInterval позволяет вызывать функцию регулярно, повторяя вызов через определённый интервал времени.
Эти методы не являются частью спецификации JavaScript. Но большинство сред выполнения JS-кода имеют
внутренний планировщик и предоставляют доступ к этим методам. В частности, они поддерживаются во всех
браузерах и [Link].
setTimeout
Синтаксис:
Параметры:
func|code
Функция или строка кода для выполнения. Обычно это функция. По историческим причинам можно передать и строку
кода, но это не рекомендуется.
delay
Задержка перед запуском в миллисекундах (1000 мс = 1 с). Значение по умолчанию – 0.
arg1 , arg2 …
Аргументы, передаваемые в функцию
function sayHi() {
alert('Привет');
}
setTimeout(sayHi, 1000);
С аргументами:
setTimeout("alert('Привет')", 1000);
296/597
Передавайте функцию, но не запускайте её
Начинающие разработчики иногда ошибаются, добавляя скобки () после функции:
// неправильно!
setTimeout(sayHi(), 1000);
Это не работает, потому что setTimeout ожидает ссылку на функцию. Здесь sayHi() запускает выполнение
функции, и результат выполнения отправляется в setTimeout . В нашем случае результатом выполнения
sayHi() является undefined (так как функция ничего не возвращает), поэтому ничего не планируется.
В коде ниже планируем вызов функции и затем отменяем его (просто передумали). В результате ничего не
происходит:
clearTimeout(timerId);
alert(timerId); // тот же идентификатор (не принимает значение null после отмены)
Как мы видим из вывода alert , в браузере идентификатором таймера является число. В других средах это может
быть что-то ещё. Например, [Link] возвращает объект таймера с дополнительными методами.
Повторюсь, что нет единой спецификации на эти методы, поэтому такое поведение является нормальным.
Для браузеров таймеры описаны в разделе таймеров стандарта HTML5.
setInterval
Все аргументы имеют такое же значение. Но отличие этого метода от setTimeout в том, что функция запускается
не один раз, а периодически через указанный интервал времени.
Чтобы остановить дальнейшее выполнение функции, необходимо вызвать clearInterval(timerId) .
Следующий пример выводит сообщение каждые 2 секунды. Через 5 секунд вывод прекращается:
297/597
Во время показа alert время тоже идёт
В большинстве браузеров, включая Chrome и Firefox, внутренний счётчик продолжает тикать во время показа
alert/confirm/prompt .
Так что если вы запустите код выше и подождёте с закрытием alert несколько секунд, то следующий alert
будет показан сразу, как только вы закроете предыдущий. Интервал времени между сообщениями alert будет
короче, чем 2 секунды.
Вложенный setTimeout
/** вместо:
let timerId = setInterval(() => alert('tick'), 2000);
*/
Метод setTimeout выше планирует следующий вызов прямо после окончания текущего (*) .
Вложенный setTimeout – более гибкий метод, чем setInterval . С его помощью последующий вызов может быть
задан по-разному в зависимости от результатов предыдущего.
Например, необходимо написать сервис, который отправляет запрос для получения данных на сервер каждые 5
секунд, но если сервер перегружен, то необходимо увеличить интервал запросов до 10, 20, 40 секунд… Вот
псевдокод:
}, delay);
А если функции, которые мы планируем, ресурсоёмкие и требуют времени, то мы можем измерить время,
затраченное на выполнение, и спланировать следующий вызов раньше или позже.
Вложенный setTimeout позволяет задать задержку между выполнениями более точно, чем setInterval .
Сравним два фрагмента кода. Первый использует setInterval :
let i = 1;
setInterval(function() {
func(i);
}, 100);
let i = 1;
setTimeout(function run() {
func(i);
setTimeout(run, 100);
}, 100);
298/597
Для setInterval внутренний планировщик будет выполнять func(i) каждые 100 мс:
Обратили внимание?
Реальная задержка между вызовами func с помощью setInterval меньше, чем указано в коде!
Это нормально, потому что время, затраченное на выполнение func , использует часть заданного интервала
времени.
Вполне возможно, что выполнение func будет дольше, чем мы ожидали, и займёт более 100 мс.
В данном случае движок ждёт окончания выполнения func и затем проверяет планировщик и, если время истекло,
немедленно запускает его снова.
В крайнем случае, если функция всегда выполняется дольше, чем задержка delay , то вызовы будут выполняться
без задержек вообще.
100 100
Для setInterval функция остаётся в памяти до тех пор, пока не будет вызван clearInterval .
Есть и побочный эффект. Функция ссылается на внешнее лексическое окружение, поэтому пока она существует,
внешние переменные существуют тоже. Они могут занимать больше памяти, чем сама функция. Поэтому, если
регулярный вызов функции больше не нужен, то лучше отменить его, даже если функция очень маленькая.
alert("Привет");
299/597
Первая строка помещает вызов в «календарь» через 0 мс. Но планировщик проверит «календарь» только после того,
как текущий код завершится. Поэтому "Привет" выводится первым, а "Мир" – после него.
Есть и более продвинутые случаи использования нулевой задержки в браузерах, которые мы рассмотрим в главе
Событийный цикл: микрозадачи и макрозадачи.
setTimeout(function run() {
[Link]([Link]() - start); // запоминаем задержку от предыдущего вызова
// пример вывода:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Первый таймер запускается сразу (как и указано в спецификации), а затем задержка вступает в игру, и мы видим
9, 15, 20, 24... .
Итого
Всё это может увеличивать минимальный интервал срабатывания таймера (и минимальную задержку) до 300 или
даже 1000 мс в зависимости от браузера и настроек производительности ОС.
300/597
Задачи
Напишите функцию printNumbers(from, to) , которая выводит число каждую секунду, начиная от from и
заканчивая to .
1. Используя setInterval .
2. Используя рекурсивный setTimeout .
К решению
В приведённом ниже коде запланирован вызов setTimeout , а затем выполняется сложное вычисление, для
завершения которого требуется более 100 мс.
1. После цикла.
2. Перед циклом.
3. В начале цикла.
let i = 0;
К решению
Прозрачное кеширование
Представим, что у нас есть функция slow(x) , выполняющая ресурсоёмкие вычисления, но возвращающая
стабильные результаты. Другими словами, для одного и того же x она всегда возвращает один и тот же результат.
Если функция вызывается часто, то, вероятно, мы захотим кешировать (запоминать) возвращаемые ею результаты,
чтобы сэкономить время на повторных вычислениях.
function slow(x) {
// здесь могут быть ресурсоёмкие вычисления
alert(`Called with ${x}`);
301/597
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if ([Link](x)) { // если кеш содержит такой x,
return [Link](x); // читаем из него результат
}
slow = cachingDecorator(slow);
В коде выше cachingDecorator – это декоратор, специальная функция, которая принимает другую функцию и
изменяет её поведение.
Идея состоит в том, что мы можем вызвать cachingDecorator с любой функцией, в результате чего мы получим
кеширующую обёртку. Это здорово, т.к. у нас может быть множество функций, использующих такую
функциональность, и всё, что нам нужно сделать – это применить к ним cachingDecorator .
Отделяя кеширующий код от основного кода, мы также сохраняем чистоту и простоту последнего.
Результат вызова cachingDecorator(func) является «обёрткой», т.е. function(x) «оборачивает» вызов
func(x) в кеширующую логику:
обёртка
вокруг функции
С точки зрения внешнего кода, обёрнутая функция slow по-прежнему делает то же самое. Обёртка всего лишь
добавляет к её поведению аспект кеширования.
Подводя итог, можно выделить несколько преимуществ использования отдельной cachingDecorator вместо
изменения кода самой slow :
● Функцию cachingDecorator можно использовать повторно. Мы можем применить её к другой функции.
● Логика кеширования является отдельной, она не увеличивает сложность самой slow (если таковая была).
● При необходимости мы можем объединить несколько декораторов (речь об этом пойдёт позже).
302/597
},
slow(x) {
// здесь может быть страшно тяжёлая задача для процессора
alert("Called with " + x);
return x * [Link](); // (*)
}
};
Ошибка возникает в строке (*) . Функция пытается получить доступ к [Link] и завершается с ошибкой.
Видите почему?
Причина в том, что в строке (**) декоратор вызывает оригинальную функцию как func(x) , и она в данном случае
получает this = undefined .
Мы бы наблюдали похожую ситуацию, если бы попытались запустить:
Т.е. декоратор передаёт вызов оригинальному методу, но без контекста. Следовательно – ошибка.
Давайте это исправим.
Существует специальный встроенный метод функции [Link](context, …args) , который позволяет вызывать
функцию, явно устанавливая this .
Синтаксис:
Он запускает функцию func , используя первый аргумент как её контекст this , а последующие – как её аргументы.
func(1, 2, 3);
[Link](obj, 1, 2, 3)
Они оба вызывают func с аргументами 1 , 2 и 3 . Единственное отличие состоит в том, что [Link] ещё и
устанавливает this равным obj .
Например, в приведённом ниже коде мы вызываем sayHi в контексте различных объектов: [Link](user)
запускает sayHi , передавая this=user , а следующая строка устанавливает this=admin :
function sayHi() {
alert([Link]);
}
303/597
// используем 'call' для передачи различных объектов в качестве 'this'
[Link]( user ); // John
[Link]( admin ); // Admin
function say(phrase) {
alert([Link] + ': ' + phrase);
}
В нашем случае мы можем использовать call в обёртке для передачи контекста в исходную функцию:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * [Link](); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if ([Link](x)) {
return [Link](x);
}
let result = [Link](this, x); // теперь 'this' передаётся правильно
[Link](x, result);
return result;
};
}
Теперь давайте сделаем cachingDecorator ещё более универсальным. До сих пор он работал только с
функциями с одним аргументом.
Как же кешировать метод с несколькими аргументами [Link] ?
let worker = {
slow(min, max) {
return min + max; // здесь может быть тяжёлая задача
}
};
304/597
// будет кешировать вызовы с одинаковыми аргументами
[Link] = cachingDecorator([Link]);
Для многих практических применений третий вариант достаточно хорош, поэтому мы будем придерживаться его.
Также нам понадобится заменить [Link](this, x) на [Link](this, ...arguments) , чтобы
передавать все аргументы обёрнутой функции, а не только первый.
Вот более мощный cachingDecorator :
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
[Link](key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
305/597
[Link](context, args)
Он выполняет func , устанавливая this=context и принимая в качестве списка аргументов псевдомассив args .
Единственная разница в синтаксисе между call и apply состоит в том, что call ожидает список аргументов, в то
время как apply принимает псевдомассив.
Так что эти вызовы дополняют друг друга. Для перебираемых объектов сработает call , а где мы ожидаем
псевдомассив – apply .
А если у нас объект, который и то, и другое, например, реальный массив, то технически мы могли бы использовать
любой метод, но apply , вероятно, будет быстрее, потому что большинство движков JavaScript внутренне
оптимизируют его лучше.
Передача всех аргументов вместе с контекстом другой функции называется «перенаправлением вызова» (call
forwarding).
Простейший вид такого перенаправления:
При вызове wrapper из внешнего кода его не отличить от вызова исходной функции.
Заимствование метода
function hash(args) {
return args[0] + ',' + args[1];
}
На данный момент она работает только для двух аргументов. Было бы лучше, если бы она могла склеить любое
количество args .
function hash(args) {
return [Link]();
}
…К сожалению, это не сработает, потому что мы вызываем hash(arguments) , а объект arguments является
перебираемым и псевдомассивом, но не реальным массивом.
Таким образом, вызов join для него потерпит неудачу, что мы и можем видеть ниже:
function hash() {
alert( [Link]() ); // Ошибка: [Link] не является функцией
}
hash(1, 2);
306/597
Тем не менее, есть простой способ использовать соединение массива:
function hash() {
alert( [].[Link](arguments) ); // 1,2
}
hash(1, 2);
Мы берём (заимствуем) метод join из обычного массива [].join . И используем [].[Link] , чтобы
выполнить его в контексте arguments .
Почему это работает?
Это связано с тем, что внутренний алгоритм встроенного метода [Link](glue) очень прост. Взято из
спецификации практически «как есть»:
1. Пускай первым аргументом будет glue или, в случае отсутствия аргументов, им будет запятая ","
2. Пускай result будет пустой строкой "" .
3. Добавить this[0] к result .
4. Добавить glue и this[1] .
5. Добавить glue и this[2] .
6. …выполнять до тех пор, пока [Link] элементов не будет склеено.
7. Вернуть result .
Таким образом, технически он принимает this и объединяет this[0] , this[1] … и т.д. вместе. Он намеренно
написан так, что допускает любой псевдомассив this (не случайно, многие методы следуют этой практике). Вот
почему он также работает с this=arguments .
Итого
Декоратор – это обёртка вокруг функции, которая изменяет поведение последней. Основная работа по-прежнему
выполняется функцией.
Обычно безопасно заменить функцию или метод декорированным, за исключением одной мелочи. Если исходная
функция предоставляет свойства, такие как [Link] или типа того, то декорированная функция их не
предоставит. Потому что это обёртка. Так что нужно быть осторожным в их использовании. Некоторые декораторы
предоставляют свои собственные свойства.
Декораторы можно рассматривать как «дополнительные возможности» или «аспекты», которые можно добавить в
функцию. Мы можем добавить один или несколько декораторов. И всё это без изменения кода оригинальной
функции!
Мы также рассмотрели пример заимствования метода, когда мы вызываем метод у объекта в контексте другого
объекта. Весьма распространено заимствовать методы массива и применять их к arguments . В качестве
альтернативы можно использовать объект с остаточными параметрами ...args , который является реальным
массивом.
На практике декораторы используются для самых разных задач. Проверьте, насколько хорошо вы их освоили, решая
задачи этой главы.
307/597
Задачи
Декоратор-шпион
важность: 5
Создайте декоратор spy(func) , который должен возвращать обёртку, которая сохраняет все вызовы функции в
своём свойстве calls .
Например:
function work(a, b) {
alert( a + b ); // произвольная функция или метод
}
work = spy(work);
work(1, 2); // 3
work(4, 5); // 9
P.S.: Этот декоратор иногда полезен для юнит-тестирования. Его расширенная форма – [Link] – содержится в
библиотеке [Link] .
К решению
Задерживающий декоратор
важность: 5
Создайте декоратор delay(f, ms) , который задерживает каждый вызов f на ms миллисекунд. Например:
function f(x) {
alert(x);
}
// создаём обёртки
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);
В приведённом выше коде f – функция с одним аргументом, но ваше решение должно передавать все аргументы и
контекст this .
К решению
Декоратор debounce
важность: 5
Результат декоратора debounce(f, ms) – это обёртка, которая откладывает вызовы f , пока не пройдёт ms
миллисекунд бездействия (без вызовов, «cooldown period»), а затем вызывает f один раз с последними
аргументами.
308/597
Другими словами, debounce – это так называемый секретарь, который принимает «телефонные звонки», и ждёт,
пока не пройдет ms миллисекунд тишины. И только после этого передает «начальнику» информацию о последнем
звонке (вызывает непосредственно f ).
Затем, если обёрнутая функция вызывается в 0, 200 и 500 мс, а потом вызовов нет, то фактическая f будет вызвана
только один раз, в 1500 мс. То есть: по истечению 1000 мс от последнего вызова.
после 1000мс
c
1000мс
время
0 200мс 500мс 1500мс
вызовы: f(a) f(b) f(c)
f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// Обёрнутая в debounce функция ждёт 1000 мс после последнего вызова, а затем запускает: alert("c")
Теперь практический пример. Предположим, пользователь набирает какой-то текст, и мы хотим отправить запрос на
сервер, когда ввод этого текста будет завершён.
Нет смысла отправлять запрос для каждого набранного символа. Вместо этого мы хотели бы подождать, а затем
обработать весь результат.
В браузере мы можем настроить обработчик событий – функцию, которая вызывается при каждом изменении поля
для ввода. Обычно обработчик событий вызывается очень часто, для каждого набранного символа. Но если мы
воспользуемся debounce на 1000мс, то он будет вызван только один раз, через 1000мс после последнего ввода
символа.
Таким образом, debounce – это отличный способ обработать последовательность событий: будь то
последовательность нажатий клавиш, движений мыши или ещё что-либо.
Он ждёт заданное время после последнего вызова, а затем запускает свою функцию, которая может обработать
результат.
К решению
● debounce запускает функцию один раз после периода «бездействия». Подходит для обработки конечного
результата.
309/597
● throttle запускает функцию не чаще, чем указанное время ms . Подходит для регулярных обновлений, которые
не должны быть слишком частыми.
Другими словами, throttle похож на секретаря, который принимает телефонные звонки, но при этом беспокоит
начальника (вызывает непосредственно f ) не чаще, чем один раз в ms миллисекунд.
Давайте рассмотрим реальное применение, чтобы лучше понять это требование и выяснить, откуда оно взято.
В браузере мы можем реализовать функцию, которая будет запускаться при каждом перемещении указателя и
получать его местоположение. Во время активного использования мыши эта функция запускается очень часто, что-то
около 100 раз в секунду (каждые 10 мс). Мы бы хотели обновлять некоторую информацию на странице при
передвижении указателя.
…Но функция обновления update() слишком ресурсоёмкая, чтобы делать это при каждом микродвижении. Да и нет
смысла делать обновление чаще, чем один раз в 1000 мс.
Поэтому мы обернём вызов в декоратор: будем использовать throttle(update, 1000) как функцию, которая
будет запускаться при каждом перемещении указателя вместо оригинальной update() . Декоратор будет
вызываться часто, но передавать вызов в update() максимум раз в 1000 мс.
1. Для первого движения указателя декорированный вариант сразу передаёт вызов в update . Это важно, т.к.
пользователь сразу видит нашу реакцию на его перемещение.
2. Затем, когда указатель продолжает движение, в течение 1000 мс ничего не происходит. Декорированный вариант
игнорирует вызовы.
3. По истечению 1000 мс происходит ещё один вызов update с последними координатами.
4. Затем, наконец, указатель где-то останавливается. Декорированный вариант ждёт, пока не истечёт 1000 мс, и
затем вызывает update с последними координатами. В итоге окончательные координаты указателя тоже
обработаны.
Пример кода:
function f(a) {
[Link](a)
}
f1000(1); // показывает 1
f1000(2); // (ограничение, 1000 мс ещё нет)
f1000(3); // (ограничение, 1000 мс ещё нет)
P.S. Аргументы и контекст this , переданные в f1000 , должны быть переданы в оригинальную f .
К решению
310/597
Потеря «this»
Мы уже видели примеры потери this . Как только метод передаётся отдельно от объекта – this теряется.
Вот как это может произойти в случае с setTimeout :
let user = {
firstName: "Вася",
sayHi() {
alert(`Привет, ${[Link]}!`);
}
};
При запуске этого кода мы видим, что вызов [Link] возвращает не «Вася», а undefined !
Это произошло потому, что setTimeout получил функцию sayHi отдельно от объекта user (именно здесь
функция и потеряла контекст). То есть последняя строка может быть переписана как:
let f = [Link];
setTimeout(f, 1000); // контекст user потеряли
Метод setTimeout в браузере имеет особенность: он устанавливает this=window для вызова функции (в [Link]
this становится объектом таймера, но здесь это не имеет значения). Таким образом, для [Link] он
пытается получить [Link] , которого не существует. В других подобных случаях this обычно просто
становится undefined .
Задача довольно типичная – мы хотим передать метод объекта куда-то ещё (в этом конкретном случае – в
планировщик), где он будет вызван. Как бы сделать так, чтобы он вызывался в правильном контексте?
Самый простой вариант решения – это обернуть вызов в анонимную функцию, создав замыкание:
let user = {
firstName: "Вася",
sayHi() {
alert(`Привет, ${[Link]}!`);
}
};
setTimeout(function() {
[Link](); // Привет, Вася!
}, 1000);
Теперь код работает корректно, так как объект user достаётся из замыкания, а затем вызывается его метод sayHi .
То же самое, только короче:
let user = {
firstName: "Вася",
sayHi() {
alert(`Привет, ${[Link]}!`);
}
};
311/597
// ...в течение 1 секунды
user = { sayHi() { alert("Другой пользователь в 'setTimeout'!"); } };
В современном JavaScript у функций есть встроенный метод bind , который позволяет зафиксировать this .
Результатом вызова [Link](context) является особый «экзотический объект» (термин взят из спецификации),
который вызывается как функция и прозрачно передаёт вызов в func , при этом устанавливая this=context .
Другими словами, вызов boundFunc подобен вызову func с фиксированным this .
Например, здесь funcUser передаёт вызов в func , фиксируя this=user :
let user = {
firstName: "Вася"
};
function func() {
alert([Link]);
}
let user = {
firstName: "Вася"
};
function func(phrase) {
alert(phrase + ', ' + [Link]);
}
funcUser("Привет"); // Привет, Вася (аргумент "Привет" передан, при этом this = user)
let user = {
firstName: "Вася",
sayHi() {
alert(`Привет, ${[Link]}!`);
}
};
312/597
В строке (*) мы берём метод [Link] и привязываем его к user . Теперь sayHi – это «связанная» функция,
которая может быть вызвана отдельно или передана в setTimeout (контекст всегда будет правильным).
Здесь мы можем увидеть, что bind исправляет только this , а аргументы передаются как есть:
let user = {
firstName: "Вася",
say(phrase) {
alert(`${phrase}, ${[Link]}!`);
}
};
Некоторые JS-библиотеки предоставляют встроенные функции для удобной массовой привязки контекста,
например _.bindAll(obj) в lodash.
Частичное применение
function mul(a, b) {
return a * b;
}
function mul(a, b) {
return a * b;
}
Вызов [Link](null, 2) создаёт новую функцию double , которая передаёт вызов mul , фиксируя null как
контекст, и 2 – как первый аргумент. Следующие аргументы передаются как есть.
313/597
Это называется частичное применение – мы создаём новую функцию, фиксируя некоторые из существующих
параметров.
Обратите внимание, что в данном случае мы на самом деле не используем this . Но для bind это обязательный
параметр, так что мы должны передать туда что-нибудь вроде null .
В следующем коде функция triple умножает значение на три:
function mul(a, b) {
return a * b;
}
В других случаях частичное применение полезно, когда у нас есть очень общая функция и для удобства мы хотим
создать её более специализированный вариант.
Например, у нас есть функция send(from, to, text) . Потом внутри объекта user мы можем захотеть
использовать её частный вариант: sendTo(to, text) , который отправляет текст от имени текущего пользователя.
Что если мы хотим зафиксировать некоторые аргументы, но не контекст this ? Например, для метода объекта.
Встроенный bind не позволяет этого. Мы не можем просто опустить контекст и перейти к аргументам.
К счастью, легко создать вспомогательную функцию partial , которая привязывает только аргументы.
Вот так:
// использование:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${[Link]}: ${phrase}!`);
}
};
[Link]("Hello");
// Что-то вроде этого:
// [10:00] John: Hello!
Результатом вызова partial(func[, arg1, arg2...]) будет обёртка (*) , которая вызывает func с:
● Тем же this , который она получает (для вызова [Link] – это будет user )
●
Затем передаёт ей ...argsBound – аргументы из вызова partial ( "10:00" )
● Затем передаёт ей ...args – аргументы, полученные обёрткой ( "Hello" )
Благодаря оператору расширения ... реализовать это очень легко, не правда ли?
Также есть готовый вариант _.partial из библиотеки lodash.
314/597
Итого
Метод bind возвращает «привязанный вариант» функции func , фиксируя контекст this и первые аргументы
arg1 , arg2 …, если они заданы.
Обычно bind применяется для фиксации this в методе объекта, чтобы передать его в качестве колбэка.
Например, для setTimeout .
Когда мы привязываем аргументы, такая функция называется «частично применённой» или «частичной».
Частичное применение удобно, когда мы не хотим повторять один и тот же аргумент много раз. Например, если у нас
есть функция send(from, to) и from всё время будет одинаков для нашей задачи, то мы можем создать
частично применённую функцию и дальше работать с ней.
Задачи
function f() {
alert( this ); // ?
}
let user = {
g: [Link](null)
};
user.g();
К решению
Повторный bind
важность: 5
function f() {
alert([Link]);
}
f();
К решению
В свойство функции записано значение. Изменится ли оно после применения bind ? Обоснуйте ответ.
function sayHi() {
alert( [Link] );
}
[Link] = 5;
315/597
К решению
Вызов askPassword() в приведённом ниже коде должен проверить пароль и затем вызвать
[Link]/loginFail в зависимости от ответа.
Исправьте выделенную строку, чтобы всё работало (других строк изменять не надо).
let user = {
name: 'Вася',
loginOk() {
alert(`${[Link]} logged in`);
},
loginFail() {
alert(`${[Link]} failed to log in`);
},
};
askPassword([Link], [Link]);
К решению
Это задание является немного усложнённым вариантом одного из предыдущих – Исправьте функцию, теряющую
"this".
Объект user был изменён. Теперь вместо двух функций loginOk/loginFail у него есть только одна –
[Link](true/false) .
Что нужно передать в вызов функции askPassword в коде ниже, чтобы она могла вызывать функцию
[Link](true) как ok и функцию [Link](false) как fail ?
let user = {
name: 'John',
login(result) {
alert( [Link] + (result ? ' logged in' : ' failed to log in') );
}
};
askPassword(?, ?); // ?
К решению
316/597
Повторяем стрелочные функции
Как мы помним из главы Методы объекта, "this", у стрелочных функций нет this . Если происходит обращение к
this , его значение берётся снаружи.
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
showList() {
[Link](
student => alert([Link] + ': ' + student)
);
}
};
[Link]();
Здесь внутри forEach использована стрелочная функция, таким образом [Link] в ней будет иметь точно
такое же значение, как в методе showList : [Link] .
Если бы мы использовали «обычную» функцию, была бы ошибка:
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
showList() {
[Link](function(student) {
// Error: Cannot read property 'title' of undefined
alert([Link] + ': ' + student)
});
}
};
[Link]();
Ошибка возникает потому, что forEach по умолчанию выполняет функции с this , равным undefined , и в итоге
мы пытаемся обратиться к [Link] .
Это не влияет на стрелочные функции, потому что у них просто нет this .
317/597
Стрелочные функции VS bind
Существует тонкая разница между стрелочной функцией => и обычной функцией, вызванной с .bind(this) :
● .bind(this) создаёт «связанную версию» функции.
●
Стрелка => ничего не привязывает. У функции просто нет this . При получении значения this – оно, как
обычная переменная, берётся из внешнего лексического окружения.
Это отлично подходит для декораторов, когда нам нужно пробросить вызов с текущими this и arguments .
Например, defer(f, ms) принимает функцию и возвращает обёртку над ней, которая откладывает вызов на ms
миллисекунд:
function sayHi(who) {
alert('Hello, ' + who);
}
Здесь мы были вынуждены создать дополнительные переменные args и ctx , чтобы функция внутри setTimeout
могла получить их.
Итого
Стрелочные функции:
● Не имеют this .
●
Не имеют arguments .
● Не могут быть вызваны с new .
● (У них также нет super , но мы про это не говорили. Про это будет в главе Наследование классов).
Всё это потому, что они предназначены для небольшого кода, который не имеет своего «контекста», выполняясь в
текущем. И они отлично справляются с этой задачей!
318/597
До этого момента мы рассматривали свойство только как пару «ключ-значение». Но на самом деле свойство объекта
гораздо мощнее и гибче.
В этой главе мы изучим дополнительные флаги конфигурации для свойств, а в следующей – увидим, как можно
незаметно превратить их в специальные функции – геттеры и сеттеры.
Флаги свойств
Помимо значения value , свойства объекта имеют три специальных атрибута (так называемые «флаги»).
●
writable – если true , свойство можно изменить, иначе оно только для чтения.
● enumerable – если true , свойство перечисляется в циклах, в противном случае циклы его игнорируют.
● configurable – если true , свойство можно удалить, а эти атрибуты можно изменять, иначе этого делать
нельзя.
Мы ещё не встречали эти атрибуты, потому что обычно они скрыты. Когда мы создаём свойство «обычным
способом», все они имеют значение true . Но мы можем изменить их в любое время.
Сначала посмотрим, как получить их текущие значения.
Метод [Link] позволяет получить полную информацию о свойстве.
Его синтаксис:
obj
Объект, из которого мы получаем информацию.
propertyName
Имя свойства.
Возвращаемое значение – это объект, так называемый «дескриптор свойства»: он содержит значение свойства и все
его флаги.
Например:
let user = {
name: "John"
};
Его синтаксис:
obj , propertyName
Объект и его свойство, для которого нужно применить дескриптор.
descriptor
Применяемый дескриптор.
319/597
Если свойство существует, defineProperty обновит его флаги. В противном случае метод создаёт новое свойство
с указанным значением и флагами; если какой-либо флаг не указан явно, ему присваивается значение false .
Например, здесь создаётся свойство name , все флаги которого имеют значение false :
[Link](user, "name", {
value: "John"
});
Сравните это с предыдущим примером, в котором мы создали свойство [Link] «обычным способом»: в этот раз
все флаги имеют значение false . Если это не то, что нам нужно, надо присвоить им значения true в параметре
descriptor .
Сделаем свойство [Link] доступным только для чтения. Для этого изменим флаг writable :
let user = {
name: "John"
};
[Link](user, "name", {
writable: false
});
[Link] = "Pete"; // Ошибка: Невозможно изменить доступное только для чтения свойство 'name'
Теперь никто не сможет изменить имя пользователя, если только не обновит соответствующий флаг новым вызовом
defineProperty .
let user = { };
[Link](user, "name", {
value: "John",
// для нового свойства необходимо явно указывать все флаги, для которых значение true
enumerable: true,
configurable: true
});
alert([Link]); // John
[Link] = "Pete"; // Ошибка
320/597
Неперечислимое свойство
let user = {
name: "John",
toString() {
return [Link];
}
};
Если мы этого не хотим, можно установить для свойства enumerable:false . Тогда оно перестанет появляться в
цикле for..in аналогично встроенному toString :
let user = {
name: "John",
toString() {
return [Link];
}
};
[Link](user, "toString", {
enumerable: false
});
alert([Link](user)); // name
Неконфигурируемое свойство
321/597
Мы также не можем изменить writable :
Определение свойства как неконфигурируемого – это дорога в один конец. Мы не можем изменить его обратно с
помощью defineProperty .
Обратите внимание: configurable: false не даст изменить флаги свойства, а также не даст его удалить.
При этом можно изменить значение свойства.
В коде ниже свойство [Link] является неконфигурируемым, но мы все ещё можем изменить его значение (т.к.
writable: true ).
let user = {
name: "John"
};
[Link](user, "name", {
configurable: false
});
let user = {
name: "John"
};
[Link](user, "name", {
writable: false,
configurable: false
});
Метод [Link]
Существует метод [Link](obj, descriptors) , который позволяет определять множество свойств сразу.
Его синтаксис:
[Link](obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
Например:
322/597
[Link](user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
[Link]
Вместе с [Link] этот метод можно использовать для клонирования объекта вместе с его
флагами:
Обычно при клонировании объекта мы используем присваивание, чтобы скопировать его свойства:
…Но это не копирует флаги. Так что если нам нужен клон «получше», предпочтительнее использовать
[Link] .
Другое отличие в том, что for..in игнорирует символьные и неперечислимые свойства, а
[Link] возвращает дескрипторы всех свойств.
[Link](obj)
Запрещает добавлять новые свойства в объект.
[Link](obj)
Запрещает добавлять/удалять свойства. Устанавливает configurable: false для всех существующих свойств.
[Link](obj)
Запрещает добавлять/удалять/изменять свойства. Устанавливает configurable: false, writable: false для
всех существующих свойств.
[Link](obj)
Возвращает false , если добавление свойств запрещено, иначе true .
[Link](obj)
Возвращает true , если добавление/удаление свойств запрещено и для всех существующих свойств установлено
configurable: false .
[Link](obj)
Возвращает true , если добавление/удаление/изменение свойств запрещено, и для всех текущих свойств
установлено configurable: false, writable: false .
323/597
Свойства - геттеры и сеттеры
Второй тип свойств мы ещё не рассматривали. Это свойства-аксессоры (accessor properties). По своей сути это
функции, которые используются для присвоения и получения значения, но во внешнем коде они выглядят как
обычные свойства объекта.
Геттеры и сеттеры
Свойства-аксессоры представлены методами: «геттер» – для чтения и «сеттер» – для записи. При литеральном
объявлении объекта они обозначаются get и set :
let obj = {
get propName() {
// геттер, срабатывает при чтении [Link]
},
set propName(value) {
// сеттер, срабатывает при записи [Link] = value
}
};
let user = {
name: "John",
surname: "Smith"
};
А теперь добавим свойство объекта fullName для полного имени, которое в нашем случае "John Smith" . Само
собой, мы не хотим дублировать уже имеющуюся информацию, так что реализуем его при помощи аксессора:
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${[Link]} ${[Link]}`;
}
};
Снаружи свойство-аксессор выглядит как обычное свойство. В этом и заключается смысл свойств-аксессоров. Мы не
вызываем [Link] как функцию, а читаем как обычное свойство: геттер выполнит всю работу за кулисами.
На данный момент у fullName есть только геттер. Если мы попытаемся назначить [Link]= , произойдёт
ошибка:
let user = {
get fullName() {
return `...`;
}
};
324/597
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${[Link]} ${[Link]}`;
},
set fullName(value) {
[[Link], [Link]] = [Link](" ");
}
};
alert([Link]); // Alice
alert([Link]); // Cooper
Например, для создания аксессора fullName при помощи defineProperty мы можем передать дескриптор с
использованием get и set :
let user = {
name: "John",
surname: "Smith"
};
[Link](user, 'fullName', {
get() {
return `${[Link]} ${[Link]}`;
},
set(value) {
[[Link], [Link]] = [Link](" ");
}
});
Ещё раз заметим, что свойство объекта может быть либо свойством-аксессором (с методами get/set ), либо
свойством-данным (со значением value ).
При попытке указать и get , и value в одном дескрипторе будет ошибка:
value: 2
});
325/597
Умные геттеры/сеттеры
Геттеры/сеттеры можно использовать как обёртки над «реальными» значениями свойств, чтобы получить больше
контроля над операциями с ними.
Например, если мы хотим запретить устанавливать короткое имя для user , мы можем использовать сеттер name
для проверки, а само значение хранить в отдельном свойстве _name :
let user = {
get name() {
return this._name;
},
set name(value) {
if ([Link] < 4) {
alert("Имя слишком короткое, должно быть более 4 символов");
return;
}
this._name = value;
}
};
[Link] = "Pete";
alert([Link]); // Pete
Таким образом, само имя хранится в _name , доступ к которому производится через геттер и сеттер.
Технически, внешний код всё ещё может получить доступ к имени напрямую с помощью user._name , но существует
широко известное соглашение о том, что свойства, которые начинаются с символа "_" , являются внутренними, и к
ним не следует обращаться из-за пределов объекта.
У аксессоров есть интересная область применения – они позволяют в любой момент взять «обычное» свойство и
изменить его поведение, поменяв на геттер и сеттер.
Например, представим, что мы начали реализовывать объект user , используя свойства-данные имя name и
возраст age :
alert( [Link] ); // 25
…Но рано или поздно всё может измениться. Взамен возраста age мы можем решить хранить дату рождения
birthday , потому что так более точно и удобно:
326/597
function User(name, birthday) {
[Link] = name;
[Link] = birthday;
Теперь старый код тоже работает, и у нас есть отличное дополнительное свойство!
Прототипы, наследование
Прототипное наследование
[[Prototype]]
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]] (так оно названо в спецификации),
которое либо равно null , либо ссылается на другой объект. Этот объект называется «прототип»:
прототип object
[[Prototype]]
object
Прототип даёт нам немного «магии». Когда мы хотим прочитать свойство из object , а оно отсутствует, JavaScript
автоматически берёт его из прототипа. В программировании такой механизм называется «прототипным
наследованием». Многие интересные возможности языка и техники программирования основываются на нём.
Свойство [[Prototype]] является внутренним и скрытым, но есть много способов задать его.
Одним из них является использование __proto__ , например так:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
Если мы ищем свойство в rabbit , а оно отсутствует, JavaScript автоматически берёт его из animal .
Например:
let animal = {
eats: true
327/597
};
let rabbit = {
jumps: true
};
animal
eats: true
[[Prototype]]
rabbit
jumps: true
Здесь мы можем сказать, что « animal является прототипом rabbit » или « rabbit прототипно наследует от
animal ».
Так что если у animal много полезных свойств и методов, то они автоматически становятся доступными у rabbit .
Такие свойства называются «унаследованными».
Если у нас есть метод в animal , он может быть вызван на rabbit :
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
animal
eats: true
walk: function
[[Prototype]]
rabbit
jumps: true
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
328/597
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
animal
eats: true
walk: function
[[Prototype]]
rabbit
jumps: true
[[Prototype]]
longEar
earLength: 10
Теперь, если мы прочтём что-нибудь из longEar , и оно будет отсутствовать, JavaScript будет искать его в rabbit , а
затем в animal .
Есть только два ограничения:
1. Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить __proto__ по кругу.
2. Значение __proto__ может быть объектом или null . Другие типы игнорируются.
Это вполне очевидно, но всё же: может быть только один [[Prototype]] . Объект не может наследоваться от двух
других объектов.
Свойство __proto__ немного устарело, оно существует по историческим причинам. Современный JavaScript
предполагает, что мы должны использовать функции [Link]/[Link]
вместо того, чтобы получать/устанавливать прототип. Мы также рассмотрим эти функции позже.
По спецификации __proto__ должен поддерживаться только браузерами, но по факту все среды, включая
серверную, поддерживают его. Так что мы вполне безопасно его используем.
Далее мы будем в примерах использовать __proto__ , так как это самый короткий и интуитивно понятный
способ установки и чтения прототипа.
let animal = {
eats: true,
walk() {
/* этот метод не будет использоваться в rabbit */
}
};
let rabbit = {
__proto__: animal
};
329/597
[Link] = function() {
alert("Rabbit! Bounce-bounce!");
};
Теперь вызов [Link]() находит метод непосредственно в объекте и выполняет его, не используя прототип:
animal
eats: true
walk: function
[[Prototype]]
rabbit
walk: function
Свойства-аксессоры – исключение, так как запись в него обрабатывается функцией-сеттером. То есть это фактически
вызов функции.
По этой причине [Link] работает корректно в приведённом ниже коде:
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[[Link], [Link]] = [Link](" ");
},
get fullName() {
return `${[Link]} ${[Link]}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
// срабатывает сеттер!
[Link] = "Alice Cooper"; // (**)
alert([Link]); // Alice
alert([Link]); // Cooper
Здесь в строке (*) свойство [Link] имеет геттер в прототипе user , поэтому вызывается он. В строке
(**) свойство также имеет сеттер в прототипе, который и будет вызван.
Значение «this»
В приведённом выше примере может возникнуть интересный вопрос: каково значение this внутри set
fullName(value) ? Куда записаны свойства [Link] и [Link] : в user или в admin ?
Это на самом деле очень важная деталь, потому что у нас может быть большой объект со множеством методов, от
которого можно наследовать. Затем наследующие объекты могут вызывать его методы, но они будут изменять своё
состояние, а не состояние объекта-родителя.
Например, здесь animal представляет собой «хранилище методов», и rabbit использует его.
Вызов [Link]() устанавливает [Link] для объекта rabbit :
330/597
// методы animal
let animal = {
walk() {
if (![Link]) {
alert(`I walk`);
}
},
sleep() {
[Link] = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// модифицирует [Link]
[Link]();
alert([Link]); // true
alert([Link]); // undefined (нет такого свойства в прототипе)
Картинка с результатом:
animal
walk: function
sleep: function
[[Prototype]]
rabbit
Если бы у нас были другие объекты, такие как bird , snake и т.д., унаследованные от animal , они также получили
бы доступ к методам animal . Но this при вызове каждого метода будет соответствовать объекту (перед точкой),
на котором происходит вызов, а не animal . Поэтому, когда мы записываем данные в this , они сохраняются в этих
объектах.
В результате методы являются общими, а состояние объекта — нет.
Цикл for…in
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
Если унаследованные свойства нам не нужны, то мы можем отфильтровать их при помощи встроенного метода
[Link](key) : он возвращает true , если у obj есть собственное, не унаследованное, свойство с
именем key .
Пример такой фильтрации:
331/597
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
В этом примере цепочка наследования выглядит так: rabbit наследует от animal , который наследует от
[Link] (так как animal – литеральный объект {...} , то это по умолчанию), а затем null на самом
верху:
null
[[Prototype]]
[Link]
toString: function
hasOwnProperty: function
...
[[Prototype]]
animal
eats: true
[[Prototype]]
rabbit
jumps: true
Заметим ещё одну деталь. Откуда взялся метод [Link] ? Мы его явно не определяли. Если
посмотреть на цепочку прототипов, то видно, что он берётся из [Link] . То есть он
унаследован.
…Но почему hasOwnProperty не появляется в цикле for..in в отличие от eats и jumps ? Он ведь перечисляет
все унаследованные свойства.
Ответ простой: оно не перечислимо. То есть у него внутренний флаг enumerable стоит false , как и у других
свойств [Link] . Поэтому оно и не появляется в цикле.
Итого
● В JavaScript все объекты имеют скрытое свойство [[Prototype]] , которое является либо другим объектом, либо
null .
● Мы можем использовать obj.__proto__ для доступа к нему (исторически обусловленный геттер/сеттер, есть
другие способы, которые скоро будут рассмотрены).
● Объект, на который ссылается [[Prototype]] , называется «прототипом».
● Если мы хотим прочитать свойство obj или вызвать метод, которого не существует у obj , тогда JavaScript
попытается найти его в прототипе.
●
Операции записи/удаления работают непосредственно с объектом, они не используют прототип (если это обычное
свойство, а не сеттер).
332/597
●
Если мы вызываем [Link]() , а метод при этом взят из прототипа, то this всё равно ссылается на obj .
Таким образом, методы всегда работают с текущим объектом, даже если они наследуются.
● Цикл for..in перебирает как свои, так и унаследованные свойства. Остальные методы получения ключей/
значений работают только с собственными свойствами объекта.
Задачи
Работа с прототипами
важность: 5
let animal = {
jumps: null
};
let rabbit = {
__proto__: animal,
jumps: true
};
delete [Link];
delete [Link];
К решению
Алгоритм поиска
важность: 5
let head = {
glasses: 1
};
let table = {
pen: 3
};
let bed = {
sheet: 1,
pillow: 2
};
let pockets = {
money: 2000
};
1. С помощью свойства __proto__ задайте прототипы так, чтобы поиск любого свойства выполнялся по
следующему пути: pockets → bed → table → head . Например, [Link] должно возвращать значение
3 (найденное в table ), а [Link] – значение 1 (найденное в head ).
2. Ответьте на вопрос: как быстрее получить значение glasses – через [Link] или через
[Link] ? При необходимости составьте цепочки поиска и сравните их.
333/597
К решению
Какой объект получит свойство full при вызове [Link]() : animal или rabbit ?
let animal = {
eat() {
[Link] = true;
}
};
let rabbit = {
__proto__: animal
};
[Link]();
К решению
У нас есть два хомяка: шустрый ( speedy ) и ленивый ( lazy ); оба наследуют от общего объекта hamster .
Когда мы кормим одного хомяка, второй тоже наедается. Почему? Как это исправить?
let hamster = {
stomach: [],
eat(food) {
[Link](food);
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
К решению
[Link]
Как мы помним, новые объекты могут быть созданы с помощью функции-конструктора new F() .
Если в [Link] содержится объект, оператор new устанавливает его в качестве [[Prototype]] для нового
объекта.
334/597
На заметку:
JavaScript использовал прототипное наследование с момента своего появления. Это одна из основных
особенностей языка.
Но раньше, в старые времена, прямого доступа к прототипу объекта не было. Надёжно работало только свойство
"prototype" функции-конструктора, описанное в этой главе. Поэтому оно используется во многих скриптах.
Обратите внимание, что [Link] означает обычное свойство с именем "prototype" для F . Это ещё не
«прототип объекта», а обычное свойство F с таким именем.
Приведём пример:
let animal = {
eats: true
};
function Rabbit(name) {
[Link] = name;
}
[Link] = animal;
Установка [Link] = animal буквально говорит интерпретатору следующее: «При создании объекта
через new Rabbit() запиши ему animal в [[Prototype]] ».
Результат будет выглядеть так:
Rabbit animal
prototype
eats: true
[[Prototype]]
rabbit
У каждой функции (за исключением стрелочных) по умолчанию уже есть свойство "prototype" .
По умолчанию "prototype" – объект с единственным свойством constructor , которое ссылается на функцию-
конструктор.
Вот такой:
function Rabbit() {}
/* прототип по умолчанию
[Link] = { constructor: Rabbit };
*/
335/597
Rabbit "prototype" по умолчанию
prototype
constructor
Проверим это:
function Rabbit() {}
// по умолчанию:
// [Link] = { constructor: Rabbit }
Соответственно, если мы ничего не меняем, то свойство constructor будет доступно всем кроликам через
[[Prototype]] :
function Rabbit() {}
// по умолчанию:
// [Link] = { constructor: Rabbit }
constructor
[[Prototype]]
rabbit
function Rabbit(name) {
[Link] = name;
alert(name);
}
Это удобно, когда у нас есть объект, но мы не знаем, какой конструктор использовался для его создания (например,
он мог быть взят из сторонней библиотеки), а нам необходимо создать ещё один такой объект.
Но, пожалуй, самое важное о свойстве "constructor" это то, что…
…JavaScript сам по себе не гарантирует правильное значение свойства "constructor" .
Да, оно является свойством по умолчанию в "prototype" у функций, но что случится с ним позже – зависит только
от нас.
В частности, если мы заменим прототип по умолчанию на другой объект, то свойства "constructor" в нём не
будет.
Например:
function Rabbit() {}
[Link] = {
jumps: true
};
336/597
Таким образом, чтобы сохранить верное свойство "constructor" , мы должны добавлять/удалять/изменять
свойства у прототипа по умолчанию вместо того, чтобы перезаписывать его целиком:
function Rabbit() {}
[Link] = {
jumps: true,
constructor: Rabbit
};
Итого
В этой главе мы кратко описали способ задания [[Prototype]] для объектов, создаваемых с помощью функции-
конструктора. Позже мы рассмотрим, как можно использовать эту возможность.
Всё достаточно просто. Выделим основные моменты:
● Свойство [Link] (не путать с [[Prototype]] ) устанавливает [[Prototype]] для новых объектов при
вызове new F() .
● Значение [Link] должно быть либо объектом, либо null . Другие значения не будут работать.
● Свойство "prototype" является особым, только когда оно назначено функции-конструктору, которая вызывается
оператором new .
let user = {
name: "John",
prototype: "Bla-bla" // никакой магии нет - обычное свойство
};
Задачи
Изменяем "prototype"
важность: 5
В коде ниже мы создаём нового кролика new Rabbit , а потом пытаемся изменить его прототип.
function Rabbit() {}
[Link] = {
eats: true
};
1.
Добавим одну строчку (выделенную в коде ниже). Что вызов alert покажет нам сейчас?
337/597
function Rabbit() {}
[Link] = {
eats: true
};
[Link] = {};
alert( [Link] ); // ?
2.
function Rabbit() {}
[Link] = {
eats: true
};
[Link] = false;
alert( [Link] ); // ?
3.
function Rabbit() {}
[Link] = {
eats: true
};
delete [Link];
alert( [Link] ); // ?
4.
function Rabbit() {}
[Link] = {
eats: true
};
delete [Link];
alert( [Link] ); // ?
К решению
Представьте, что у нас имеется некий объект obj , созданный функцией-конструктором – мы не знаем какой именно,
но хотелось бы создать ещё один объект такого же типа.
338/597
Приведите пример функции-конструктора для объекта obj , с которой такой вызов корректно сработает. И пример
функции-конструктора, с которой такой код поведёт себя неправильно.
К решению
Встроенные прототипы
Свойство "prototype" широко используется внутри самого языка JavaScript. Все встроенные функции-
конструкторы используют его.
Сначала мы рассмотрим детали, а затем используем "prototype" для добавления встроенным объектам новой
функциональности.
[Link]
Где код, который генерирует строку "[object Object]" ? Это встроенный метод toString , но где он? obj ведь
пуст!
…Но краткая нотация obj = {} – это то же самое, что и obj = new Object() , где Object – встроенная
функция-конструктор для объектов с собственным свойством prototype , которое ссылается на огромный объект с
методом toString и другими.
Вот что происходит:
Object [Link]
prototype
constructor: Object
toString: function
...
Когда вызывается new Object() (или создаётся объект с помощью литерала {...} ), свойство [[Prototype]]
этого объекта устанавливается на [Link] по правилам, которые мы обсуждали в предыдущей главе:
Object [Link]
prototype
constructor: Object
toString: function
...
[[Prototype]]
Обратите внимание, что по цепочке прототипов выше [Link] больше нет свойства [[Prototype]] :
alert([Link].__proto__); // null
Другие встроенные объекты, такие как Array , Date , Function и другие, также хранят свои методы в прототипах.
339/597
Например, при создании массива [1, 2, 3] внутренне используется конструктор массива Array . Поэтому
прототипом массива становится [Link] , предоставляя ему свои методы. Это позволяет эффективно
использовать память.
Согласно спецификации, наверху иерархии встроенных прототипов находится [Link] . Поэтому иногда
говорят, что «всё наследует от объектов».
Вот более полная картина (для трёх встроенных объектов):
null
[[Prototype]]
[Link]
toString: function
other object methods
[[Prototype]] [[Prototype]]
[[Prototype]]
// наследует ли от [Link]?
alert( arr.__proto__ === [Link] ); // true
Некоторые методы в прототипах могут пересекаться, например, у [Link] есть свой метод toString ,
который выводит элементы массива через запятую:
Как мы видели ранее, у [Link] есть свой метод toString , но так как [Link] ближе в
цепочке прототипов, то берётся именно вариант для массивов:
[Link]
toString: function
...
[[Prototype]]
[Link]
toString: function
...
[[Prototype]]
[1, 2, 3]
В браузерных инструментах, таких как консоль разработчика, можно посмотреть цепочку наследования (возможно,
потребуется использовать [Link] для встроенных объектов):
340/597
Другие встроенные объекты устроены аналогично. Даже функции – они объекты встроенного конструктора
Function , и все их методы ( call / apply и другие) берутся из [Link] . Также у функций есть свой
метод toString .
function f() {}
Примитивы
Встроенные прототипы можно изменять. Например, если добавить метод к [Link] , метод становится
доступен для всех строк:
[Link] = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
В течение процесса разработки у нас могут возникнуть идеи о новых встроенных методах, которые нам хотелось бы
иметь, и искушение добавить их во встроенные прототипы. Это плохая идея.
Важно:
Прототипы глобальны, поэтому очень легко могут возникнуть конфликты. Если две библиотеки добавляют метод
[Link] , то одна из них перепишет метод другой.
В современном программировании есть только один случай, в котором одобряется изменение встроенных
прототипов. Это создание полифилов.
341/597
Полифил – это термин, который означает эмуляцию метода, который существует в спецификации JavaScript, но ещё
не поддерживается текущим движком JavaScript.
Тогда мы можем реализовать его сами и добавить во встроенный прототип.
Например:
[Link] = function(n) {
// повторить строку n раз
Заимствование у прототипов
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
[Link] = [Link];
Это работает, потому что для внутреннего алгоритма встроенного метода join важны только корректность индексов
и свойство length , он не проверяет, является ли объект на самом деле массивом. И многие встроенные методы
работают так же.
Альтернативная возможность – мы можем унаследовать от массива, установив obj.__proto__ как
[Link] , таким образом все методы Array станут автоматически доступны в obj .
Но это будет невозможно, если obj уже наследует от другого объекта. Помните, мы можем наследовать только от
одного объекта одновременно.
Заимствование методов – гибкий способ, позволяющий смешивать функциональность разных объектов по
необходимости.
Итого
● Все встроенные объекты следуют одному шаблону:
● Методы хранятся в прототипах ( [Link] , [Link] , [Link] и т.д.).
● Сами объекты хранят только данные (элементы массивов, свойства объектов, даты).
● Примитивы также хранят свои методы в прототипах объектов-обёрток: [Link] , [Link] ,
[Link] . Только у значений undefined и null нет объектов-обёрток.
● Встроенные прототипы могут быть изменены или дополнены новыми методами. Но не рекомендуется менять их.
Единственная допустимая причина – это добавление нового метода из стандарта, который ещё не поддерживается
движком JavaScript.
342/597
Задачи
Добавьте всем функциям в прототип метод defer(ms) , который вызывает функции через ms миллисекунд.
function f() {
alert("Hello!");
}
К решению
Добавьте всем функциям в прототип метод defer(ms) , который возвращает обёртку, откладывающую вызов
функции на ms миллисекунд.
function f(a, b) {
alert( a + b );
}
К решению
В первой главе этого раздела мы упоминали, что существуют современные методы работы с прототипами.
Свойство __proto__ считается устаревшим, и по стандарту оно должно поддерживаться только браузерами.
Современные же методы это:
●
[Link](proto[, descriptors]) – создаёт пустой объект со свойством [[Prototype]] , указанным как proto ,
и необязательными дескрипторами свойств descriptors .
● [Link](obj) – возвращает свойство [[Prototype]] объекта obj .
● [Link](obj, proto) – устанавливает свойство [[Prototype]] объекта obj как proto .
Например:
let animal = {
eats: true
};
alert([Link]); // true
343/597
У [Link] есть необязательный второй аргумент: дескрипторы свойств. Мы можем добавить
дополнительное свойство новому объекту таким образом:
let animal = {
eats: true
};
alert([Link]); // true
Такой вызов создаёт точную копию объекта obj , включая все свойства: перечисляемые и неперечисляемые,
геттеры/сеттеры для свойств – и всё это с правильным свойством [[Prototype]] .
Краткая история
Если пересчитать все способы управления прототипом, то их будет много! И многие из них делают одно и то же!
Почему так?
В силу исторических причин.
● Свойство "prototype" функции-конструктора существует с совсем давних времён.
●
Позднее, в 2012 году, в стандарте появился метод [Link] . Это давало возможность создавать объекты с
указанным прототипом, но не позволяло устанавливать/получать его. Тогда браузеры реализовали нестандартный
аксессор __proto__ , который позволил устанавливать/получать прототип в любое время.
● Позднее, в 2015 году, в стандарт были добавлены [Link] и [Link],
заменяющие собой аксессор __proto__ , который упоминается в Приложении Б стандарта, которое не
обязательно к поддержке в небраузерных окружениях. При этом де-факто __proto__ всё ещё поддерживается
везде.
В итоге сейчас у нас есть все эти способы для работы с прототипом.
Почему же __proto__ был заменён на функции getPrototypeOf/setPrototypeOf ? Читайте далее, чтобы
узнать ответ.
"Простейший" объект
Как мы знаем, объекты можно использовать как ассоциативные массивы для хранения пар ключ/значение.
…Но если мы попробуем хранить созданные пользователями ключи (например, словари с пользовательским
вводом), мы можем заметить интересный сбой: все ключи работают как ожидается, за исключением "__proto__" .
Посмотрите на пример:
344/597
let obj = {};
Конкретно в этом примере последствия не так ужасны, но если мы присваиваем объектные значения, то прототип и в
самом деле может быть изменён. В результате дальнейшее выполнение пойдёт совершенно непредсказуемым
образом.
Что хуже всего – разработчики не задумываются о такой возможности совсем. Это делает такие ошибки сложным для
отлавливания или даже превращает их в уязвимости, особенно когда JavaScript используется на сервере.
Неожиданные вещи могут случаться также при присвоении свойства toString , которое по умолчанию функция, и
других свойств, которые тоже на самом деле являются встроенными методами.
Как же избежать проблемы?
Во-первых, мы можем переключиться на использование коллекции Map , и тогда всё будет в порядке.
Но и Object может также хорошо подойти, потому что создатели языка уже давно продумали решение проблемы.
Свойство __proto__ – не обычное, а аксессор, заданный в [Link] :
Object [Link]
prototype
...
get __proto__: function
set __proto__: function
[[Prototype]]
obj
Так что при чтении или установке obj.__proto__ вызывается соответствующий геттер/сеттер из прототипа obj , и
именно он устанавливает/получает свойство [[Prototype]] .
Как было сказано в начале этой секции учебника, __proto__ – это способ доступа к свойству [[Prototype]] , это
не само свойство [[Prototype]] .
Теперь, если мы хотим использовать объект как ассоциативный массив, мы можем сделать это с помощью
небольшого трюка:
null
[[Prototype]]
obj
Таким образом не будет унаследованного геттера/сеттера для __proto__ . Теперь это свойство обрабатывается как
обычное свойство, и приведённый выше пример работает правильно.
345/597
Мы можем назвать такой объект «простейшим» или «чистым словарным объектом», потому что он ещё проще, чем
обычные объекты {...} .
Недостаток в том, что у таких объектов не будет встроенных методов объекта, таких как toString :
alert([Link](chineseDictionary)); // hello,bye
Итого
Встроенный геттер/сеттер __proto__ не безопасен, если мы хотим использовать созданные пользователями ключи
в объекте. Как минимум потому, что пользователь может ввести "__proto__" как ключ, от чего может возникнуть
ошибка. Если повезёт – последствия будут лёгкими, но, вообще говоря, они непредсказуемы.
Так что мы можем использовать либо [Link](null) для создания «простейшего» объекта, либо
использовать коллекцию Map .
Кроме этого, [Link] даёт нам лёгкий способ создать поверхностную копию объекта со всеми
дескрипторами:
Мы также ясно увидели, что __proto__ – это геттер/сеттер для свойства [[Prototype]] , и находится он в
[Link] , как и другие методы.
Мы можем создавать объекты без прототипов с помощью [Link](null) . Такие объекты можно
использовать как «чистые словари», у них нет проблем с использованием строки "__proto__" в качестве ключа.
Ещё методы:
●
[Link](obj) / [Link](obj) / [Link](obj) – возвращают массив всех перечисляемых
собственных строковых ключей/значений/пар ключ-значение.
● [Link](obj) – возвращает массив всех собственных символьных ключей.
● [Link](obj) – возвращает массив всех собственных строковых ключей.
● [Link](obj) – возвращает массив всех собственных ключей.
● [Link](key) : возвращает true , если у obj есть собственное (не унаследованное) свойство с
именем key .
Все методы, которые возвращают свойства объектов (такие как [Link] и другие), возвращают «собственные»
свойства. Если мы хотим получить и унаследованные, можно воспользоваться циклом for..in .
346/597
Задачи
Имеется объект dictionary , созданный с помощью [Link](null) для хранения любых пар ключ/
значение .
Добавьте ему метод [Link]() , который должен возвращать список ключей, разделённых запятой.
Ваш toString не должен выводиться при итерации объекта с помощью цикла for..in .
К решению
function Rabbit(name) {
[Link] = name;
}
[Link] = function() {
alert([Link]);
};
[Link]();
[Link]();
[Link](rabbit).sayHi();
rabbit.__proto__.sayHi();
К решению
Классы
Класс: базовый синтаксис
347/597
На практике нам часто надо создавать много объектов одного вида, например пользователей, товары или что-то ещё.
Как мы уже знаем из главы Конструктор, оператор "new", с этим может помочь new function .
Но в современном JavaScript есть и более продвинутая конструкция «class», которая предоставляет новые
возможности, полезные для объектно-ориентированного программирования.
Синтаксис «class»
class MyClass {
// методы класса
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
Затем используйте вызов new MyClass() для создания нового объекта со всеми перечисленными методами.
При этом автоматически вызывается метод constructor() , в нём мы можем инициализировать объект.
Например:
class User {
constructor(name) {
[Link] = name;
}
sayHi() {
alert([Link]);
}
// Использование:
let user = new User("Иван");
[Link]();
Итак, что же такое class ? Это не полностью новая языковая сущность, как может показаться на первый взгляд.
Давайте развеем всю магию и посмотрим, что такое класс на самом деле. Это поможет в понимании многих сложных
аспектов.
В JavaScript класс – это разновидность функции.
Взгляните:
348/597
class User {
constructor(name) { [Link] = name; }
sayHi() { alert([Link]); }
}
При вызове метода объекта new User он будет взят из прототипа, как описано в главе [Link]. Таким образом,
объекты new User имеют доступ к методам класса.
На картинке показан результат объявления class User :
User [Link]
prototype
sayHi: function
constructor: User
class User {
constructor(name) { [Link] = name; }
sayHi() { alert([Link]); }
}
Иногда говорят, что class – это просто «синтаксический сахар» в JavaScript (синтаксис для улучшения читаемости
кода, но не делающий ничего принципиально нового), потому что мы можем сделать всё то же самое без конструкции
class :
// Использование:
let user = new User("Иван");
[Link]();
349/597
Результат этого кода очень похож. Поэтому, действительно, есть причины, по которым class можно считать
синтаксическим сахаром для определения конструктора вместе с методами прототипа.
Однако есть важные отличия:
1. Во-первых, функция, созданная с помощью class , помечена специальным внутренним свойством
[[IsClassConstructor]]: true . Поэтому это не совсем то же самое, что создавать её вручную.
В отличие от обычных функций, конструктор класса не может быть вызван без new :
class User {
constructor() {}
}
Кроме того, строковое представление конструктора класса в большинстве движков JavaScript начинается с «class
…»
class User {
constructor() {}
}
2. Методы класса являются неперечислимыми. Определение класса устанавливает флаг enumerable в false для
всех методов в "prototype" .
И это хорошо, так как если мы проходимся циклом for..in по объекту, то обычно мы не хотим при этом получать
методы класса.
3. Классы всегда используют use strict . Весь код внутри класса автоматически находится в строгом режиме.
Также в дополнение к основной, описанной выше, функциональности, синтаксис class даёт ряд других интересных
возможностей, с которыми мы познакомимся чуть позже.
Class Expression
Как и функции, классы можно определять внутри другого выражения, передавать, возвращать, присваивать и т.д.
Пример Class Expression (по аналогии с Function Expression):
350/597
function makeClass(phrase) {
// объявляем класс и возвращаем его
return class {
sayHi() {
alert(phrase);
};
};
}
Как и в литеральных объектах, в классах можно объявлять вычисляемые свойства, геттеры/сеттеры и т.д.
Вот пример [Link] , реализованного с использованием get/set :
class User {
constructor(name) {
// вызывает сеттер
[Link] = name;
}
get name() {
return this._name;
}
set name(value) {
if ([Link] < 4) {
alert("Имя слишком короткое.");
return;
}
this._name = value;
}
[Link]([Link], {
name: {
get() {
return this._name
},
set(name) {
// ...
}
}
});
class User {
['say' + 'Hi']() {
alert("Привет");
}
351/597
new User().sayHi();
Свойства классов
В приведённом выше примере у класса User были только методы. Давайте добавим свойство:
class User {
name = "Аноним";
sayHi() {
alert(`Привет, ${[Link]}!`);
}
}
new User().sayHi();
Свойство name не устанавливается в [Link] . Вместо этого оно создаётся оператором new перед
запуском конструктора, это именно свойство объекта.
Итого
class MyClass {
prop = value; // свойство
constructor(...) { // конструктор
// ...
}
method(...) {} // метод
get something(...) {} // геттер
set something(...) {} // сеттер
[[Link]]() {} // метод с вычисляемым именем (здесь - символом)
// ...
}
MyClass технически является функцией (той, которую мы определяем как constructor ), в то время как методы,
геттеры и сеттеры записываются в [Link] .
В следующих главах мы узнаем больше о классах, включая наследование и другие возможности.
Задачи
Перепишите класс
важность: 5
Класс Clock написан в функциональном стиле. Перепишите его, используя современный синтаксис классов.
К решению
Наследование классов
352/597
Ключевое слово «extends»
class Animal {
constructor(name) {
[Link] = 0;
[Link] = name;
}
run(speed) {
[Link] = speed;
alert(`${[Link]} бежит со скоростью ${[Link]}.`);
}
stop() {
[Link] = 0;
alert(`${[Link]} стоит неподвижно.`);
}
}
Animal [Link]
prototype
constructor: Animal
run: function
stop: function
[[Prototype]]
new Animal
name: "Мой питомец"
Объект класса Rabbit имеет доступ как к методам Rabbit , таким как [Link]() , так и к методам Animal ,
таким как [Link]() .
Внутри ключевое слово extends работает по старой доброй механике прототипов. Оно устанавливает
[Link].[[Prototype]] в [Link] . Таким образом, если метода не оказалось в
[Link] , JavaScript берет его из [Link] .
353/597
Animal [Link]
constructor prototype
constructor: Animal
run: function
stop: function
extends
[[Prototype]]
Rabbit [Link]
constructor prototype
constructor: Rabbit
hide: function
[[Prototype]]
new Rabbit
name: "Белый кролик"
Например, чтобы найти метод [Link] , движок проверяет (снизу вверх на картинке):
Как мы помним из главы Встроенные прототипы, сам JavaScript использует наследование на прототипах для
встроенных объектов. Например, [Link].[[Prototype]] является [Link] , поэтому у дат
есть универсальные методы объекта.
function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}
Это может быть полезно для продвинутых приёмов проектирования, где мы можем использовать функции для
генерации классов в зависимости от многих условий и затем наследовать их.
Переопределение методов
Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе
Rabbit , берутся непосредственно «как есть» из класса Animal .
Но если мы укажем в Rabbit собственный метод, например stop() , то он будет использован вместо него:
Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе,
изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод
до/после или в процессе.
У классов есть ключевое слово "super" для таких случаев.
● [Link](...) вызывает родительский метод.
354/597
●
super(...) для вызова родительского конструктора (работает только внутри нашего конструктора).
class Animal {
constructor(name) {
[Link] = 0;
[Link] = name;
}
run(speed) {
[Link] = speed;
alert(`${[Link]} бежит со скоростью ${[Link]}.`);
}
stop() {
[Link] = 0;
alert(`${[Link]} стоит.`);
}
stop() {
[Link](); // вызываем родительский метод stop
[Link](); // и затем hide
}
}
Теперь у класса Rabbit есть метод stop , который вызывает родительский [Link]() в процессе
выполнения.
В примере super в стрелочной функции тот же самый, что и в stop() , поэтому метод отрабатывает как и
ожидается. Если бы мы указали здесь «обычную» функцию, была бы ошибка:
// Unexpected super
setTimeout(function() { [Link]() }, 1000);
Переопределение конструктора
355/597
class Rabbit extends Animal {
// генерируется для классов-потомков, у которых нет своего конструктора
constructor(...args) {
super(...args);
}
}
Как мы видим, он просто вызывает конструктор родительского класса. Так будет происходить, пока мы не создадим
собственный конструктор.
Давайте добавим конструктор для Rabbit . Он будет устанавливать earLength в дополнение к name :
class Animal {
constructor(name) {
[Link] = 0;
[Link] = name;
}
// ...
}
constructor(name, earLength) {
[Link] = 0;
[Link] = name;
[Link] = earLength;
}
// ...
}
// Не работает!
let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not defined.
Поэтому, если мы создаём собственный конструктор, мы должны вызвать super , в противном случае объект для
this не будет создан, и мы получим ошибку.
Чтобы конструктор Rabbit работал, он должен вызвать super() до того, как использовать this , чтобы не было
ошибки:
class Animal {
constructor(name) {
[Link] = 0;
[Link] = name;
}
// ...
}
356/597
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
[Link] = earLength;
}
// ...
}
// теперь работает
let rabbit = new Rabbit("Белый кролик", 10);
alert([Link]); // Белый кролик
alert([Link]); // 10
Продвинутое замечание
В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других
языках программирования.
Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок
(но не очень часто).
Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем
вернитесь к нему через некоторое время.
Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение
отличается от большинства других языков программирования.
Рассмотрим этот пример:
class Animal {
name = 'animal';
constructor() {
alert([Link]); // (*)
}
}
Здесь, класс Rabbit расширяет Animal и переопределяет поле name своим собственным значением.
В Rabbit нет собственного конструктора, поэтому вызывается конструктор Animal .
Что интересно, в обоих случаях: new Animal() и new Rabbit() , alert в строке (*) показывает animal .
Другими словами, родительский конструктор всегда использует своё собственное значение поля, а не
переопределённое.
class Animal {
showName() { // вместо [Link] = 'animal'
alert('animal');
}
constructor() {
[Link](); // вместо alert([Link]);
}
357/597
}
В нашем случае Rabbit – это производный класс. В нем нет конструктора constructor() . Как было сказано
ранее, это то же самое, как если бы был пустой конструктор, содержащий только super(...args) .
Итак, new Rabbit() вызывает super() , таким образом, выполняя родительский конструктор, и (согласно правилу
для производных классов) только после этого инициализируются поля его класса. На момент выполнения
родительского конструктора ещё нет полей класса Rabbit , поэтому используются поля Animal .
Это тонкое различие между полями и методами характерно для JavaScript.
К счастью, такое поведение проявляется только в том случае, когда переопределенное поле используется в
родительском конструкторе. Тогда может быть трудно понять, что происходит, поэтому мы объясняем это здесь.
Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей.
Продвинутая информация
Если вы читаете учебник первый раз – эту секцию можно пропустить.
Она рассказывает о внутреннем устройстве наследования и вызовe super .
Давайте заглянем «под капот» super . Здесь есть некоторые интересные моменты.
Вообще, исходя из наших знаний до этого момента, super вообще не может работать!
Ну правда, давайте спросим себя – как он должен работать, чисто технически? Когда метод объекта выполняется, он
получает текущий объект как this . Если мы вызываем [Link]() , то движку необходимо получить method
из прототипа текущего объекта. И как ему это сделать?
Задача может показаться простой, но это не так. Движок знает текущий this и мог бы попытаться получить
родительский метод как this.__proto__.method . Однако, увы, такой «наивный» путь не работает.
Продемонстрируем проблему. Без классов, используя простые объекты для наглядности.
Вы можете пропустить эту часть и перейти ниже к подсекции [[HomeObject]] , если не хотите знать детали. Вреда
не будет. Или читайте далее, если хотите разобраться.
В примере ниже rabbit.__proto__ = animal . Попробуем в [Link]() вызвать [Link]() , используя
this.__proto__ :
let animal = {
name: "Animal",
eat() {
alert(`${[Link]} ест.`);
}
};
358/597
let rabbit = {
__proto__: animal,
name: "Кролик",
eat() {
// вот как предположительно может работать [Link]()
this.__proto__.[Link](this); // (*)
}
};
В строке (*) мы берём eat из прототипа ( animal ) и вызываем его в контексте текущего объекта. Обратите
внимание, что .call(this) здесь неспроста: простой вызов this.__proto__.eat() будет выполнять
родительский eat в контексте прототипа, а не текущего объекта.
Приведённый выше код работает так, как задумано: выполняется нужный alert .
Теперь давайте добавим ещё один объект в цепочку наследования и увидим, как все сломается:
let animal = {
name: "Животное",
eat() {
alert(`${[Link]} ест.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...делаем что-то специфичное для кролика и вызываем родительский (animal) метод
this.__proto__.[Link](this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...делаем что-то, связанное с длинными ушами, и вызываем родительский (rabbit) метод
this.__proto__.[Link](this); // (**)
}
};
rabbit longEar
rabbit longEar
359/597
// внутри [Link]() у нас this = longEar
this.__proto__.[Link](this) // (**)
// становится
longEar.__proto__.[Link](this)
// то же что и
[Link](this);
2. В строке (*) в [Link] мы хотим передать вызов выше по цепочке, но this=longEar , поэтому
this.__proto__.eat снова равен [Link] !
3. … [Link] вызывает себя в бесконечном цикле, потому что не может подняться дальше по цепочке.
[[HomeObject]]
Для решения этой проблемы в JavaScript было добавлено специальное внутреннее свойство для функций:
[[HomeObject]] .
Когда функция объявлена как метод внутри класса или объекта, её свойство [[HomeObject]] становится равно
этому объекту.
Затем super использует его, чтобы получить прототип родителя и его методы.
Давайте посмотрим, как это работает – опять же, используя простые объекты:
let animal = {
name: "Животное",
eat() { // [Link].[[HomeObject]] == animal
alert(`${[Link]} ест.`);
}
};
let rabbit = {
__proto__: animal,
name: "Кролик",
eat() { // [Link].[[HomeObject]] == rabbit
[Link]();
}
};
let longEar = {
__proto__: rabbit,
name: "Длинноух",
eat() { // [Link].[[HomeObject]] == longEar
[Link]();
}
};
// работает верно
[Link](); // Длинноух ест.
Это работает как задумано благодаря [[HomeObject]] . Метод, такой как [Link] , знает свой
[[HomeObject]] и получает метод родителя из его прототипа. Вообще без использования this .
Методы не «свободны»
До этого мы неоднократно видели, что функции в JavaScript «свободны», не привязаны к объектам. Их можно
копировать между объектами и вызывать с любым this .
Но само существование [[HomeObject]] нарушает этот принцип, так как методы запоминают свои объекты.
[[HomeObject]] нельзя изменить, эта связь – навсегда.
Единственное место в языке, где используется [[HomeObject]] – это super . Поэтому если метод не использует
super , то мы все ещё можем считать его свободным и копировать между объектами. А вот если super в коде есть,
то возможны побочные эффекты.
360/597
Вот пример неверного результата super после копирования:
let animal = {
sayHi() {
alert("Я животное");
}
};
let plant = {
sayHi() {
alert("Я растение");
}
};
animal plant
sayHi sayHi
rabbit tree
[[HomeObject]]
sayHi sayHi
Методы, а не свойства-функции
Свойство [[HomeObject]] определено для методов как классов, так и обычных объектов. Но для объектов методы
должны быть объявлены именно как method() , а не "method: function()" .
Для нас различий нет, но они есть для JavaScript.
В приведённом ниже примере используется синтаксис не метода, свойства-функции. Поэтому у него нет
[[HomeObject]] , и наследование не работает:
let animal = {
eat: function() { // намеренно пишем так, а не eat() { ...
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
[Link]();
}
361/597
};
Итого
Также:
● У стрелочных функций нет своего this и super , поэтому они «прозрачно» встраиваются во внешний контекст.
Задачи
class Animal {
constructor(name) {
[Link] = name;
}
К решению
Улучшенные часы
важность: 5
class Clock {
constructor({ template }) {
[Link] = template;
}
render() {
let date = new Date();
362/597
let hours = [Link]();
if (hours < 10) hours = '0' + hours;
[Link](output);
}
stop() {
clearInterval([Link]);
}
start() {
[Link]();
[Link] = setInterval(() => [Link](), 1000);
}
}
Создайте новый класс ExtendedClock , который будет наследоваться от Clock и добавьте параметр precision –
количество миллисекунд между «тиками». Установите значение в 1000 (1 секунда) по умолчанию.
К решению
Мы также можем присвоить метод самому классу. Такие методы называются статическими.
В объявление класса они добавляются с помощью ключевого слова static , например:
class User {
static staticMethod() {
alert(this === User);
}
}
[Link](); // true
Это фактически то же самое, что присвоить метод напрямую как свойство функции:
class User { }
[Link] = function() {
alert(this === User);
};
Значением this при вызове [Link]() является сам конструктор класса User (правило «объект до
точки»).
Обычно статические методы используются для реализации функций, которые будут принадлежать классу в целом, но
не какому-либо его конкретному объекту.
Звучит не очень понятно? Сейчас все встанет на свои места.
Например, есть объекты статей Article , и нужна функция для их сравнения.
363/597
Естественное решение – сделать для этого статический метод [Link] :
class Article {
constructor(title, date) {
[Link] = title;
[Link] = date;
}
// использование
let articles = [
new Article("HTML", new Date(2019, 1, 1)),
new Article("CSS", new Date(2019, 0, 1)),
new Article("JavaScript", new Date(2019, 11, 1))
];
[Link]([Link]);
Здесь метод [Link] стоит «над» статьями, как средство для их сравнения. Это метод не отдельной
статьи, а всего класса.
Другим примером может быть так называемый «фабричный» метод.
Скажем, нам нужно несколько способов создания статьи:
1. Создание через заданные параметры ( title , date и т. д.).
2. Создание пустой статьи с сегодняшней датой.
3. …или как-то ещё.
Первый способ может быть реализован через конструктор. А для второго можно использовать статический метод
класса.
Такой как [Link]() в следующем примере:
class Article {
constructor(title, date) {
[Link] = title;
[Link] = date;
}
static createTodays() {
// помним, что this = Article
return new this("Сегодняшний дайджест", new Date());
}
}
Теперь каждый раз, когда нам нужно создать сегодняшний дайджест, нужно вызывать [Link]() .
Ещё раз, это не метод одной статьи, а метод всего класса.
Статические методы также используются в классах, относящихся к базам данных, для поиска/сохранения/удаления
вхождений в базу данных, например:
364/597
Статические методы недоступны для отдельных объектов
Статические методы могут вызываться для классов, но не для отдельных объектов.
Например. такой код не будет работать:
// ...
[Link](); /// Error: [Link] is not a function
Статические свойства
Новая возможность
Эта возможность была добавлена в язык недавно. Примеры работают в последнем Chrome.
Статические свойства также возможны, они выглядят как свойства класса, но с static в начале:
class Article {
static publisher = "Илья Кантор";
}
class Animal {
constructor(name, speed) {
[Link] = speed;
[Link] = name;
}
run(speed = 0) {
[Link] += speed;
alert(`${[Link]} бежит со скоростью ${[Link]}.`);
}
// Наследует от Animal
class Rabbit extends Animal {
hide() {
alert(`${[Link]} прячется!`);
}
}
let rabbits = [
new Rabbit("Белый кролик", 10),
new Rabbit("Чёрный кролик", 5)
];
[Link]([Link]);
365/597
rabbits[0].run(); // Чёрный кролик бежит со скоростью 5.
Animal [Link]
prototype
compare constructor: Animal
run: function
[[Prototype]] [[Prototype]]
Rabbit [Link]
prototype
constructor: Rabbit
hide: function
[[Prototype]]
rabbit
В результате наследование работает как для обычных, так и для статических методов.
Давайте это проверим кодом:
class Animal {}
class Rabbit extends Animal {}
// для статики
alert(Rabbit.__proto__ === Animal); // true
Итого
class MyClass {
static property = ...;
static method() {
...
}
}
366/597
[Link] = ...
[Link] = ...
Задачи
Как мы уже знаем, все объекты наследуют от [Link] и имеют доступ к «общим» методам объекта,
например hasOwnProperty .
Пример:
class Rabbit {
constructor(name) {
[Link] = name;
}
}
Но что если мы явно напишем "class Rabbit extends Object" – тогда результат будет отличаться от обычного
"class Rabbit" ?
В чем разница?
К решению
Например, кофеварка. Простая снаружи: кнопка, экран, несколько отверстий… И, конечно, результат – прекрасный
кофе! :)
367/597
Но внутри… (картинка из инструкции по ремонту)
Если мы продолжаем аналогию с кофеваркой – то, что скрыто внутри: трубка кипятильника, нагревательный элемент
и т.д. – это внутренний интерфейс.
Внутренний интерфейс используется для работы объекта, его детали используют друг друга. Например, трубка
кипятильника прикреплена к нагревательному элементу.
Но снаружи кофеварка закрыта защитным кожухом, так что никто не может добраться до сложных частей. Детали
скрыты и недоступны. Мы можем использовать их функции через внешний интерфейс.
368/597
Итак, всё, что нам нужно для использования объекта, это знать его внешний интерфейс. Мы можем совершенно не
знать, как это работает внутри, и это здорово.
Это было общее введение.
В JavaScript есть два типа полей (свойств и методов) объекта:
● Публичные: доступны отовсюду. Они составляют внешний интерфейс. До этого момента мы использовали только
публичные свойства и методы.
● Приватные: доступны только внутри класса. Они для внутреннего интерфейса.
Во многих других языках также существуют «защищённые» поля, доступные только внутри класса или для дочерних
классов (то есть, как приватные, но разрешён доступ для наследующих классов) и также полезны для внутреннего
интерфейса. В некотором смысле они более распространены, чем приватные, потому что мы обычно хотим, чтобы
наследующие классы получали доступ к внутренним полям.
Защищённые поля не реализованы в JavaScript на уровне языка, но на практике они очень удобны, поэтому их
эмулируют.
А теперь давайте сделаем кофеварку на JavaScript со всеми этими типами свойств. Кофеварка имеет множество
деталей, мы не будем их моделировать для простоты примера (хотя могли бы).
class CoffeeMachine {
waterAmount = 0; // количество воды внутри
constructor(power) {
[Link] = power;
alert( `Создана кофеварка, мощность: ${power}` );
}
// создаём кофеварку
let coffeeMachine = new CoffeeMachine(100);
// добавляем воды
[Link] = 200;
Прямо сейчас свойства waterAmount и power публичные. Мы можем легко получать и устанавливать им любое
значение извне.
Давайте изменим свойство waterAmount на защищённое, чтобы иметь больше контроля над ним. Например, мы не
хотим, чтобы кто-либо устанавливал его ниже нуля.
Защищённые свойства обычно начинаются с префикса _ .
Это не синтаксис языка: есть хорошо известное соглашение между программистами, что такие свойства и методы не
должны быть доступны извне. Большинство программистов следуют этому соглашению.
Так что наше свойство будет называться _waterAmount :
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) throw new Error("Отрицательное количество воды");
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
369/597
}
Теперь доступ под контролем, поэтому указать воду ниже нуля не удалось.
Давайте сделаем свойство power доступным только для чтения. Иногда нужно, чтобы свойство устанавливалось
только при создании объекта и после этого никогда не изменялось.
Это как раз требуется для кофеварки: мощность никогда не меняется.
Для этого нам нужно создать только геттер, но не сеттер:
class CoffeeMachine {
// ...
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
// создаём кофеварку
let coffeeMachine = new CoffeeMachine(100);
Геттеры/сеттеры
Здесь мы использовали синтаксис геттеров/сеттеров.
Но в большинстве случаев использование функций get.../set... предпочтительнее:
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount (value) {
if (value < 0) throw new Error("Отрицательное количество воды");
this._waterAmount = value;
}
getWaterAmount () {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
Это выглядит немного длиннее, но функции более гибкие. Они могут принимать несколько аргументов (даже если
они нам сейчас не нужны). Итак, на будущее, если нам надо что-то отрефакторить, функции – более безопасный
выбор.
С другой стороны, синтаксис get/set короче, решать вам.
370/597
Защищённые поля наследуются
Если мы унаследуем class MegaMachine extends CoffeeMachine , ничто не помешает нам обращаться к
this._waterAmount или this._power из методов нового класса.
Таким образом, защищённые поля, конечно же, наследуются. В отличие от приватных полей, в чём мы убедимся
ниже.
Новая возможность
Эта возможность была добавлена в язык недавно. В движках JavaScript пока не поддерживается или
поддерживается частично, нужен полифил.
Есть новшество в языке JavaScript, которое почти добавлено в стандарт: оно добавляет поддержку приватных
свойств и методов.
Приватные свойства и методы должны начинаться с # . Они доступны только внутри класса.
Например, в классе ниже есть приватное свойство #waterLimit и приватный метод #checkWater для проверки
количества воды:
class CoffeeMachine {
#waterLimit = 200;
#checkWater(value) {
if (value < 0) throw new Error("Отрицательный уровень воды");
if (value > this.#waterLimit) throw new Error("Слишком много воды");
}
}
На уровне языка # является специальным символом, который означает, что поле приватное. Мы не можем получить
к нему доступ извне или из наследуемых классов.
Приватные поля не конфликтуют с публичными. У нас может быть два поля одновременно – приватное
#waterAmount и публичное waterAmount .
Например, давайте сделаем аксессор waterAmount для #waterAmount :
class CoffeeMachine {
#waterAmount = 0;
get waterAmount() {
return this.#waterAmount;
}
set waterAmount(value) {
if (value < 0) throw new Error("Отрицательный уровень воды");
this.#waterAmount = value;
}
}
[Link] = 100;
alert(machine.#waterAmount); // Error
В отличие от защищённых, функциональность приватных полей обеспечивается самим языком. Это хорошо.
Но если мы унаследуем от CoffeeMachine , то мы не получим прямого доступа к #waterAmount . Мы будем
вынуждены полагаться на геттер/сеттер waterAmount :
371/597
class MegaCoffeeMachine extends CoffeeMachine {
method() {
alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
}
}
Во многих случаях такое ограничение слишком жёсткое. Раз уж мы расширяем CoffeeMachine , у нас может быть
вполне законная причина для доступа к внутренним методам и свойствам. Поэтому защищённые свойства
используются чаще, хоть они и не поддерживаются синтаксисом языка.
Важно:
Приватные поля особенные.
Как мы помним, обычно мы можем получить доступ к полям объекта с помощью this[name] :
class User {
...
sayHi() {
let fieldName = "name";
alert(`Hello, ${this [fieldName]}`);
}
}
С приватными свойствами такое невозможно: this['#name'] не работает. Это ограничение синтаксиса сделано
для обеспечения приватности.
Итого
Поддерживаемость
Ситуация в программировании сложнее, чем с реальной кофеваркой, потому что мы не просто покупаем её один раз.
Код постоянно подвергается разработке и улучшению.
Если мы чётко отделим внутренний интерфейс, то разработчик класса сможет свободно менять его
внутренние свойства и методы, даже не информируя пользователей…
Если вы разработчик такого класса, то приятно знать, что приватные методы можно безопасно переименовывать, их
параметры можно изменять и даже удалять, потому что от них не зависит никакой внешний код.
В новой версии вы можете полностью всё переписать, но пользователю будет легко обновиться, если внешний
интерфейс остался такой же.
Сокрытие сложности
Люди обожают использовать простые вещи. По крайней мере, снаружи. Что внутри – это другое дело.
Программисты не являются исключением.
372/597
Всегда удобно, когда детали реализации скрыты, и доступен простой, хорошо документированный внешний
интерфейс.
В настоящее время приватные поля не очень хорошо поддерживаются в браузерах, но можно использовать полифил.
От встроенных классов, таких как Array , Map и других, тоже можно наследовать.
Например, в этом примере PowerArray наследуется от встроенного Array :
Обратите внимание на интересный момент: встроенные методы, такие как filter , map и другие возвращают новые
объекты унаследованного класса PowerArray . Их внутренняя реализация такова, что для этого они используют
свойство объекта constructor .
В примере выше,
Поэтому при вызове метода [Link]() он внутри создаёт массив результатов, именно используя
[Link] , а не обычный массив. Это замечательно, поскольку можно продолжать использовать методы
PowerArray далее на результатах.
Более того, мы можем настроить это поведение.
При помощи специального статического геттера [Link] можно вернуть конструктор, который JavaScript
будет использовать в filter , map и других методах для создания новых объектов.
Если бы мы хотели, чтобы методы map , filter и т. д. возвращали обычные массивы, мы могли бы вернуть Array
в [Link] , вот так:
373/597
let filteredArr = [Link](item => item >= 10);
Как вы видите, теперь .filter возвращает Array . Расширенная функциональность не будет передаваться далее.
Object [Link]
prototype
defineProperty constructor: Object
keys toString: function
... hasOwnProperty: function
...
[[Prototype]]
Date [Link]
prototype
now constructor: Date
parse toString: function
... getDate: function
...
[[Prototype]]
new Date()
1 Jan 2019
Как видите, нет связи между Date и Object . Они независимы, только [Link] наследует от
[Link] .
В этом важное отличие наследования встроенных объектов от того, что мы получаем с использованием extends .
Такая проверка может потребоваться во многих случаях. Здесь мы используем её для создания полиморфной
функции, которая интерпретирует аргументы по-разному в зависимости от их типа.
Оператор instanceof
Синтаксис:
Оператор вернёт true , если obj принадлежит классу Class или наследующему от него.
Например:
374/597
class Rabbit {}
let rabbit = new Rabbit();
// вместо класса
function Rabbit() {}
Пожалуйста, обратите внимание, что arr также принадлежит классу Object , потому что Array наследует от
Object .
Обычно оператор instanceof просматривает для проверки цепочку прототипов. Но это поведение может быть
изменено при помощи статического метода [Link] .
2. Большая часть классов не имеет метода [Link] . В этом случае используется стандартная логика:
проверяется, равен ли [Link] одному из прототипов в прототипной цепочке obj .
Другими словами, сравнивается:
В примере выше rabbit.__proto__ === [Link] , так что результат будет получен немедленно.
В случае с наследованием, совпадение будет на втором шаге:
class Animal {}
class Rabbit extends Animal {}
375/597
// rabbit.__proto__ === [Link] (нет совпадения)
// rabbit.__proto__.__proto__ === [Link] (совпадение!)
null
[[Prototype]]
[Link]
[[Prototype]]
[Link]
= [Link]?
[[Prototype]]
[Link]
[[Prototype]]
rabbit
Кстати, есть метод [Link](objB) , который возвращает true , если объект objA есть где-то в
прототипной цепочке объекта objB . Так что obj instanceof Class можно перефразировать как
[Link](obj) .
Забавно, но сам конструктор Class не участвует в процессе проверки! Важна только цепочка прототипов
[Link] .
Это может приводить к интересным последствиям при изменении свойства prototype после создания объекта.
Как, например, тут:
function Rabbit() {}
let rabbit = new Rabbit();
// заменяем прототип
[Link] = {};
// ...больше не rabbit!
alert( rabbit instanceof Rabbit ); // false
Мы уже знаем, что обычные объекты преобразуются к строке как [object Object] :
Так работает реализация метода toString . Но у toString имеются скрытые возможности, которые делают метод
гораздо более мощным. Мы можем использовать его как расширенную версию typeof и как альтернативу
instanceof .
Звучит странно? Так и есть. Давайте развеем мистику.
Согласно спецификации встроенный метод toString может быть позаимствован у объекта и вызван в контексте
любого другого значения. И результат зависит от типа этого значения.
● Для числа это будет [object Number]
● Для булева типа это будет [object Boolean]
● Для null : [object Null]
376/597
● Для undefined : [object Undefined]
● Для массивов: [object Array]
● …и т.д. (поведение настраивается).
Давайте продемонстрируем:
В примере мы использовали call , как описано в главе Декораторы и переадресация вызова, call/apply, чтобы
выполнить функцию objectToString в контексте this=arr .
Внутри, алгоритм метода toString анализирует контекст вызова this и возвращает соответствующий результат.
Больше примеров:
let s = [Link];
[Link]
Поведение метода объектов toString можно настраивать, используя специальное свойство объекта
[Link] .
Например:
let user = {
[[Link]]: "User"
};
Такое свойство есть у большей части объектов, специфичных для определённых окружений. Вот несколько примеров
для браузера:
Как вы можете видеть, результат – это значение [Link] (если он имеется) обёрнутое в [object
...] .
В итоге мы получили «typeof на стероидах», который не только работает с примитивными типами данных, но также и
со встроенными объектами, и даже может быть настроен.
Можно использовать {}.[Link] вместо instanceof для встроенных объектов, когда мы хотим получить
тип в виде строки, а не просто сделать проверку.
Итого
377/597
работает для возвращает
Задачи
Странный instanceof
важность: 5
Почему instanceof в примере ниже возвращает true ? Мы же видим, что a не создан с помощью B() .
function A() {}
function B() {}
К решению
Примеси
В JavaScript можно наследовать только от одного объекта. Объект имеет единственный [[Prototype]] . И класс
может расширить только один другой класс.
Иногда это может ограничивать нас. Например, у нас есть класс StreetSweeper и класс Bicycle , а мы хотим
создать их смесь: StreetSweepingBicycle .
Или у нас есть класс User , который реализует пользователей, и класс EventEmitter , реализующий события. Мы
хотели бы добавить функциональность класса EventEmitter к User , чтобы пользователи могли легко
генерировать события.
Для таких случаев существуют «примеси».
По определению из Википедии, примесь – это класс, методы которого предназначены для использования в других
классах, причём без наследования от примеси.
Другими словами, примесь определяет методы, которые реализуют определённое поведение. Мы не используем
примесь саму по себе, а используем её, чтобы добавить функциональность другим классам.
Пример примеси
Простейший способ реализовать примесь в JavaScript – это создать объект с полезными методами, которые затем
могут быть легко добавлены в прототип любого класса.
В примере ниже примесь sayHiMixin имеет методы, которые придают объектам класса User возможность вести
разговор:
// примесь
let sayHiMixin = {
sayHi() {
alert(`Привет, ${[Link]}`);
},
sayBye() {
alert(`Пока, ${[Link]}`);
}
};
378/597
// использование:
class User {
constructor(name) {
[Link] = name;
}
}
// копируем методы
[Link]([Link], sayHiMixin);
Это не наследование, а просто копирование методов. Таким образом, класс User может наследовать от другого
класса, но при этом также включать в себя примеси, «подмешивающие» другие методы, например:
[Link]([Link], sayHiMixin);
let sayMixin = {
say(phrase) {
alert(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // (или мы можем использовать [Link] для задания прототипа)
sayHi() {
// вызываем метод родителя
[Link](`Привет, ${[Link]}`); // (*)
},
sayBye() {
[Link](`Пока, ${[Link]}`); // (*)
}
};
class User {
constructor(name) {
[Link] = name;
}
}
// копируем методы
[Link]([Link], sayHiMixin);
Обратим внимание, что при вызове родительского метода [Link]() из sayHiMixin (строки, помеченные (*) )
этот метод ищется в прототипе самой примеси, а не класса.
Вот диаграмма (см. правую часть):
379/597
sayMixin
say: function
[[Prototype]]
[Link] sayHiMixin
constructor: User [[HomeObject] sayHi: function
sayHi: function
sayBye: function
sayBye: function
[[Prototype]]
user
name: ...
Это связано с тем, что методы sayHi и sayBye были изначально созданы в объекте sayHiMixin . Несмотря на то,
что они скопированы, их внутреннее свойство [[HomeObject]] ссылается на sayHiMixin , как показано на
картинке выше.
Так как super ищет родительские методы в [[HomeObject]].[[Prototype]] , это означает, что он ищет
sayHiMixin.[[Prototype]] .
EventMixin
Многие объекты в браузерной разработке (и не только) обладают важной способностью – они могут генерировать
события. События – отличный способ передачи информации всем, кто в ней заинтересован. Давайте создадим
примесь, которая позволит легко добавлять функциональность по работе с событиями любым классам/объектам.
● Примесь добавит метод .trigger(name, [...data]) для генерации события. Аргумент name – это имя
события, за которым могут следовать дополнительные аргументы с данными для события.
●
Также будет добавлен метод .on(name, handler) , который назначает обработчик для события с заданным
именем. Обработчик будет вызван, когда произойдёт событие с указанным именем name , и получит данные из
.trigger .
● …и метод .off(name, handler) , который удаляет обработчик указанного события.
После того, как все методы примеси будут добавлены, объект user сможет сгенерировать событие "login" после
входа пользователя в личный кабинет. А другой объект, к примеру, calendar сможет использовать это событие,
чтобы показывать зашедшему пользователю актуальный для него календарь.
Или menu может генерировать событие "select" , когда элемент меню выбран, а другие объекты могут назначать
обработчики, чтобы реагировать на это событие, и т.п.
let eventMixin = {
/**
* Подписаться на событие, использование:
* [Link]('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Отменить подписку, использование:
* [Link]('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < [Link]; i++) {
if (handlers[i] === handler) {
[Link](i--, 1);
}
}
},
380/597
/**
* Сгенерировать событие с указанным именем и данными
* [Link]('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // обработчиков для этого события нет
}
// вызовем обработчики
this._eventHandlers[eventName].forEach(handler => [Link](this, args));
}
};
Использование:
// Создадим класс
class Menu {
choose(value) {
[Link]("select", value);
}
}
// Добавим примесь с методами для событий
[Link]([Link], eventMixin);
Теперь если у нас есть код, заинтересованный в событии "select" , то он может слушать его с помощью
[Link](...) .
А eventMixin позволяет легко добавить такое поведение в любой класс без вмешательства в цепочку
наследования.
Итого
Примесь – общий термин в объектно-ориентированном программировании: класс, который содержит в себе методы
для других классов.
Некоторые другие языки допускают множественное наследование. JavaScript не поддерживает множественное
наследование, но с помощью примесей мы можем реализовать нечто похожее, скопировав методы в прототип.
Мы можем использовать примеси для расширения функциональности классов, например, для обработки событий, как
мы сделали это выше.
С примесями могут возникнуть конфликты, если они перезаписывают существующие методы класса. Стоит помнить
об этом и быть внимательнее при выборе имён для методов примеси, чтобы их избежать.
Обработка ошибок
Обработка ошибок, "try..catch"
Неважно, насколько мы хороши в программировании, иногда наши скрипты содержат ошибки. Они могут возникать
из-за наших промахов, неожиданного ввода пользователя, неправильного ответа сервера и по тысяче других причин.
381/597
Обычно скрипт в случае ошибки «падает» (сразу же останавливается), с выводом ошибки в консоль.
Но есть синтаксическая конструкция try..catch , которая позволяет «ловить» ошибки и вместо падения делать что-
то более осмысленное.
Синтаксис «try…catch»
try {
// код...
} catch (err) {
// обработка ошибки
Начало
try {
// код...
}
Таким образом, при ошибке в блоке try {…} скрипт не «падает», и мы получаем возможность обработать ошибку
внутри catch .
try {
} catch(err) {
382/597
● Пример с ошибками: выведет (1) и (3) :
try {
} catch(err) {
try {
{{{{{{{{{{{{
} catch(e) {
alert("Движок не может понять этот код, он некорректен");
}
JavaScript-движок сначала читает код, а затем исполняет его. Ошибки, которые возникают во время фазы чтения,
называются ошибками парсинга. Их нельзя обработать (изнутри этого кода), потому что движок не понимает код.
Таким образом, try..catch может обрабатывать только ошибки, которые возникают в корректном коде. Такие
ошибки называют «ошибками во время выполнения», а иногда «исключениями».
try {
setTimeout(function() {
noSuchVariable; // скрипт упадёт тут
}, 1000);
} catch (e) {
alert( "не сработает" );
}
Это потому, что функция выполняется позже, когда движок уже покинул конструкцию try..catch .
Чтобы поймать исключение внутри запланированной функции, try..catch должен находиться внутри самой
этой функции:
setTimeout(function() {
try {
noSuchVariable; // try..catch обрабатывает ошибку!
} catch {
alert( "ошибка поймана!" );
}
}, 1000);
383/597
Объект ошибки
Когда возникает ошибка, JavaScript генерирует объект, содержащий её детали. Затем этот объект передаётся как
аргумент в блок catch :
try {
// ...
} catch(err) { // <-- объект ошибки, можно использовать другое название вместо err
// ...
}
Для всех встроенных ошибок этот объект имеет два основных свойства:
name
Имя ошибки. Например, для неопределённой переменной это "ReferenceError" .
message
Текстовое сообщение о деталях ошибки.
В большинстве окружений доступны и другие, нестандартные свойства. Одно из самых широко используемых и
поддерживаемых – это:
stack
Текущий стек вызова: строка, содержащая информацию о последовательности вложенных вызовов, которые привели
к ошибке. Используется в целях отладки.
Например:
try {
lalala; // ошибка, переменная не определена!
} catch(err) {
alert([Link]); // ReferenceError
alert([Link]); // lalala is not defined
alert([Link]); // ReferenceError: lalala is not defined at (...стек вызовов)
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
try {
// ...
} catch { // <-- без (err)
// ...
}
Использование «try…catch»
Обычно он используется для декодирования данных, полученных по сети, от сервера или из другого источника.
Мы получаем их и вызываем [Link] вот так:
384/597
let json = '{"name":"John", "age": 30}'; // данные с сервера
Вы можете найти более детальную информацию о JSON в главе Формат JSON, метод toJSON.
Если json некорректен, [Link] генерирует ошибку, то есть скрипт «падает».
Устроит ли нас такое поведение? Конечно нет!
Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об
этом не узнает. А люди очень не любят, когда что-то «просто падает» без всякого сообщения об ошибке.
Давайте используем try..catch для обработки ошибки:
try {
} catch (e) {
// ...выполнение прыгает сюда
alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз." );
alert( [Link] );
alert( [Link] );
}
Здесь мы используем блок catch только для вывода сообщения, но мы также можем сделать гораздо больше:
отправить новый сетевой запрос, предложить посетителю альтернативный способ, отослать информацию об ошибке
на сервер для логирования, … Всё лучше, чем просто «падение».
try {
} catch (e) {
alert( "не выполнится" );
}
Здесь [Link] выполнится без ошибок, но на самом деле отсутствие свойства name для нас ошибка.
Для того, чтобы унифицировать обработку ошибок, мы воспользуемся оператором throw .
Оператор «throw»
Оператор throw генерирует ошибку.
Синтаксис:
Технически в качестве объекта ошибки можно передать что угодно. Это может быть даже примитив, число или строка,
но всё же лучше, чтобы это был объект, желательно со свойствами name и message (для совместимости со
встроенными ошибками).
385/597
В JavaScript есть множество встроенных конструкторов для стандартных ошибок: Error , SyntaxError ,
ReferenceError , TypeError и другие. Можно использовать и их для создания объектов ошибки.
Их синтаксис:
Для встроенных ошибок (не для любых объектов, только для ошибок), свойство name – это в точности имя
конструктора. А свойство message берётся из аргумента.
Например:
alert([Link]); // Error
alert([Link]); // Ого, ошибка! o_O
try {
[Link]("{ некорректный json o_O }");
} catch(e) {
alert([Link]); // SyntaxError
alert([Link]); // Expected property name or '}' in JSON at position 2 (line 1 column 3)
}
try {
if (![Link]) {
throw new SyntaxError("Данные неполны: нет имени"); // (*)
}
alert( [Link] );
} catch(e) {
alert( "JSON Error: " + [Link] ); // JSON Error: Данные неполны: нет имени
}
В строке (*) оператор throw генерирует ошибку SyntaxError с сообщением message . Точно такого же вида,
как генерирует сам JavaScript. Выполнение блока try немедленно останавливается, и поток управления прыгает в
catch .
Теперь блок catch становится единственным местом для обработки всех ошибок: и для [Link] и для других
случаев.
Проброс исключения
В примере выше мы использовали try..catch для обработки некорректных данных. А что, если в блоке try
{...} возникнет другая неожиданная ошибка? Например, программная (неопределённая переменная) или какая-то
ещё, а не ошибка, связанная с некорректными данными.
Пример:
386/597
let json = '{ "age": 30 }'; // данные неполны
try {
user = [Link](json); // <-- забыл добавить "let" перед user
// ...
} catch(err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (не JSON ошибка на самом деле)
}
Конечно, возможно все! Программисты совершают ошибки. Даже в утилитах с открытым исходным кодом,
используемых миллионами людей на протяжении десятилетий – вдруг может быть обнаружена ошибка, которая
приводит к ужасным взломам.
В нашем случае try..catch предназначен для выявления ошибок, связанных с некорректными данными. Но по
своей природе catch получает все свои ошибки из try . Здесь он получает неожиданную ошибку, но всё также
показывает то же самое сообщение "JSON Error" . Это неправильно и затрудняет отладку кода.
К счастью, мы можем выяснить, какую ошибку мы получили, например, по её свойству name :
try {
user = { /*...*/ };
} catch(e) {
alert([Link]); // "ReferenceError" из-за неопределённой переменной
}
if (![Link]) {
throw new SyntaxError("Данные неполны: нет имени");
}
alert( [Link] );
} catch(e) {
if ([Link] == "SyntaxError") {
alert( "JSON Error: " + [Link] );
} else {
throw e; // проброс (*)
}
Ошибка в строке (*) из блока catch «выпадает наружу» и может быть поймана другой внешней конструкцией
try..catch (если есть), или «убьёт» скрипт.
Таким образом, блок catch фактически обрабатывает только те ошибки, с которыми он знает, как справляться, и
пропускает остальные.
Пример ниже демонстрирует, как такие ошибки могут быть пойманы с помощью ещё одного уровня try..catch :
387/597
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // ошибка!
} catch (e) {
// ...
if ([Link] != 'SyntaxError') {
throw e; // проброс исключения (не знаю как это обработать)
}
}
}
try {
readData();
} catch (e) {
alert( "Внешний catch поймал: " + e ); // поймал!
}
Здесь readData знает только, как обработать SyntaxError , тогда как внешний блок try..catch знает, как
обработать всё.
try…catch…finally
try {
... пробуем выполнить код...
} catch(e) {
... обрабатываем ошибки ...
} finally {
... выполняем всегда ...
}
try {
alert( 'try' );
if (confirm('Сгенерировать ошибку?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
Секцию finally часто используют, когда мы начали что-то делать и хотим завершить это вне зависимости от того,
будет ошибка или нет.
Например, мы хотим измерить время, которое занимает функция чисел Фибоначчи fib(n) . Естественно, мы можем
начать измерения до того, как функция начнёт выполняться и закончить после. Но что делать, если при вызове
функции возникла ошибка? В частности, реализация fib(n) в коде ниже возвращает ошибку для отрицательных и
для нецелых чисел.
388/597
Секция finally отлично подходит для завершения измерений несмотря ни на что.
Здесь finally гарантирует, что время будет измерено корректно в обеих ситуациях – и в случае успешного
завершения fib и в случае ошибки:
function fib(n) {
if (n < 0 || [Link](n) != n) {
throw new Error("Должно быть целое неотрицательное число");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
try {
result = fib(num);
} catch (e) {
result = 0;
} finally {
diff = [Link]() - start;
}
Вы можете это проверить, запустив этот код и введя 35 в prompt – код завершится нормально, finally
выполнится после try . А затем введите -1 – незамедлительно произойдёт ошибка, выполнение займёт 0ms . Оба
измерения выполняются корректно.
Другими словами, неважно как завершилась функция: через return или throw . Секция finally срабатывает в
обоих случаях.
finally и return
Блок finally срабатывает при любом выходе из try..catch , в том числе и return .
В примере ниже из try происходит return , но finally получает управление до того, как контроль
возвращается во внешний код.
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
389/597
try..finally
Конструкция try..finally без секции catch также полезна. Мы применяем её, когда не хотим здесь
обрабатывать ошибки (пусть выпадут), но хотим быть уверены, что начатые процессы завершились.
function func() {
// начать делать что-то, что требует завершения (например, измерения)
try {
// ...
} finally {
// завершить это, даже если все упадёт
}
}
В приведённом выше коде ошибка всегда выпадает наружу, потому что тут нет блока catch . Но finally
отрабатывает до того, как поток управления выйдет из функции.
Глобальный catch
Зависит от окружения
Информация из данной секции не является частью языка JavaScript.
Давайте представим, что произошла фатальная ошибка (программная или что-то ещё ужасное) снаружи
try..catch , и скрипт упал.
Существует ли способ отреагировать на такие ситуации? Мы можем захотеть залогировать ошибку, показать что-то
пользователю (обычно они не видят сообщение об ошибке) и т.д.
Такого способа нет в спецификации, но обычно окружения предоставляют его, потому что это весьма полезно.
Например, в [Link] для этого есть [Link]("uncaughtException") . А в браузере мы можем присвоить
функцию специальному свойству [Link] , которая будет вызвана в случае необработанной ошибки.
Синтаксис:
message
Сообщение об ошибке.
url
URL скрипта, в котором произошла ошибка.
line , col
Номера строки и столбца, в которых произошла ошибка.
error
Объект ошибки.
Пример:
<script>
[Link] = function(message, url, line, col, error) {
alert(`${message}\n В ${line}:${col} на ${url}`);
};
function readData() {
badFunc(); // Ой, что-то пошло не так!
}
390/597
readData();
</script>
Итого
Конструкция try..catch позволяет обрабатывать ошибки во время исполнения кода. Она позволяет запустить код
и перехватить ошибки, которые могут в нём возникнуть.
Синтаксис:
try {
// исполняем код
} catch(err) {
// если случилась ошибка, прыгаем сюда
// err - это объект ошибки
} finally {
// выполняется всегда после try/catch
}
Секций catch или finally может не быть, то есть более короткие конструкции try..catch и try..finally
также корректны.
Объекты ошибок содержат следующие свойства:
● message – понятное человеку сообщение.
● name – строка с именем ошибки (имя конструктора ошибки).
● stack (нестандартное, но хорошо поддерживается) – стек на момент ошибки.
Если объект ошибки не нужен, мы можем пропустить его, используя catch { вместо catch(err) { .
Мы можем также генерировать собственные ошибки, используя оператор throw . Аргументом throw может быть что
угодно, но обычно это объект ошибки, наследуемый от встроенного класса Error . Подробнее о расширении ошибок
см. в следующей главе.
Проброс исключения – это очень важный приём обработки ошибок: блок catch обычно ожидает и знает, как
обработать определённый тип ошибок, поэтому он должен пробрасывать дальше ошибки, о которых он не знает.
Даже если у нас нет try..catch , большинство сред позволяют настроить «глобальный» обработчик ошибок, чтобы
ловить ошибки, которые «выпадают наружу». В браузере это [Link] .
Задачи
1.
try {
начать работу
работать
391/597
} catch (e) {
обработать ошибку
} finally {
очистить рабочее пространство
}
2.
try {
начать работу
работать
} catch (e) {
обработать ошибку
}
Нам определённо нужна очистка после работы, неважно возникли ошибки или нет.
Есть ли здесь преимущество в использовании finally или оба фрагмента кода одинаковы? Если такое
преимущество есть, то дайте пример, когда оно проявляется.
К решению
Когда что-то разрабатываем, то нам часто необходимы собственные классы ошибок для разных вещей, которые
могут пойти не так в наших задачах. Для ошибок при работе с сетью может понадобиться HttpError , для операций
с базой данных DbError , для поиска – NotFoundError и т.д.
Наши ошибки должны поддерживать базовые свойства, такие как message , name и, желательно, stack . Но также
они могут иметь свои собственные свойства. Например, объекты HttpError могут иметь свойство statusCode со
значениями 404 , 403 или 500 .
JavaScript позволяет вызывать throw с любыми аргументами, то есть технически наши классы ошибок не нуждаются
в наследовании от Error . Но если использовать наследование, то появляется возможность идентификации
объектов ошибок посредством obj instanceof Error . Так что лучше применять наследование.
По мере роста приложения, наши собственные ошибки образуют иерархию, например, HttpTimeoutError может
наследовать от HttpError и так далее.
Расширение Error
В качестве примера рассмотрим функцию readUser(json) , которая должна читать данные пользователя в
формате JSON.
Пример того, как может выглядеть корректный json :
Внутри будем использовать [Link] . При получении некорректного json он будет генерировать ошибку
SyntaxError . Но даже если json синтаксически верен, то это не значит, что это будет корректный пользователь,
верно? Могут быть пропущены необходимые данные. Например, могут отсутствовать свойства name и age , которые
являются необходимыми для наших пользователей.
Наша функция readUser(json) будет не только читать JSON-данные, но и проверять их («валидировать»). Если
необходимые поля отсутствуют или данные в неверном формате, то это будет ошибкой. Но не синтаксической
ошибкой SyntaxError , потому что данные синтаксически корректны. Это будет другая ошибка.
Назовём её ошибкой валидации ValidationError и создадим для неё класс. Ошибка этого вида должна
содержать информацию о поле, которое является источником ошибки.
Наш класс ValidationError должен наследовать от встроенного класса Error .
Класс Error встроенный, вот его примерный код, просто чтобы мы понимали, что расширяем:
392/597
// "Псевдокод" встроенного класса Error, определённого самим JavaScript
class Error {
constructor(message) {
[Link] = message;
[Link] = "Error"; // (разные имена для разных встроенных классов ошибок)
[Link] = <стек вызовов>; // нестандартное свойство, но обычно поддерживается
}
}
function test() {
throw new ValidationError("Упс!");
}
try {
test();
} catch(err) {
alert([Link]); // Упс!
alert([Link]); // ValidationError
alert([Link]); // список вложенных вызовов с номерами строк для каждого
}
Обратите внимание: в строке (1) вызываем родительский конструктор. JavaScript требует от нас вызова super в
дочернем конструкторе, так что это обязательно. Родительский конструктор устанавливает свойство message .
Родительский конструктор также устанавливает свойство name для "Error" , поэтому в строке (2) мы сбрасываем
его на правильное значение.
Попробуем использовать его в readUser(json) :
// Использование
function readUser(json) {
let user = [Link](json);
if (![Link]) {
throw new ValidationError("Нет поля: age");
}
if (![Link]) {
throw new ValidationError("Нет поля: name");
}
return user;
}
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Некорректные данные: " + [Link]); // Некорректные данные: Нет поля: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Ошибка Синтаксиса: " + [Link]);
} else {
throw err; // неизвестная ошибка, пробросить исключение (**)
393/597
}
}
Обратите внимание, как мы используем instanceof для проверки конкретного типа ошибки в строке (*) .
Мы можем также проверить тип, используя [Link] :
// ...
// вместо (err instanceof SyntaxError)
} else if ([Link] == "SyntaxError") { // (*)
// ...
Версия с instanceof гораздо лучше, потому что в будущем мы собираемся расширить ValidationError , сделав
его подтипы, такие как PropertyRequiredError . И проверка instanceof продолжит работать для новых
наследованных классов. Так что это на будущее.
Также важно, что если catch встречает неизвестную ошибку, то он пробрасывает её в строке (**) . Блок catch
знает, только как обрабатывать ошибки валидации и синтаксические ошибки, а другие виды ошибок (из-за опечаток в
коде и другие непонятные) он должен выпустить наружу.
Дальнейшее наследование
Класс ValidationError является слишком общим. Много что может пойти не так. Свойство может отсутствовать
или иметь неверный формат (например, строка как значение возраста age ). Поэтому для отсутствующих свойств
сделаем более конкретный класс PropertyRequiredError . Он будет нести дополнительную информацию о
свойстве, которое отсутствует.
// Применение
function readUser(json) {
let user = [Link](json);
if (![Link]) {
throw new PropertyRequiredError("age");
}
if (![Link]) {
throw new PropertyRequiredError("name");
}
return user;
}
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Неверные данные: " + [Link]); // Неверные данные: Нет свойства: name
alert([Link]); // PropertyRequiredError
alert([Link]); // name
} else if (err instanceof SyntaxError) {
alert("Ошибка синтаксиса JSON: " + [Link]);
} else {
394/597
throw err; // неизвестная ошибка, повторно выбросит исключение
}
}
Новый класс PropertyRequiredError очень просто использовать: необходимо указать только имя свойства new
PropertyRequiredError(property) . Сообщение для пользователя message генерируется конструктором.
Обратите внимание, что свойство [Link] в конструкторе PropertyRequiredError снова присвоено вручную.
Правда, немного утомительно – присваивать [Link] = <class name> в каждом классе пользовательской
ошибки. Можно этого избежать, если сделать наш собственный «базовый» класс ошибки, который будет ставить
[Link] = [Link] . И затем наследовать все ошибки уже от него.
Давайте назовём его MyError .
Вот упрощённый код с MyError и другими пользовательскими классами ошибок:
// name корректное
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Теперь пользовательские ошибки стали намного короче, особенно ValidationError , так как мы избавились от
строки "[Link] = ..." в конструкторе.
Обёртывание исключений
Назначение функции readUser в приведённом выше коде – это «чтение данных пользователя». В процессе могут
возникнуть различные виды ошибок. Сейчас у нас есть SyntaxError и ValidationError , но в будущем функция
readUser может расшириться и, возможно, генерировать другие виды ошибок.
Итак, давайте создадим новый класс ReadError для представления таких ошибок. Если ошибка возникает внутри
readUser , мы её перехватим и сгенерируем ReadError . Мы также сохраним ссылку на исходную ошибку в
свойстве cause . Тогда внешний код должен будет только проверить наличие ReadError .
Этот код определяет ошибку ReadError и демонстрирует её использование в readUser и try..catch :
395/597
class PropertyRequiredError extends ValidationError { /* ... */ }
function validateUser(user) {
if (![Link]) {
throw new PropertyRequiredError("age");
}
if (![Link]) {
throw new PropertyRequiredError("name");
}
}
function readUser(json) {
let user;
try {
user = [Link](json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Синтаксическая ошибка", err);
} else {
throw err;
}
}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Ошибка валидации", err);
} else {
throw err;
}
}
try {
readUser('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Исходная ошибка: SyntaxError:Unexpected token b in JSON at position 1
alert("Исходная ошибка: " + [Link]);
} else {
throw e;
}
}
В приведённом выше коде readUser работает так, как описано – функция распознаёт синтаксические ошибки и
ошибки валидации и выдаёт вместо них ошибки ReadError (неизвестные ошибки, как обычно, пробрасываются).
Внешний код проверяет только instanceof ReadError . Не нужно перечислять все возможные типы ошибок
Этот подход называется «обёртывание исключений», потому что мы берём «исключения низкого уровня» и
«оборачиваем» их в ReadError , который является более абстрактным и более удобным для использования в
вызывающем коде. Такой подход широко используется в объектно-ориентированном программировании.
Итого
● Мы можем наследовать свои классы ошибок от Error и других встроенных классов ошибок, но нужно
позаботиться о свойстве name и не забыть вызвать super .
● Мы можем использовать instanceof для проверки типа ошибок. Это также работает с наследованием. Но иногда
у нас объект ошибки, возникшей в сторонней библиотеке, и нет простого способа получить класс. Тогда для
проверки типа ошибки можно использовать свойство name .
● Обёртывание исключений является распространённой техникой: функция ловит низкоуровневые исключения и
создаёт одно «высокоуровневое» исключение вместо разных низкоуровневых. Иногда низкоуровневые исключения
становятся свойствами этого объекта, как [Link] в примерах выше, но это не обязательно.
396/597
Задачи
Наследование от SyntaxError
важность: 5
Пример использования:
К решению
Промисы, async/await
Введение: колбэки
Если вы не знакомы с этими методами, и их использование в примерах вызывает у вас недоумение, возможно,
вам стоит прочитать несколько глав из следующей части учебника.
Тем не менее, мы все равно попытаемся максимально доходчиво всё разъяснить. Ничего особо сложного в плане
браузера не будет.
function loadScript(src) {
let script = [Link]('script');
[Link] = src;
[Link](script);
}
Эта функция загружает на страницу новый скрипт. Когда в тело документа добавится конструкция <script
src="…"> , браузер загрузит скрипт и выполнит его.
Вот пример использования этой функции:
Такие функции называют «асинхронными», потому что действие (загрузка скрипта) будет завершено не сейчас, а
потом.
Если после вызова loadScript(…) есть какой-то код, то он не будет ждать, пока скрипт загрузится.
loadScript('/my/[Link]');
// код, написанный после вызова функции loadScript,
397/597
// не будет дожидаться полной загрузки скрипта
// ...
Мы хотели бы использовать новый скрипт, как только он будет загружен. Скажем, он объявляет новую функцию,
которую мы хотим выполнить.
Но если мы просто вызовем эту функцию после loadScript(…) , у нас ничего не выйдет:
Действительно, ведь у браузера не было времени загрузить скрипт. Сейчас функция loadScript никак не
позволяет отследить момент загрузки. Скрипт загружается, а потом выполняется. Но нам нужно точно знать, когда это
произойдёт, чтобы использовать функции и переменные из этого скрипта.
Давайте передадим функцию callback вторым аргументом в loadScript , чтобы вызвать её, когда скрипт
загрузится:
Событие onload описано в статье Загрузка ресурсов: onload и onerror, оно в основном выполняет функцию после
загрузки и выполнения скрипта.
Теперь, если мы хотим вызвать функцию из скрипта, нужно делать это в колбэке:
loadScript('/my/[Link]', function() {
// эта функция вызовется после того, как загрузится скрипт
newFunction(); // теперь всё работает
...
});
Смысл такой: вторым аргументом передаётся функция (обычно анонимная), которая выполняется по завершении
действия.
Возьмём для примера реальный скрипт с библиотекой функций:
Колбэк в колбэке
Как нам загрузить два скрипта один за другим: сначала первый, а за ним второй?
Первое, что приходит в голову, вызвать loadScript ещё раз уже внутри колбэка, вот так:
398/597
loadScript('/my/[Link]', function(script) {
loadScript('/my/[Link]', function(script) {
alert(`Здорово, второй скрипт загрузился`);
});
});
Когда внешняя функция loadScript выполнится, вызовется та, что внутри колбэка.
А что если нам нужно загрузить ещё один скрипт?..
loadScript('/my/[Link]', function(script) {
loadScript('/my/[Link]', function(script) {
loadScript('/my/[Link]', function(script) {
// ...и так далее, пока все скрипты не будут загружены
});
})
});
Каждое новое действие мы вынуждены вызывать внутри колбэка. Этот вариант подойдёт, когда у нас одно-два
действия, но для большего количества уже не удобно. Альтернативные подходы мы скоро разберём.
Перехват ошибок
В примерах выше мы не думали об ошибках. А что если загрузить скрипт не удалось? Колбэк должен уметь
реагировать на возможные проблемы.
Ниже улучшенная версия loadScript , которая умеет отслеживать ошибки загрузки:
[Link](script);
}
Мы вызываем callback(null, script) в случае успешной загрузки и callback(error) , если загрузить скрипт
не удалось.
Живой пример:
Опять же, подход, который мы использовали в loadScript , также распространён и называется «колбэк с первым
аргументом-ошибкой» («error-first callback»).
Правила таковы:
1. Первый аргумент функции callback зарезервирован для ошибки. В этом случае вызов выглядит вот так:
callback(err) .
399/597
2. Второй и последующие аргументы — для результатов выполнения. В этом случае вызов выглядит вот так:
callback(null, result1, result2…) .
Одна и та же функция callback используется и для информирования об ошибке, и для передачи результатов.
На первый взгляд это рабочий способ написания асинхронного кода. Так и есть. Для одного или двух вложенных
вызовов всё выглядит нормально.
Но для нескольких асинхронных действий, которые нужно выполнить друг за другом, код выглядит вот так:
if (error) {
handleError(error);
} else {
// ...
loadScript('[Link]', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('[Link]', function(error, script) {
if (error) {
handleError(error);
} else {
// ...и так далее, пока все скрипты не будут загружены (*)
}
});
}
})
}
});
В примере выше:
1. Мы загружаем [Link] . Продолжаем, если нет ошибок.
2. Мы загружаем [Link] . Продолжаем, если нет ошибок.
3. Мы загружаем [Link] . Продолжаем, если нет ошибок. И так далее (*) .
Чем больше вложенных вызовов, тем наш код будет иметь всё большую вложенность, которую сложно поддерживать,
особенно если вместо ... у нас код, содержащий другие цепочки вызовов, условия и т.д.
Пирамида вложенных вызовов растёт вправо с каждым асинхронным действием. В итоге вы сами будете путаться,
где что есть.
Такой подход к написанию кода не приветствуется.
Мы можем попытаться решить эту проблему, изолируя каждое действие в отдельную функцию, вот так:
loadScript('[Link]', step1);
400/597
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('[Link]', step2);
}
}
Заметили? Этот код делает всё то же самое, но вложенность отсутствует, потому что все действия вынесены в
отдельные функции.
Код абсолютно рабочий, но кажется разорванным на куски. Его трудно читать, вы наверняка заметили это.
Приходится прыгать глазами между кусками кода, когда пытаешься его прочесть. Это неудобно, особенно, если
читатель не знаком с кодом и не знает, что за чем следует.
Кроме того, все функции step* одноразовые, и созданы лишь только, чтобы избавиться от «адской пирамиды
вызовов». Никто не будет их переиспользовать где-либо ещё. Таким образом, мы, кроме всего прочего, засоряем
пространство имён.
Нужно найти способ получше.
К счастью, такие способы существуют. Один из лучших — использовать промисы, о которых рассказано в следующей
главе.
Промисы
Представьте, что вы известный певец, которого фанаты постоянно донимают расспросами о предстоящем сингле.
Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в
который они могут записаться. Они могут оставить там свой e-mail, чтобы получить песню, как только она выйдет. И
даже больше: если что-то пойдёт не так, например, в студии будет пожар и песню выпустить не выйдет, они также
получат уведомление об этом.
Все счастливы! Вы счастливы, потому что вас больше не донимают фанаты, а фанаты больше не беспокоятся, что
пропустят новый сингл.
Это аналогия из реальной жизни для ситуаций, с которыми мы часто сталкиваемся в программировании:
1. Есть «создающий» код, который делает что-то, что занимает время. Например, загружает данные по сети. В нашей
аналогии это – «певец».
2. Есть «потребляющий» код, который хочет получить результат «создающего» кода, когда он будет готов. Он может
быть необходим более чем одной функции. Это – «фанаты».
3. Promise (по англ. promise , будем называть такой объект «промис») – это специальный объект в JavaScript,
который связывает «создающий» и «потребляющий» коды вместе. В терминах нашей аналогии – это «список для
подписки». «Создающий» код может выполняться сколько потребуется, чтобы получить результат, а промис делает
результат доступным для кода, который подписан на него, когда результат готов.
Аналогия не совсем точна, потому что объект Promise в JavaScript гораздо сложнее простого списка подписок: он
обладает дополнительными возможностями и ограничениями. Но для начала и такая аналогия хороша.
Синтаксис создания Promise :
401/597
let promise = new Promise(function(resolve, reject) {
// функция-исполнитель (executor)
// "певец"
});
Функция, переданная в конструкцию new Promise , называется исполнитель (executor). Когда Promise создаётся,
она запускается автоматически. Она должна содержать «создающий» код, который когда-нибудь создаст результат. В
терминах нашей аналогии: исполнитель – это «певец».
Её аргументы resolve и reject – это колбэки, которые предоставляет сам JavaScript. Наш код – только внутри
исполнителя.
Когда он получает результат, сейчас или позже – не важно, он должен вызвать один из этих колбэков:
● resolve(value) — если работа завершилась успешно, с результатом value .
● reject(error) — если произошла ошибка, error – объект ошибки.
Итак, исполнитель запускается автоматически, он должен выполнить работу, а затем вызвать resolve или reject .
У объекта promise , возвращаемого конструктором new Promise , есть внутренние свойства:
● state («состояние») — вначале "pending" («ожидание»), потом меняется на "fulfilled" («выполнено
успешно») при вызове resolve или на "rejected" («выполнено с ошибкой») при вызове reject .
● result («результат») — вначале undefined , далее изменяется на value при вызове resolve(value) или на
error при вызове reject(error) .
state: "fulfilled"
result: value
e)
new Promise(executor) valu
lve(
reso
state: "pending"
result: undefined reje
ct(e
rror)
state: "rejected"
result: error
Спустя одну секунду «обработки» исполнитель вызовет resolve("done") , чтобы передать результат:
new Promise(executor)
Это был пример успешно выполненной задачи, в результате мы получили «успешно выполненный» промис.
А теперь пример, в котором исполнитель сообщит, что задача выполнена с ошибкой:
402/597
let promise = new Promise(function(resolve, reject) {
// спустя одну секунду будет сообщено, что задача выполнена с ошибкой
setTimeout(() => reject (new Error("Whoops!")), 1000);
});
new Promise(executor)
Подведём промежуточные итоги: исполнитель выполняет задачу (что-то, что обычно требует времени), затем
вызывает resolve или reject , чтобы изменить состояние соответствующего Promise .
Промис – и успешный, и отклонённый будем называть «завершённым», в отличие от изначального промиса «в
ожидании».
Идея в том, что задача, выполняемая исполнителем, может иметь только один итог: результат или ошибку.
Также заметим, что функция resolve / reject ожидает только один аргумент (или ни одного). Все
дополнительные аргументы будут проигнорированы.
Это может случиться, например, когда мы начали выполнять какую-то задачу, но тут же увидели, что ранее её уже
выполняли, и результат закеширован.
Такая ситуация нормальна. Мы сразу получим успешно завершённый Promise .
403/597
Потребители: then, catch
Объект Promise служит связующим звеном между исполнителем («создающим» кодом или «певцом») и функциями-
потребителями («фанатами»), которые получат либо результат, либо ошибку. Функции-потребители могут быть
зарегистрированы (подписаны) с помощью методов .then и .catch .
then
Наиболее важный и фундаментальный метод – .then .
Синтаксис:
[Link](
function(result) { /* обработает успешное выполнение */ },
function(error) { /* обработает ошибку */ }
);
Первый аргумент метода .then – функция, которая выполняется, когда промис переходит в состояние «выполнен
успешно», и получает результат.
Второй аргумент .then – функция, которая выполняется, когда промис переходит в состояние «выполнен с
ошибкой», и получает ошибку.
Например, вот реакция на успешно выполненный промис:
Если мы заинтересованы только в результате успешного выполнения задачи, то в then можно передать только одну
функцию:
catch
Если мы хотели бы только обработать ошибку, то можно использовать null в качестве первого аргумента:
.then(null, errorHandlingFunction) . Или можно воспользоваться методом
.catch(errorHandlingFunction) , который сделает то же самое:
404/597
// .catch(f) это то же самое, что [Link](null, f)
[Link](alert); // выведет "Error: Ошибка!" спустя одну секунду
Очистка: finally
По аналогии с блоком finally из обычного try {...} catch {...} , у промисов также есть метод finally .
Вызов .finally(f) похож на .then(f, f) , в том смысле, что f выполнится в любом случае, когда промис
завершится: успешно или с ошибкой.
Идея finally состоит в том, чтобы настроить обработчик для выполнения очистки/доведения после завершения
предыдущих операций.
Например, остановка индикаторов загрузки, закрытие больше не нужных соединений и т.д.
Думайте об этом как о завершении вечеринки. Независимо от того, была ли вечеринка хорошей или плохой, сколько
на ней было друзей, нам все равно нужно (или, по крайней мере, мы должны) сделать уборку после нее.
Код может выглядеть следующим образом:
Обратите внимание, что finally(f) – это не совсем псевдоним then(f,f) , как можно было подумать.
Пожалуйста, взгляните на приведенный выше пример: как вы можете видеть, обработчик finally не имеет
аргументов, а результат promise обрабатывается в следующем обработчике.
2. Обработчик finally «пропускает» результат или ошибку дальше, к последующим обработчикам.
Как вы можете видеть, значение возвращаемое первым промисом, передается через finally к следующему
then .
Это очень удобно, потому что finally не предназначен для обработки результата промиса. Как уже было
сказано, это место для проведения общей очистки, независимо от того, каков был результат.
А здесь ошибка из промиса проходит через finally к catch :
3. Обработчик finally также не должен ничего возвращать. Если это так, то возвращаемое значение молча
игнорируется.
405/597
Единственным исключением из этого правила является случай, когда обработчик finally выдает ошибку. Затем
эта ошибка передается следующему обработчику вместо любого предыдущего результата.
Подведем итог:
● Обработчик finally не получает результат предыдущего обработчика (у него нет аргументов). Вместо этого этот
результат передается следующему подходящему обработчику.
● Если обработчик finally возвращает что-то, это игнорируется.
● Когда finally выдает ошибку, выполнение переходит к ближайшему обработчику ошибок.
Эти функции полезны и заставляют все работать правильно, если мы используем finally так, как предполагается:
для общих процедур очистки.
Пример: loadScript
Теперь рассмотрим несколько практических примеров того, как промисы могут облегчить нам написание
асинхронного кода.
У нас есть функция loadScript для загрузки скрипта из предыдущей главы.
Давайте вспомним, как выглядел вариант с колбэками:
[Link](script);
}
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = [Link]('script');
[Link] = src;
[Link](script);
});
}
Применение:
406/597
let promise = loadScript("[Link]
[Link](
script => alert(`${[Link]} загружен!`),
error => alert(`Ошибка: ${[Link]}`)
);
Промисы Колбэки
Таким образом, промисы позволяют улучшить порядок кода и дают нам гибкость. Но это далеко не всё. Мы узнаем
ещё много полезного в последующих главах.
Задачи
[Link](alert);
К решению
Задержка на промисах
Функция delay(ms) должна возвращать промис, который перейдёт в состояние «выполнен» через ms
миллисекунд, так чтобы мы могли добавить к нему .then :
function delay(ms) {
// ваш код
}
К решению
Перепишите функцию showCircle , написанную в задании Анимация круга с помощью колбэка таким образом,
чтобы она возвращала промис, вместо того чтобы принимать в аргументы функцию-callback.
Новое использование:
407/597
[Link]("Hello, world!");
});
К решению
Цепочка промисов
Давайте вернёмся к ситуации из главы Введение: колбэки: у нас есть последовательность асинхронных задач,
которые должны быть выполнены одна за другой. Например, речь может идти о загрузке скриптов. Как же грамотно
реализовать это в коде?
Промисы предоставляют несколько способов решения подобной задачи.
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
Идея состоит в том, что результат первого промиса передаётся по цепочке обработчиков .then .
Поток выполнения такой:
В итоге результат передаётся по цепочке обработчиков, и мы видим несколько alert подряд, которые выводят: 1
→ 2 → 4.
new Promise
resolve(1)
.then
return 2
.then
return 4
.then
408/597
Всё это работает, потому что вызов [Link] тоже возвращает промис, так что мы можем вызвать на нём
следующий .then .
Когда обработчик возвращает какое-то значение, то оно становится результатом выполнения соответствующего
промиса и передаётся в следующий .then .
Классическая ошибка новичков: технически возможно добавить много обработчиков .then к единственному
промису. Но это не цепочка.
Например:
[Link](function(result) {
alert(result); // 1
return result * 2;
});
[Link](function(result) {
alert(result); // 1
return result * 2;
});
[Link](function(result) {
alert(result); // 1
return result * 2;
});
Мы добавили несколько обработчиков к одному промису. Они не передают друг другу результаты своего выполнения,
а действуют независимо.
new Promise
resolve(1)
Все обработчики .then на одном и том же промисе получают одно и то же значение – результат выполнения того же
самого промиса. Таким образом, в коде выше все alert показывают одно и то же: 1 .
На практике весьма редко требуется назначать несколько обработчиков одному промису. А вот цепочка промисов
используется куда чаще.
Возвращаем промисы
}).then(function(result) {
alert(result); // 1
}).then(function(result) { // (**)
alert(result); // 2
409/597
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
Здесь первый .then показывает 1 и возвращает новый промис new Promise(…) в строке (*) . Через одну
секунду этот промис успешно выполняется, и его результат (аргумент в resolve , то есть result * 2 ) передаётся
обработчику в следующем .then . Он находится в строке (**) , показывает 2 и делает то же самое.
Таким образом, как и в предыдущем примере, выводятся 1 → 2 → 4, но сейчас между вызовами alert существует
пауза в 1 секунду.
Возвращая промисы, мы можем строить цепочки из асинхронных действий.
Пример: loadScript
Давайте используем эту возможность вместе с промисифицированной функцией loadScript , созданной нами в
предыдущей главе, чтобы загружать скрипты по очереди, последовательно:
loadScript("/article/promise-chaining/[Link]")
.then(function(script) {
return loadScript("/article/promise-chaining/[Link]");
})
.then(function(script) {
return loadScript("/article/promise-chaining/[Link]");
})
.then(function(script) {
// вызовем функции, объявленные в загружаемых скриптах,
// чтобы показать, что они действительно загрузились
one();
two();
three();
});
loadScript("/article/promise-chaining/[Link]")
.then(script => loadScript("/article/promise-chaining/[Link]"))
.then(script => loadScript("/article/promise-chaining/[Link]"))
.then(script => {
// скрипты загружены, мы можем использовать объявленные в них функции
one();
two();
three();
});
Здесь каждый вызов loadScript возвращает промис, и следующий обработчик в .then срабатывает, только когда
этот промис завершается. Затем инициируется загрузка следующего скрипта и так далее. Таким образом, скрипты
загружаются один за другим.
Мы можем добавить и другие асинхронные действия в цепочку. Обратите внимание, что наш код всё ещё «плоский»,
он «растёт» вниз, а не вправо. Нет никаких признаков «адской пирамиды вызовов».
Технически мы бы могли добавлять .then напрямую к каждому вызову loadScript , вот так:
loadScript("/article/promise-chaining/[Link]").then(script1 => {
loadScript("/article/promise-chaining/[Link]").then(script2 => {
loadScript("/article/promise-chaining/[Link]").then(script3 => {
// эта функция имеет доступ к переменным script1, script2 и script3
one();
two();
three();
});
410/597
});
});
Этот код делает то же самое: последовательно загружает 3 скрипта. Но он «растёт вправо», так что возникает такая
же проблема, как и с колбэками.
Разработчики, которые не так давно начали использовать промисы, иногда не знают про цепочки и пишут код именно
так, как показано выше. В целом, использование цепочек промисов предпочтительнее.
Иногда всё же приемлемо добавлять .then напрямую, чтобы вложенная в него функция имела доступ к внешней
области видимости. В примере выше самая глубоко вложенная функция обратного вызова имеет доступ ко всем
переменным script1 , script2 , script3 . Но это скорее исключение, чем правило.
Thenable
Если быть более точными, обработчик может возвращать не именно промис, а любой объект, содержащий метод
.then , такие объекты называют «thenable», и этот объект будет обработан как промис.
Смысл в том, что сторонние библиотеки могут создавать свои собственные совместимые с промисами объекты.
Они могут иметь свои наборы методов и при этом быть совместимыми со встроенными промисами, так как
реализуют метод .then .
Вот пример такого объекта:
class Thenable {
constructor(num) {
[Link] = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// будет успешно выполнено с аргументом [Link]*2 через 1 секунду
setTimeout(() => resolve([Link] * 2), 1000); // (**)
}
}
JavaScript проверяет объект, возвращаемый из обработчика .then в строке (*) : если у него имеется метод
then , который можно вызвать, то этот метод вызывается, и в него передаются как аргументы встроенные
функции resolve и reject , вызов одной из которых потом ожидается. В примере выше происходит вызов
resolve(2) через 1 секунду (**) . Затем результат передаётся дальше по цепочке.
Это позволяет добавлять в цепочки промисов пользовательские объекты, не заставляя их наследовать от
Promise .
Во фронтенд-разработке промисы часто используются, чтобы делать запросы по сети. Давайте рассмотрим один
такой пример.
Мы будем использовать метод fetch, чтобы подгрузить информацию о пользователях с удалённого сервера. Этот
метод имеет много опциональных параметров, разобранных в соответствующих разделах, но базовый синтаксис
весьма прост:
Этот код запрашивает по сети url и возвращает промис. Промис успешно выполняется и в свою очередь
возвращает объект response после того, как удалённый сервер присылает заголовки ответа, но до того, как весь
ответ сервера полностью загружен.
Чтобы прочитать полный ответ, надо вызвать метод [Link]() : он тоже возвращает промис, который
выполняется, когда данные полностью загружены с удалённого сервера, и возвращает эти данные.
411/597
fetch('/article/promise-chaining/[Link]')
// .then в коде ниже выполняется, когда удалённый сервер отвечает
.then(function(response) {
// [Link]() возвращает новый промис,
// который выполняется и возвращает полный ответ сервера,
// когда он загрузится
return [Link]();
})
.then(function(text) {
// ...и здесь содержимое полученного файла
alert(text); // {"name": "iliakan", isAdmin: true}
});
Есть также метод [Link]() , который читает данные в формате JSON. Он больше подходит для нашего
примера, так что давайте использовать его.
Мы также применим стрелочные функции для более компактной записи кода:
// то же самое, что и раньше, только теперь [Link]() читает данные в формате JSON
fetch('/article/promise-chaining/[Link]')
.then(response => [Link]())
.then(user => alert([Link])); // iliakan, получили имя пользователя
Например, мы можем послать запрос на GitHub, чтобы загрузить данные из профиля пользователя и показать его
аватар:
// Запрашиваем [Link]
fetch('/article/promise-chaining/[Link]')
// Загружаем данные в формате json
.then(response => [Link]())
// Делаем запрос к GitHub
.then(user => fetch(`[Link]
// Загружаем ответ в формате json
.then(response => [Link]())
// Показываем аватар (githubUser.avatar_url) в течение 3 секунд (возможно, с анимацией)
.then(githubUser => {
let img = [Link]('img');
[Link] = githubUser.avatar_url;
[Link] = "promise-avatar-example";
[Link](img);
Код работает, детали реализации отражены в комментариях. Однако в нём есть одна потенциальная проблема, с
которой часто сталкиваются новички.
Посмотрите на строку (*) : как мы можем предпринять какие-то действия после того, как аватар был показан и
удалён? Например, мы бы хотели показывать форму редактирования пользователя или что-то ещё. Сейчас это
невозможно.
Чтобы сделать наш код расширяемым, нам нужно возвращать ещё один промис, который выполняется после того, как
завершается показ аватара.
Примерно так:
fetch('/article/promise-chaining/[Link]')
.then(response => [Link]())
.then(user => fetch(`[Link]
.then(response => [Link]())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = [Link]('img');
[Link] = githubUser.avatar_url;
[Link] = "promise-avatar-example";
[Link](img);
setTimeout(() => {
[Link]();
412/597
resolve(githubUser); // (**)
}, 3000);
}))
// срабатывает через 3 секунды
.then(githubUser => alert(`Закончили показ ${[Link]}`));
То есть, обработчик .then в строке (*) будет возвращать new Promise , который перейдёт в состояние
«выполнен» только после того, как в setTimeout (**) будет вызвана resolve(githubUser) .
Соответственно, следующий по цепочке .then будет ждать этого.
Как правило, все асинхронные действия должны возвращать промис.
Это позволяет планировать после него какие-то дополнительные действия. Даже если эта возможность не нужна
прямо сейчас, она может понадобиться в будущем.
И, наконец, давайте разобьём написанный код на отдельные функции, пригодные для повторного использования:
function loadJson(url) {
return fetch(url)
.then(response => [Link]());
}
function loadGithubUser(name) {
return fetch(`[Link]
.then(response => [Link]());
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = [Link]('img');
[Link] = githubUser.avatar_url;
[Link] = "promise-avatar-example";
[Link](img);
setTimeout(() => {
[Link]();
resolve(githubUser);
}, 3000);
});
}
// Используем их:
loadJson('/article/promise-chaining/[Link]')
.then(user => loadGithubUser([Link]))
.then(showAvatar)
.then(githubUser => alert(`Показ аватара ${[Link]} завершён`));
// ...
Итого
Если обработчик в .then (или в catch/finally , без разницы) возвращает промис, последующие элементы
цепочки ждут, пока этот промис выполнится. Когда это происходит, результат его выполнения (или ошибка)
передаётся дальше.
Вот полная картина происходящего:
413/597
вызов .then(handler) всегда возвращает промис:
state: "pending"
result: undefined
Задачи
Являются ли фрагменты кода ниже эквивалентными? Другими словами, ведут ли они себя одинаково во всех
обстоятельствах, для всех переданных им обработчиков?
[Link](f1).catch(f2);
Против:
[Link](f1, f2);
К решению
fetch('[Link] // ошибка
.then(response => [Link]())
.catch(err => alert(err)) // TypeError: failed to fetch (текст может отличаться)
Как видно, .catch не обязательно должен быть сразу после ошибки, он может быть далее, после одного или даже
нескольких .then
Или, может быть, с сервером всё в порядке, но в ответе мы получим некорректный JSON. Самый лёгкий путь
перехватить все ошибки – это добавить .catch в конец цепочки:
fetch('/article/promise-chaining/[Link]')
.then(response => [Link]())
.then(user => fetch(`[Link]
.then(response => [Link]())
.then(githubUser => new Promise((resolve, reject) => {
let img = [Link]('img');
[Link] = githubUser.avatar_url;
[Link] = "promise-avatar-example";
[Link](img);
setTimeout(() => {
[Link]();
resolve(githubUser);
414/597
}, 3000);
}))
.catch(error => alert([Link]));
Если все в порядке, то такой .catch вообще не выполнится. Но если любой из промисов будет отклонён (проблемы
с сетью или некорректная json-строка, или что угодно другое), то ошибка будет перехвачена.
Неявный try…catch
Вокруг функции промиса и обработчиков находится «невидимый try..catch ». Если происходит исключение, то оно
перехватывается, и промис считается отклонённым с этой ошибкой.
Например, этот код:
Это происходит для всех ошибок, не только для тех, которые вызваны оператором throw . Например, программная
ошибка:
Финальный .catch перехватывает как промисы, в которых вызван reject , так и случайные ошибки в
обработчиках.
Пробрасывание ошибок
Как мы уже заметили, .catch ведёт себя как try..catch . Мы можем иметь столько обработчиков .then , сколько
мы хотим, и затем использовать один .catch в конце, чтобы перехватить ошибки из всех обработчиков.
В обычном try..catch мы можем проанализировать ошибку и повторно пробросить дальше, если не можем её
обработать. То же самое возможно для промисов.
Если мы пробросим ( throw ) ошибку внутри блока .catch , то управление перейдёт к следующему ближайшему
обработчику ошибок. А если мы обработаем ошибку и завершим работу обработчика нормально, то продолжит
работу ближайший успешный обработчик .then .
В примере ниже .catch успешно обрабатывает ошибку:
415/597
// the execution: catch -> then
new Promise((resolve, reject) => {
}).catch(function(error) {
Здесь блок .catch завершается нормально. Поэтому вызывается следующий успешный обработчик .then .
В примере ниже мы видим другую ситуацию с блоком .catch . Обработчик (*) перехватывает ошибку и не может
обработать её (например, он знает как обработать только URIError ), поэтому ошибка пробрасывается далее:
}).catch(function(error) { // (*)
}).then(function() {
/* не выполнится */
}).catch(error => { // (**)
});
Управление переходит от первого блока .catch (*) к следующему (**) , вниз по цепочке.
Необработанные ошибки
Что произойдёт, если ошибка не будет обработана? Например, мы просто забыли добавить .catch в конец цепочки,
как здесь:
new Promise(function() {
noSuchFunction(); // Ошибка (нет такой функции)
})
.then(() => {
// обработчики .then, один или более
}); // без .catch в самом конце!
В случае ошибки выполнение должно перейти к ближайшему обработчику ошибок. Но в примере выше нет никакого
обработчика. Поэтому ошибка как бы «застревает», её некому обработать.
На практике, как и при обычных необработанных ошибках в коде, это означает, что что-то пошло сильно не так.
Что происходит, когда обычная ошибка не перехвачена try..catch ? Скрипт умирает с сообщением в консоли.
Похожее происходит и в случае необработанной ошибки промиса.
JavaScript-движок отслеживает такие ситуации и генерирует в этом случае глобальную ошибку. Вы можете увидеть её
в консоли, если запустите пример выше.
В браузере мы можем поймать такие ошибки, используя событие unhandledrejection :
416/597
[Link]('unhandledrejection', function(event) {
// объект события имеет два специальных свойства:
alert([Link]); // [object Promise] - промис, который сгенерировал ошибку
alert([Link]); // Error: Ошибка! - объект ошибки, которая не была обработана
});
new Promise(function() {
throw new Error("Ошибка!");
}); // нет обработчика ошибок
Итого
● .catch перехватывает все виды ошибок в промисах: будь то вызов reject() или ошибка, брошенная в
обработчике при помощи throw .
● .then также перехватывает ошибки таким же образом, если задан второй аргумент (который является
обработчиком ошибок).
●
Необходимо размещать .catch там, где мы хотим обработать ошибки и знаем, как это сделать. Обработчик
может проанализировать ошибку (могут быть полезны пользовательские классы ошибок) и пробросить её, если
ничего не знает о ней (возможно, это программная ошибка).
● Можно и совсем не использовать .catch , если нет нормального способа восстановиться после ошибки.
●
В любом случае нам следует использовать обработчик события unhandledrejection (для браузеров и аналог
для других окружений), чтобы отслеживать необработанные ошибки и информировать о них пользователя (и,
возможно, наш сервер), благодаря чему наше приложение никогда не будет «просто умирать».
Задачи
Ошибка в setTimeout
К решению
Promise API
[Link]
Допустим, нам нужно запустить множество промисов параллельно и дождаться, пока все они выполнятся.
Например, параллельно загрузить несколько файлов и обработать результат, когда он готов.
Для этого как раз и пригодится [Link] .
Синтаксис:
417/597
let promise = [Link](iterable);
Метод [Link] принимает массив промисов (может принимать любой перебираемый объект, но обычно
используется массив) и возвращает новый промис.
Новый промис завершится, когда завершится весь переданный список промисов, и его результатом будет массив их
результатов.
Например, [Link] , представленный ниже, выполнится спустя 3 секунды, его результатом будет массив [1,
2, 3] :
[Link]([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // когда все промисы выполнятся, результат будет 1,2,3
// каждый промис даёт элемент массива
Обратите внимание, что порядок элементов массива в точности соответствует порядку исходных промисов. Даже
если первый промис будет выполняться дольше всех, его результат всё равно будет первым в массиве.
Часто применяемый трюк – пропустить массив данных через map-функцию, которая для каждого элемента создаст
задачу-промис, и затем обернуть получившийся массив в [Link] .
Например, если у нас есть массив ссылок, то мы можем загрузить их вот так:
let urls = [
'[Link]
'[Link]
'[Link]
];
А вот пример побольше, с получением информации о пользователях GitHub по их логинам из массива (мы могли бы
получать массив товаров по их идентификаторам, логика та же):
[Link](requests)
.then(responses => {
// все промисы успешно завершены
for(let response of responses) {
alert(`${[Link]}: ${[Link]}`); // покажет 200 для каждой ссылки
}
return responses;
})
// преобразовать массив ответов response в [Link](),
// чтобы прочитать содержимое каждого
.then(responses => [Link]([Link](r => [Link]())))
// все JSON-ответы обработаны, users - массив с результатами
.then(users => [Link](user => alert([Link])));
Например:
418/597
[Link]([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Ошибка!
Здесь второй промис завершится с ошибкой через 2 секунды. Это приведёт к немедленной ошибке в [Link] ,
так что выполнится .catch : ошибка этого промиса становится ошибкой всего [Link] .
[Link]([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1, 2, 3
Таким образом, мы можем передавать уже готовые значения, которые не являются промисами, в [Link] ,
иногда это бывает удобно.
[Link]
Новая возможность
Эта возможность была добавлена в язык недавно. В старых браузерах может понадобиться полифил.
Синтаксис:
[Link] завершается с ошибкой, если она возникает в любом из переданных промисов. Это подходит для
ситуаций «всё или ничего», когда нам нужны все результаты для продолжения:
[Link]([
fetch('/[Link]'),
fetch('/[Link]'),
fetch('/[Link]')
]).then(render); // методу render нужны результаты всех fetch
Метод [Link] всегда ждёт завершения всех промисов. В массиве результатов будет
● {status:"fulfilled", value:результат} для успешных завершений,
● {status:"rejected", reason:ошибка} для ошибок.
419/597
Например, мы хотели бы загрузить информацию о множестве пользователей. Даже если в каком-то запросе ошибка,
нас всё равно интересуют остальные.
Используем для этого [Link] :
let urls = [
'[Link]
'[Link]
'[Link]
];
[
{status: 'fulfilled', value: ...объект ответа...},
{status: 'fulfilled', value: ...объект ответа...},
{status: 'rejected', reason: ...объект ошибки...}
]
Полифил
Если браузер не поддерживает [Link] , для него легко сделать полифил:
if(![Link]) {
[Link] = function(promises) {
return [Link]([Link](p => [Link](p).then(value => ({
status: 'fulfilled',
value: value
}), error => ({
status: 'rejected',
reason: error
}))));
};
}
В этом коде [Link] берёт аргументы, превращает их в промисы (на всякий случай) и добавляет каждому
обработчик .then .
Этот обработчик превращает успешный результат value в {state:'fulfilled', value: value} , а ошибку
error в {state:'rejected', reason: error} . Это как раз и есть формат результатов [Link] .
Затем мы можем использовать [Link] , чтобы получить результаты всех промисов, даже если при
выполнении какого-то возникнет ошибка.
[Link]
Метод очень похож на [Link] , но ждёт только первый выполненный промис, из которого берёт результат (или
ошибку).
Синтаксис:
420/597
[Link]([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
Быстрее всех выполнился первый промис, он и дал результат. После этого остальные промисы игнорируются.
[Link]
Метод очень похож на [Link] , но ждёт только первый успешно выполненный промис, из которого берёт
результат.
Если ни один из переданных промисов не завершится успешно, тогда возвращённый объект Promise будет отклонён с
помощью AggregateError – специального объекта ошибок, который хранит все ошибки промисов в своём свойстве
errors .
Синтаксис:
[Link]([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
Первый промис в этом примере был самым быстрым, но он был отклонён, поэтому результатом стал второй. После
того, как первый успешно выполненный промис «выиграет гонку», все дальнейшие результаты будут
проигнорированы.
[Link]([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ещё одна ошибка!")), 2000))
]).catch(error => {
[Link]([Link]); // AggregateError
[Link]([Link][0]); // Error: Ошибка!
[Link]([Link][1]); // Error: Ещё одна ошибка!
});
Как вы можете видеть, объекты ошибок для отклонённых промисов доступны в свойстве errors объекта
AggregateError .
[Link]/reject
Методы [Link] и [Link] редко используются в современном коде, так как синтаксис
async/await (мы рассмотрим его чуть позже) делает их, в общем-то, не нужными.
Мы рассмотрим их здесь для полноты картины, а также для тех, кто по каким-то причинам не может использовать
async/await .
[Link]
То же самое, что:
421/597
Этот метод используют для совместимости: когда ожидается, что функция возвратит именно промис.
Например, функция loadCached ниже загружает URL и запоминает (кеширует) его содержимое. При будущих
вызовах с тем же URL он тут же читает предыдущее содержимое из кеша, но использует [Link] , чтобы
сделать из него промис, для того, чтобы возвращаемое значение всегда было промисом:
function loadCached(url) {
if ([Link](url)) {
return [Link]([Link](url)); // (*)
}
return fetch(url)
.then(response => [Link]())
.then(text => {
[Link](url,text);
return text;
});
}
Мы можем писать loadCached(url).then(…) , потому что функция loadCached всегда возвращает промис. Мы
всегда можем использовать .then после loadCached . Это и есть цель использования [Link] в
строке (*) .
[Link]
То же самое, что:
Итого
Промисификация
Промисификация – это длинное слово для простого преобразования. Мы берём функцию, которая принимает колбэк
и меняем её, чтобы она вместо этого возвращала промис.
Такие преобразования часто необходимы в реальной жизни, так как многие функции и библиотеки основаны на
колбэках, а использование промисов более удобно, поэтому есть смысл «промисифицировать» их.
422/597
Например, у нас есть loadScript(src, callback) из главы Введение: колбэки.
[Link](script);
}
// использование:
// loadScript('path/[Link]', (err, script) => {...})
Давайте промисифицируем её. Новая функция loadScriptPromise(src) будет делать то же самое, но будет
принимать только src (не callback ) и возвращать промис.
// использование:
// loadScriptPromise('path/[Link]').then(...)
function promisify(f) {
return function (...args) { // возвращает функцию-обёртку
return new Promise((resolve, reject) => {
function callback(err, result) { // наш специальный колбэк для f
if (err) {
reject(err);
} else {
resolve(result);
}
}
// использование:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
Здесь мы предполагаем, что исходная функция ожидает колбэк с двумя аргументами (err, result) . Это то, с чем
мы чаще всего сталкиваемся. Тогда наш колбэк – в правильном формате, и promisify отлично работает для такого
случая.
423/597
Но что, если исходная f ожидает колбэк с большим количеством аргументов callback(err, res1, res2,
...) ?
Ниже описана улучшенная функция promisify : при вызове promisify(f, true) результатом промиса будет
массив результатов [res1, res2, ...] :
[Link](callback);
[Link](this, ...args);
});
};
};
// использование:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
Для более экзотических форматов колбэка, например без err : callback(result) , мы можем промисифицировать
функции без помощника, «вручную».
Также существуют модули с более гибкой промисификацией, например, es6-promisify или встроенная функция
[Link] в [Link].
На заметку:
Промисификация – это отличный подход, особенно, если вы будете использовать async/await (см. следующую
главу об Async/await) но она не является тотальной заменой любых колбэков.
Помните, промис может иметь только один результат, но колбэк технически может вызываться сколько угодно раз.
Поэтому промисификация используется для функций, которые вызывают колбэк только один раз. Последующие
вызовы колбэка будут проигнорированы.
Микрозадачи
Даже когда промис сразу же выполнен, код в строках ниже .then / .catch / .finally будет запущен до этих
обработчиков.
Вот демо:
Если вы запустите его, сначала вы увидите код выполнен , а потом промис выполнен .
Это странно, потому что промис определённо был выполнен с самого начала.
Почему .then срабатывает позже? Что происходит?
424/597
Очередь микрозадач
Асинхронные задачи требуют правильного управления. Для этого стандарт предусматривает внутреннюю очередь
PromiseJobs , более известную как «очередь микрозадач (microtask queue)» (термин V8).
Как сказано в спецификации :
●
Очередь определяется как первым-пришёл-первым-ушёл (FIFO): задачи, попавшие в очередь первыми,
выполняются тоже первыми.
● Выполнение задачи происходит только в том случае, если ничего больше не запущено.
Или, проще говоря, когда промис выполнен, его обработчики .then/catch/finally попадают в очередь. Они пока
не выполняются. Движок JavaScript берёт задачу из очереди и выполняет её, когда он освободится от выполнения
текущего кода.
Вот почему сообщение «код выполнен» в примере выше будет показано первым.
Если есть цепочка с несколькими .then/catch/finally , то каждый из них выполняется асинхронно. То есть
сначала ставится в очередь, а потом выполняется, когда выполнение текущего кода завершено и добавленные ранее
в очередь обработчики выполнены.
Но что если порядок имеет значение для нас? Как мы можем вывести код выполнен после промис
выполнен ?
Легко, используя .then :
[Link]()
.then(() => alert("промис выполнен!"))
.then(() => alert("код выполнен"));
Необработанные ошибки
…Но если мы забудем добавить .catch , то, когда очередь микрозадач опустеет, движок сгенерирует событие:
425/597
// Ошибка в промисе!
[Link]('unhandledrejection', event => alert([Link]));
// Ошибка в промисе!
[Link]('unhandledrejection', event => alert([Link]));
Но теперь мы понимаем, что событие unhandledrejection возникает, когда очередь микрозадач завершена:
движок проверяет все промисы и, если какой-либо из них в состоянии «rejected», то генерируется это событие.
В примере выше .catch , добавленный в setTimeout , также срабатывает, но позже, уже после возникновения
unhandledrejection , так что это ни на что не влияет.
Итого
Обработка промисов всегда асинхронная, т.к. все действия промисов проходят через внутреннюю очередь «promise
jobs», так называемую «очередь микрозадач (microtask queue)» (термин V8).
Таким образом, обработчики .then/catch/finally вызываются после выполнения текущего кода.
Если нам нужно гарантировать выполнение какого-то кода после .then/catch/finally , то лучше всего добавить
его вызов в цепочку .then .
В большинстве движков JavaScript, включая браузеры и [Link], микрозадачи тесно связаны с так называемым
«событийным циклом» и «макрозадачами». Так как они не связаны напрямую с промисами, то рассматриваются в
другой части учебника, в главе Событийный цикл: микрозадачи и макрозадачи.
Async/await
Существует специальный синтаксис для работы с промисами, который называется «async/await». Он удивительно
прост для понимания и использования.
Асинхронные функции
Начнём с ключевого слова async . Оно ставится перед функцией, вот так:
У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются
в завершившийся успешно промис автоматически.
Например, эта функция возвратит выполненный промис с результатом 1 :
f().then(alert); // 1
426/597
}
f().then(alert); // 1
Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис.
Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово – await , которое можно
использовать только внутри async -функций.
Await
Синтаксис:
Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не
выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.
В этом примере промис успешно выполнится через 1 секунду:
let result = await promise; // будет ждать, пока промис не выполнится (*)
alert(result); // "готово!"
}
f();
В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это
произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат
выполнения промиса, и браузер отобразит alert-окно «готово!».
Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов
процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие
скрипты, обрабатывать события и т.п.
По сути, это просто «синтаксический сахар» для получения результата промиса, более наглядный, чем
[Link] .
function f() {
let promise = [Link](1);
let result = await promise; // SyntaxError
}
Ошибки не будет, если мы укажем ключевое слово async перед объявлением функции. Как было сказано
раньше, await можно использовать только внутри async –функций.
427/597
let response = await fetch('/article/promise-chaining/[Link]');
let user = await [Link]();
[Link]();
return githubUser;
}
showAvatar();
Можно обернуть этот код в анонимную async –функцию, тогда всё заработает:
(async () => {
let response = await fetch('/article/promise-chaining/[Link]');
let user = await [Link]();
...
})();
428/597
await работает с «thenable»–объектами
Как и [Link] , await позволяет работать с промис–совместимыми объектами. Идея в том, что если у
объекта можно вызвать метод then , этого достаточно, чтобы использовать его с await .
В примере ниже, экземпляры класса Thenable будут работать вместе с await :
class Thenable {
constructor(num) {
[Link] = num;
}
then(resolve, reject) {
alert(resolve);
// выполнить resolve со значением [Link] * 2 через 1000мс
setTimeout(() => resolve([Link] * 2), 1000); // (*)
}
};
f();
Когда await получает объект с .then , не являющийся промисом, JavaScript автоматически запускает этот
метод, передавая ему аргументы – встроенные функции resolve и reject . Затем await приостановит
дальнейшее выполнение кода, пока любая из этих функций не будет вызвана (в примере это строка (*) ). После
чего выполнение кода продолжится с результатом resolve или reject соответственно.
class Waiter {
async wait() {
return await [Link](1);
}
}
new Waiter()
.wait()
.then(alert); // 1
Как и в случае с асинхронными функциями, такой метод гарантированно возвращает промис, и в его теле можно
использовать await .
Обработка ошибок
Когда промис завершается успешно, await promise возвращает результат. Когда завершается с ошибкой – будет
выброшено исключение. Как если бы на этом месте находилось выражение throw .
Такой код:
429/597
Но есть отличие: на практике промис может завершиться с ошибкой не сразу, а через некоторое время. В этом случае
будет задержка, а затем await выбросит исключение.
Такие ошибки можно ловить, используя try..catch , как с обычным throw :
try {
let response = await fetch('[Link]
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
В случае ошибки выполнение try прерывается и управление прыгает в начало блока catch . Блоком try можно
обернуть несколько строк:
try {
let response = await fetch('/no-user-here');
let user = await [Link]();
} catch(err) {
// перехватит любую ошибку в блоке try: и в fetch, и в [Link]
alert(err);
}
}
f();
Если у нас нет try..catch , асинхронная функция будет возвращать завершившийся с ошибкой промис (в
состоянии rejected ). В этом случае мы можем использовать метод .catch промиса, чтобы обработать ошибку:
Если забыть добавить .catch , то будет сгенерирована ошибка «Uncaught promise error» и информация об этом
будет выведена в консоль. Такие ошибки можно поймать глобальным обработчиком, о чём подробно написано в
разделе Промисы: обработка ошибок.
async/await и [Link]/catch
При работе с async/await , .then используется нечасто, так как await автоматически ожидает завершения
выполнения промиса. В этом случае обычно (но не всегда) гораздо удобнее перехватывать ошибки, используя
try..catch , нежели чем .catch .
Но на верхнем уровне вложенности (вне async –функций) await использовать нельзя, поэтому .then/catch
для обработки финального результата или ошибок – обычная практика.
Так сделано в строке (*) в примере выше.
430/597
async/await отлично работает с [Link]
Когда необходимо подождать несколько промисов одновременно, можно обернуть их в [Link] , и затем
await :
В случае ошибки она будет передаваться как обычно: от завершившегося с ошибкой промиса к [Link] . А
после будет сгенерировано исключение, которое можно отловить, обернув выражение в try..catch .
Итого
Ключевое слово await перед промисом заставит JavaScript дождаться его выполнения, после чего:
1. Если промис завершается с ошибкой, будет сгенерировано исключение, как если бы на этом месте находилось
throw .
2. Иначе вернётся результат промиса.
Вместе они предоставляют отличный каркас для написания асинхронного кода. Такой код легко и писать, и читать.
Хотя при работе с async/await можно обходиться без [Link]/catch , иногда всё-таки приходится
использовать эти методы (на верхнем уровне вложенности, например). Также await отлично работает в сочетании с
[Link] , если необходимо выполнить несколько задач параллельно.
Задачи
Перепишите один из примеров раздела Цепочка промисов, используя async/await вместо .then/catch :
function loadJson(url) {
return fetch(url)
.then(response => {
if ([Link] == 200) {
return [Link]();
} else {
throw new Error([Link]);
}
})
}
loadJson('[Link]') // (3)
.catch(alert); // Error: 404
К решению
Ниже пример из раздела Цепочка промисов, перепишите его, используя async/await вместо .then/catch .
В функции demoGithubUser замените рекурсию на цикл: используя async/await , сделать это будет просто.
431/597
[Link] = 'HttpError';
[Link] = response;
}
}
function loadJson(url) {
return fetch(url)
.then(response => {
if ([Link] == 200) {
return [Link]();
} else {
throw new HttpError(response);
}
})
}
return loadJson(`[Link]
.then(user => {
alert(`Полное имя: ${[Link]}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && [Link] == 404) {
alert("Такого пользователя не существует, пожалуйста, повторите ввод.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
К решению
Есть «обычная» функция. Как можно внутри неё получить результат выполнения async –функции?
return 10;
}
function f() {
// ...что здесь написать?
// чтобы вызвать wait() и дождаться результата "10" от async–функции
// не забывайте, здесь нельзя использовать "await"
}
P.S. Технически задача очень простая, но этот вопрос часто задают разработчики, недавно познакомившиеся с
async/await.
К решению
Генераторы могут порождать (yield) множество значений одно за другим, по мере необходимости. Генераторы
отлично работают с перебираемыми объектами и позволяют легко создавать потоки данных.
432/597
Функция-генератор
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Функции-генераторы ведут себя не так, как обычные. Когда такая функция вызвана, она не выполняет свой код.
Вместо этого она возвращает специальный объект, так называемый «генератор», для управления её выполнением.
Вот, посмотрите:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Основным методом генератора является next() . При вызове он запускает выполнение кода до ближайшей
инструкции yield <значение> (значение может отсутствовать, в этом случае оно предполагается равным
undefined ). По достижении yield выполнение функции приостанавливается, а соответствующее значение –
возвращается во внешний код:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
На данный момент мы получили только первое значение, выполнение функции остановлено на второй строке:
433/597
{value: 1, done: false}
Повторный вызов [Link]() возобновит выполнение кода и вернёт результат следующего yield :
Сейчас генератор полностью выполнен. Мы можем увидеть это по свойству done:true и обработать value:3 как
окончательный результат.
Новые вызовы [Link]() больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но
будут возвращать один и тот же объект: {done: true} .
Перебор генераторов
Как вы, наверное, уже догадались по наличию метода next() , генераторы являются перебираемыми объектами.
Возвращаемые ими значения можно перебирать через for..of :
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
434/597
Выглядит гораздо красивее, чем использование .next().value , верно?
…Но обратите внимание: пример выше выводит значение 1 , затем 2 . Значение 3 выведено не будет!
Это из-за того, что перебор через for..of игнорирует последнее значение, при котором done: true . Поэтому,
если мы хотим, чтобы были все значения при переборе через for..of , то надо возвращать их через yield :
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
Так как генераторы являются перебираемыми объектами, мы можем использовать всю связанную с ними
функциональность, например оператор расширения ... :
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
alert(sequence); // 0, 1, 2, 3
Некоторое время назад, в главе Перебираемые объекты, мы создали перебираемый объект range , который
возвращает значения from..to .
Давайте вспомним код:
let range = {
from: 1,
to: 5,
435/597
Мы можем использовать функцию-генератор для итерации, указав её в [Link] .
Вот тот же range , но с гораздо более компактным итератором:
let range = {
from: 1,
to: 5,
Это работает, потому что range[[Link]]() теперь возвращает генератор, и его методы – в точности то,
что ожидает for..of :
● у него есть метод .next()
●
который возвращает значения в виде {value: ..., done: true/false}
Это не совпадение, конечно. Генераторы были добавлены в язык JavaScript, в частности, с целью упростить создание
перебираемых объектов.
Вариант с генератором намного короче, чем исходный вариант перебираемого range , и сохраняет те же
функциональные возможности.
Конечно, нам потребуется break (или return ) в цикле for..of по такому генератору, иначе цикл будет
продолжаться бесконечно, и скрипт «зависнет».
Композиция генераторов
Композиция генераторов – это особенная возможность генераторов, которая позволяет прозрачно «встраивать»
генераторы друг в друга.
Например, у нас есть функция для генерации последовательности чисел:
Мы можем использовать такую последовательность для генерации паролей, выбирать символы из неё (может быть,
ещё добавить символы пунктуации), но сначала её нужно сгенерировать.
В обычной функции, чтобы объединить результаты из нескольких других функций, мы вызываем их, сохраняем
промежуточные результаты, а затем в конце их объединяем.
Для генераторов есть особый синтаксис yield* , который позволяет «вкладывать» генераторы один в другой
(осуществлять их композицию).
Вот генератор с композицией:
436/597
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
alert(str); // 0..9A..Za..z
Директива yield* делегирует выполнение другому генератору. Этот термин означает, что yield* gen
перебирает генератор gen и прозрачно направляет его вывод наружу. Как если бы значения были сгенерированы
внешним генератором.
Результат – такой же, как если бы мы встроили код из вложенных генераторов:
function* generateAlphaNum() {
alert(str); // 0..9a..zA..Z
Композиция генераторов – естественный способ вставлять вывод одного генератора в поток другого. Она не
использует дополнительную память для хранения промежуточных результатов.
До этого момента генераторы сильно напоминали перебираемые объекты, со специальным синтаксисом для
генерации значений. Но на самом деле они намного мощнее и гибче.
Всё дело в том, что yield – дорога в обе стороны: он не только возвращает результат наружу, но и может
передавать значение извне в генератор.
Чтобы это сделать, нам нужно вызвать [Link](arg) с аргументом. Этот аргумент становится результатом
yield .
Продемонстрируем это на примере:
437/597
function* gen() {
// Передаём вопрос во внешний код и ожидаем ответа
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
.next(4)
1. Первый вызов [Link]() – всегда без аргумента, он начинает выполнение и возвращает результат
первого yield "2+2=?" . На этой точке генератор приостанавливает выполнение.
2. Затем, как показано на картинке выше, результат yield переходит во внешний код в переменную question .
3. При [Link](4) выполнение генератора возобновляется, а 4 выходит из присваивания как результат:
let result = 4 .
Обратите внимание, что внешний код не обязан немедленно вызывать next(4) . Ему может потребоваться время.
Это не проблема, генератор подождёт.
Например:
Как видно, в отличие от обычных функций, генератор может обмениваться результатами с вызывающим кодом,
передавая значения в next/yield .
Чтобы сделать происходящее более очевидным, вот ещё один пример с большим количеством вызовов:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
alert(ask2); // 9
}
Картинка выполнения:
438/597
Генератор Вызывающий код
"2 + 2 = ?"
. next ( 4 )
"3 * 3 = ?"
. next ( 9 )
Получается такой «пинг-понг»: каждый next(value) передаёт в генератор значение, которое становится
результатом текущего yield , возобновляет выполнение и получает выражение из следующего yield .
[Link]
Как мы видели в примерах выше, внешний код может передавать значение в генератор как результат yield .
…Но можно передать не только результат, но и инициировать ошибку. Это естественно, так как ошибка является
своего рода результатом.
Для того, чтобы передать ошибку в yield , нам нужно вызвать [Link](err) . В таком случае
исключение err возникнет на строке с yield .
Например, здесь yield "2 + 2 = ?" приведёт к ошибке:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("Выполнение программы не дойдёт до этой строки, потому что выше возникнет исключение");
} catch(e) {
alert(e); // покажет ошибку
}
}
Ошибка, которая проброшена в генератор на строке (2) , приводит к исключению на строке (1) с yield . В
примере выше try..catch перехватывает её и отображает.
Если мы не хотим перехватывать её, то она, как и любое обычное исключение, «вывалится» из генератора во
внешний код.
Текущая строка вызывающего кода – это строка с [Link] , отмечена (2) . Таким образом, мы можем
отловить её во внешнем коде, как здесь:
function* generate() {
let result = yield "2 + 2 = ?"; // Ошибка в этой строке
}
439/597
try {
[Link](new Error("Ответ не найден в моей базе данных"));
} catch(e) {
alert(e); // покажет ошибку
}
Если же ошибка и там не перехвачена, то дальше – как обычно, она выпадает наружу и, если не перехвачена,
«повалит» скрипт.
Итого
В современном JavaScript генераторы используются редко. Но иногда они оказываются полезными, потому что
способность функции обмениваться данными с вызывающим кодом во время выполнения совершенно уникальна. И,
конечно, для создания перебираемых объектов.
Также, в следующей главе мы будем изучать асинхронные генераторы, которые используются, чтобы читать потоки
асинхронно сгенерированных данных (например, постранично загружаемые из сети) в цикле for await ... of .
В веб-программировании мы часто работаем с потоками данных, так что это ещё один важный случай использования.
Задачи
Псевдослучайный генератор
Одной из них является тестирование. Нам могут понадобиться случайные данные: текст, числа и т.д., чтобы хорошо
всё проверить.
В JavaScript мы можем использовать [Link]() . Но если что-то пойдёт не так, то нам нужно будет
перезапустить тест, используя те же самые данные.
Для этого используются так называемые «сеяные псевдослучайные генераторы». Они получают «зерно», как первое
значение, и затем генерируют следующее, используя формулу. Так что одно и то же зерно даёт одинаковую
последовательность, и, следовательно, весь поток легко воспроизводим. Нам нужно только запомнить зерно, чтобы
воспроизвести последовательность.
1. 16807
2. 282475249
3. 1622650073
4. …и так далее…
Задачей является создать функцию-генератор pseudoRandom(seed) , которая получает seed и создаёт генератор с
указанной формулой.
Пример использования:
alert([Link]().value); // 16807
alert([Link]().value); // 282475249
alert([Link]().value); // 1622650073
440/597
Открыть песочницу с тестами для задачи.
К решению
Асинхронные итераторы позволяют перебирать данные, поступающие асинхронно. Например, когда мы загружаем
что-то по частям по сети. Асинхронные генераторы делают такой перебор ещё удобнее.
Давайте сначала рассмотрим простой пример, чтобы понять синтаксис, а затем – реальный практический.
Асинхронные итераторы
let range = {
from: 1,
to: 5,
Если нужно, пожалуйста, ознакомьтесь с главой про итераторы, где обычные итераторы разбираются подробно.
Чтобы сделать объект итерируемым асинхронно:
1. Используется [Link] вместо [Link] .
2. next() должен возвращать промис.
3. Чтобы перебрать такой объект, используется цикл for await (let item of iterable) .
Давайте создадим итерируемый объект range , как и в предыдущем примере, но теперь он будет возвращать
значения асинхронно, по одному в секунду:
let range = {
from: 1,
to: 5,
441/597
last: [Link],
(async () => {
})()
Это естественно, так как он ожидает [Link] , как и for..of без await . Ему не подходит
[Link] .
Асинхронные генераторы
442/597
}
В обычных генераторах мы не можем использовать await . Все значения должны поступать синхронно: в for..of
нет места для задержки, это синхронная конструкция.
Но что если нам нужно использовать await в теле генератора? Для выполнения сетевых запросов, например.
Нет проблем, просто добавьте в начале async , например, вот так:
yield i;
}
(async () => {
})();
Теперь у нас есть асинхронный генератор, который можно перебирать с помощью for await ... of .
Это действительно очень просто. Мы добавляем ключевое слово async , и внутри генератора теперь можно
использовать await , а также промисы и другие асинхронные функции.
С технической точки зрения, ещё одно отличие асинхронного генератора заключается в том, что его метод
[Link]() теперь тоже асинхронный и возвращает промисы.
Из обычного генератора мы можем получить значения при помощи result = [Link]() . Для
асинхронного нужно добавить await , вот так:
Как мы уже знаем, чтобы сделать объект перебираемым, нужно добавить к нему [Link] .
let range = {
from: 1,
to: 5,
[[Link]]() {
return <объект с next, чтобы сделать range перебираемым>
}
}
Обычная практика для [Link] – возвращать генератор, а не простой объект с next , как в предыдущем
примере.
Давайте вспомним пример из главы Генераторы:
let range = {
from: 1,
to: 5,
443/597
*[[Link]]() { // сокращение для [[Link]]: function*()
for(let value = [Link]; value <= [Link]; value++) {
yield value;
}
}
};
Здесь созданный объект range является перебираемым, а генератор *[[Link]] реализует логику для
перечисления значений.
Если хотим добавить асинхронные действия в генератор, нужно заменить [Link] на асинхронный
[Link] :
let range = {
from: 1,
to: 5,
yield value;
}
}
};
(async () => {
})();
До сих пор мы видели простые примеры, чтобы просто получить базовое представление. Теперь давайте рассмотрим
реальную ситуацию.
Есть много онлайн-сервисов, которые предоставляют данные постранично. Например, когда нам нужен список
пользователей, запрос возвращает предопределённое количество (например, 100) пользователей – «одну страницу»,
и URL следующей страницы.
Этот подход очень распространён, и речь не только о пользователях, а о чём угодно. Например, GitHub позволяет
получать коммиты таким образом, с разбивкой по страницам:
● Нужно сделать запрос на URL в виде [Link] .
●
В ответ придёт JSON с 30 коммитами, а также со ссылкой на следующую страницу в заголовке Link .
● Затем можно использовать эту ссылку для следующего запроса, чтобы получить дополнительную порцию
коммитов, и так далее.
Но нам бы, конечно же, хотелось вместо этого сложного взаимодействия иметь просто объект с коммитами, которые
можно перебирать, вот так:
444/597
Мы бы хотели сделать функцию fetchCommits(repo) , которая будет получать коммиты, делая запросы всякий раз,
когда это необходимо. И пусть она сама разбирается со всем, что касается нумерации страниц, для нас это будет
просто for await..of .
С асинхронными генераторами это довольно легко реализовать:
while (url) {
const response = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // GitHub требует заголовок user-agent
});
const body = await [Link](); // (2) ответ в формате JSON (массив коммитов)
url = nextPage;
for(let commit of body) { // (4) вернуть коммиты один за другим, до окончания страницы
yield commit;
}
}
}
1. Мы используем метод fetch браузера для загрузки с удалённого URL. Он позволяет при необходимости добавлять
авторизацию и другие заголовки, здесь GitHub требует User-Agent .
2. Результат fetch обрабатывается как JSON, это опять-таки метод, присущий fetch .
3. Нужно получить URL следующей страницы из заголовка ответа Link . Он имеет специальный формат, поэтому мы
используем регулярное выражение. URL следующей страницы может выглядеть как
[Link] , он генерируется самим GitHub.
4. Затем мы выдаём все полученные коммиты, а когда они закончатся – сработает следующая итерация
while(url) , которая сделает ещё один запрос.
(async () => {
let count = 0;
[Link]([Link]);
})();
Это именно то, что мы хотели. Внутренняя механика постраничных запросов снаружи не видна. Для нас это просто
асинхронный генератор, который возвращает коммиты.
Итого
Обычные итераторы и генераторы прекрасно работают с данными, которые не требуют времени для их создания или
получения.
Когда мы ожидаем, что данные будут поступать асинхронно, с задержками, можно использовать их асинхронные
аналоги и for await..of вместо for..of .
Синтаксические различия между асинхронными и обычными итераторами:
445/597
Перебираемый объект Асинхронно перебираемый
next() возвращает {value:…, done: true/false} промис, который завершается с {value:…, done: true/false}
[Link]() возвращает {value:…, done: true/false} промис, который завершается с {value:…, done: true/false}
В веб-разработке мы часто встречаемся с потоками данных, когда они поступают по частям. Например, загрузка или
выгрузка большого файла.
Мы можем использовать асинхронные генераторы для обработки таких данных. Также заметим, что в некоторых
окружениях, например, браузерах, есть и другое API, называемое Streams (потоки), который предоставляет
специальные интерфейсы для работы с такими потоками данных, их преобразования и передачи из одного потока в
другой (например, загрузка из одного источника и сразу отправка в другое место).
Модули
Модули, введение
По мере роста нашего приложения, мы обычно хотим разделить его на много файлов, так называемых «модулей».
Модуль обычно содержит класс или библиотеку с функциями.
Долгое время в JavaScript отсутствовал синтаксис модулей на уровне языка. Это не было проблемой, потому что
первые скрипты были маленькими и простыми. В модулях не было необходимости.
Но со временем скрипты становились всё более и более сложными, поэтому сообщество придумало несколько
вариантов организации кода в модули. Появились библиотеки для динамической подгрузки модулей.
Например:
● AMD – одна из самых старых модульных систем, изначально реализована библиотекой [Link] .
● CommonJS – модульная система, созданная для сервера [Link].
● UMD – ещё одна модульная система, предлагается как универсальная, совместима с AMD и CommonJS.
Теперь все они постепенно становятся частью истории, хотя их и можно найти в старых скриптах.
Система модулей на уровне языка появилась в стандарте JavaScript в 2015 году и постепенно эволюционировала. На
данный момент она поддерживается большинством браузеров и [Link]. Далее мы будем изучать именно её.
// 📁 [Link]
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 📁 [Link]
import {sayHi} from './[Link]';
446/597
alert(sayHi); // function...
sayHi('John'); // Hello, John!
Директива import загружает модуль по пути ./[Link] относительно текущего файла и записывает
экспортированную функцию sayHi в соответствующую переменную.
Давайте запустим пример в браузере.
Так как модули поддерживают ряд специальных ключевых слов, и у них есть ряд особенностей, то необходимо явно
сказать браузеру, что скрипт является модулем, при помощи атрибута <script type="module"> .
Вот так:
[Link]
Браузер автоматически загрузит и запустит импортированный модуль (и те, которые он импортирует, если надо), а
затем запустит скрипт.
<script type="module">
a = 5; // ошибка
</script>
Модули должны экспортировать функциональность, предназначенную для использования извне. А другие модули
могут её импортировать.
Так что нам надо импортировать [Link] в [Link] и взять из него нужную функциональность, вместо того
чтобы полагаться на глобальные переменные.
Правильный вариант:
[Link]
В браузере также существует независимая область видимости для каждого скрипта <script type="module"> :
<script type="module">
// Переменная доступна только в этом модуле
let user = "John";
</script>
<script type="module">
alert(user); // Error: user is not defined
</script>
447/597
Если нам нужно сделать глобальную переменную уровня всей страницы, можно явно присвоить её объекту window ,
тогда получить значение переменной можно обратившись к [Link] . Но это должно быть исключением,
требующим веской причины.
// 📁 [Link]
alert("Модуль выполнен!");
// 📁 [Link]
import `./[Link]`; // Модуль выполнен!
// 📁 [Link]
import `./[Link]`; // (ничего не покажет)
На практике, задача кода модуля – это обычно инициализация, создание внутренних структур данных, а если мы
хотим, чтобы что-то можно было использовать много раз, то экспортируем это.
Теперь более продвинутый пример.
Давайте представим, что модуль экспортирует объект:
// 📁 [Link]
export let admin = {
name: "John"
};
Если модуль импортируется в нескольких файлах, то код модуля будет выполнен только один раз, объект admin
будет создан и в дальнейшем будет передан всем импортёрам.
Все импортёры получат один-единственный объект admin :
// 📁 [Link]
import {admin} from './[Link]';
[Link] = "Pete";
// 📁 [Link]
import {admin} from './[Link]';
alert([Link]); // Pete
Ещё раз заметим – модуль выполняется только один раз. Генерируется экспорт и после передаётся всем
импортёрам, поэтому, если что-то изменится в объекте admin , то другие модули тоже увидят эти изменения.
Такое поведение позволяет конфигурировать модули при первом импорте. Мы можем установить его свойства один
раз, и в дальнейших импортах он будет уже настроенным.
Например, модуль [Link] предоставляет определённую функциональность, но ожидает передачи учётных
данных в объект admin извне:
// 📁 [Link]
export let admin = { };
448/597
alert(`Ready to serve, ${[Link]}!`);
}
В [Link] , первом скрипте нашего приложения, мы установим [Link] . Тогда все это увидят, включая вызовы,
сделанные из самого [Link] :
// 📁 [Link]
import {admin} from './[Link]';
[Link] = "Pete";
// 📁 [Link]
import {admin, sayHi} from './[Link]';
alert([Link]); // Pete
[Link]
Объект [Link] содержит информацию о текущем модуле.
Содержимое зависит от окружения. В браузере он содержит ссылку на скрипт или ссылку на текущую веб-страницу,
если модуль встроен в HTML:
<script type="module">
alert([Link]); // ссылка на html страницу для встроенного скрипта
</script>
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
Особенности в браузерах
Есть и несколько других, именно браузерных особенностей скриптов с type="module" по сравнению с обычными
скриптами.
Если вы читаете материал в первый раз или, если не собираетесь использовать модули в браузерах, то сейчас
можете пропустить эту секцию.
Как побочный эффект, модули всегда видят полностью загруженную HTML-страницу, включая элементы под ними.
Например:
449/597
<script type="module">
alert(typeof button); // object: скрипт может 'видеть' кнопку под ним
// так как модули являются отложенными, то скрипт начнёт выполнятся только после полной загрузки страницы
</script>
<script>
alert(typeof button); // Ошибка: кнопка не определена, скрипт не видит элементы под ним
// обычные скрипты запускаются сразу, не дожидаясь полной загрузки страницы
</script>
<button id="button">Кнопка</button>
Пожалуйста, обратите внимание: второй скрипт выполнится раньше, чем первый! Поэтому мы увидим сначала
undefined , а потом object .
Это потому, что модули начинают выполняться после полной загрузки страницы. Обычные скрипты запускаются сразу
же, поэтому сообщение из обычного скрипта мы видим первым.
При использовании модулей нам стоит иметь в виду, что HTML-страница будет показана браузером до того, как
выполнятся модули и JavaScript-приложение будет готово к работе. Некоторые функции могут ещё не работать. Нам
следует разместить «индикатор загрузки» или что-то ещё, чтобы не смутить этим посетителя.
[Link]();
</script>
Внешние скрипты
Внешние скрипты с атрибутом type="module" имеют два отличия:
1. Внешние скрипты с одинаковым атрибутом src запускаются только один раз:
<!-- скрипт [Link] загрузится и будет выполнен только один раз -->
<script type="module" src="[Link]"></script>
<script type="module" src="[Link]"></script>
2. Внешний скрипт, который загружается с другого домена, требует указания заголовков CORS . Другими словами,
если модульный скрипт загружается с другого домена, то удалённый сервер должен установить заголовок
Access-Control-Allow-Origin означающий, что загрузка скрипта разрешена.
450/597
Например, этот import неправильный:
Другие окружения, например [Link], допускают использование «голых» модулей, без путей, так как в них есть свои
правила, как работать с такими модулями и где их искать. Но браузеры пока не поддерживают «голые» модули.
Совместимость, «nomodule»
Старые браузеры не понимают атрибут type="module" . Скрипты с неизвестным атрибутом type просто
игнорируются. Мы можем сделать для них «резервный» скрипт при помощи атрибута nomodule :
<script type="module">
alert("Работает в современных браузерах");
</script>
<script nomodule>
alert("Современные браузеры понимают оба атрибута - и type=module, и nomodule, поэтому пропускают этот тег script")
alert("Старые браузеры игнорируют скрипты с неизвестным атрибутом type=module, но выполняют этот.");
</script>
Инструменты сборки
В реальной жизни модули в браузерах редко используются в «сыром» виде. Обычно, мы объединяем модули вместе,
используя специальный инструмент, например Webpack и после выкладываем код на рабочий сервер.
Одно из преимуществ использования сборщика – он предоставляет больший контроль над тем, как модули ищутся,
позволяет использовать «голые» модули и многое другое «своё», например CSS/HTML-модули.
Сборщик делает следующее:
1. Берёт «основной» модуль, который мы собираемся поместить в <script type="module"> в HTML.
2. Анализирует зависимости (импорты, импорты импортов и так далее)
3. Собирает один файл со всеми модулями (или несколько файлов, это можно настроить), перезаписывает
встроенный import функцией импорта от сборщика, чтобы всё работало. «Специальные» типы модулей, такие
как HTML/CSS тоже поддерживаются.
4. В процессе могут происходить и другие трансформации и оптимизации кода:
● Недостижимый код удаляется.
● Неиспользуемые экспорты удаляются («tree-shaking»).
●
Специфические операторы для разработки, такие как console и debugger , удаляются.
● Современный синтаксис JavaScript также может быть трансформирован в предыдущий стандарт, с похожей
функциональностью, например, с помощью Babel .
● Полученный файл можно минимизировать (удалить пробелы, заменить названия переменных на более короткие
и т.д.).
Если мы используем инструменты сборки, то они объединяют модули вместе в один или несколько файлов, и
заменяют import/export на свои вызовы. Поэтому итоговую сборку можно подключать и без атрибута
type="module" , как обычный скрипт:
<!-- Предположим, что мы собрали [Link], используя например утилиту Webpack -->
<script src="[Link]"></script>
Хотя и «как есть» модули тоже можно использовать, а сборщик настроить позже при необходимости.
Итого
451/597
● Для загрузки внешних модулей с другого источника, он должен ставить заголовки CORS.
● Дублирующиеся внешние скрипты игнорируются.
2. У модулей есть своя область видимости, обмениваться функциональностью можно через import/export .
3. В модулях всегда включена директива use strict .
4. Код в модулях выполняется только один раз. Экспортируемая функциональность создаётся один раз и передаётся
всем импортёрам.
Когда мы используем модули, каждый модуль реализует свою функциональность и экспортирует её. Затем мы
используем import , чтобы напрямую импортировать её туда, куда необходимо. Браузер загружает и анализирует
скрипты автоматически.
В реальной жизни часто используется сборщик Webpack , чтобы объединить модули: для производительности и
других «плюшек».
В следующей главе мы увидим больше примеров и вариантов импорта/экспорта.
Экспорт и импорт
Директивы экспорт и импорт имеют несколько вариантов вызова.
В предыдущей главе мы видели простое использование, давайте теперь посмотрим больше примеров.
Экспорт до объявления
Мы можем пометить любое объявление как экспортируемое, разместив export перед ним, будь то переменная,
функция или класс.
Например, все следующие экспорты допустимы:
// экспорт массива
export let months = ['Jan', 'Feb', 'Mar', 'Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// экспорт константы
export const MODULES_BECAME_STANDARD_YEAR = 2015;
// экспорт класса
export class User {
constructor(name) {
[Link] = name;
}
}
// 📁 [Link]
function sayHi(user) {
452/597
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
Импорт *
Обычно мы располагаем список того, что хотим импортировать, в фигурных скобках import {...} , например вот
так:
// 📁 [Link]
import {sayHi, sayBye} from './[Link]';
Но если импортировать нужно много чего, мы можем импортировать всё сразу в виде объекта, используя import *
as <obj> . Например:
// 📁 [Link]
import * as say from './[Link]';
[Link]('John');
[Link]('John');
На первый взгляд «импортировать всё» выглядит очень удобно, не надо писать лишнего, зачем нам вообще может
понадобиться явно перечислять список того, что нужно импортировать?
Для этого есть несколько причин.
1. Современные инструменты сборки (webpack и другие) собирают модули вместе и оптимизируют их, ускоряя
загрузку и удаляя неиспользуемый код.
Предположим, мы добавили в наш проект стороннюю библиотеку [Link] с множеством функций:
// 📁 [Link]
export function sayHi() { ... }
export function sayBye() { ... }
export function becomeSilent() { ... }
// 📁 [Link]
import {sayHi} from './[Link]';
…Тогда оптимизатор увидит, что другие функции не используются, и удалит остальные из собранного кода, тем
самым делая код меньше. Это называется «tree-shaking».
2. Явно перечисляя то, что хотим импортировать, мы получаем более короткие имена функций: sayHi() вместо
[Link]() .
3. Явное перечисление импортов делает код более понятным, позволяет увидеть, что именно и где используется. Это
упрощает поддержку и рефакторинг кода.
Импорт «как»
453/597
// 📁 [Link]
import {sayHi as hi, sayBye as bye} from './[Link]';
Экспортировать «как»
// 📁 [Link]
...
export {sayHi as hi, sayBye as bye};
Теперь hi и bye – официальные имена для внешнего кода, их нужно использовать при импорте:
// 📁 [Link]
import * as say from './[Link]';
Экспорт по умолчанию
По большей части, удобнее второй подход, когда каждая «вещь» находится в своём собственном модуле.
Естественно, требуется много файлов, если для всего делать отдельный модуль, но это не проблема. Так даже
удобнее: навигация по проекту становится проще, особенно, если у файлов хорошие имена, и они структурированы
по папкам.
Модули предоставляют специальный синтаксис export default («экспорт по умолчанию») для второго подхода.
Ставим export default перед тем, что нужно экспортировать:
// 📁 [Link]
export default class User { // просто добавьте "default"
constructor(name) {
[Link] = name;
}
}
// 📁 [Link]
import User from './[Link]'; // не {User}, просто User
new User('John');
Импорты без фигурных скобок выглядят красивее. Обычная ошибка начинающих: забывать про фигурные скобки.
Запомним: фигурные скобки необходимы в случае именованных экспортов, для export default они не нужны.
454/597
Технически в одном модуле может быть как экспорт по умолчанию, так и именованные экспорты, но на практике
обычно их не смешивают. То есть, в модуле находятся либо именованные экспорты, либо один экспорт по умолчанию.
Так как в файле может быть максимум один export default , то экспортируемая сущность не обязана иметь имя.
Например, всё это – полностью корректные экспорты по умолчанию:
Это нормально, потому что может быть только один export default на файл, так что import без фигурных
скобок всегда знает, что импортировать.
Без default такой экспорт выдал бы ошибку:
Имя «default»
В некоторых ситуациях для обозначения экспорта по умолчанию в качестве имени используется default .
Например, чтобы экспортировать функцию отдельно от её объявления:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
Или, ещё ситуация, давайте представим следующее: модуль [Link] экспортирует одну сущность «по умолчанию»
и несколько именованных (редкий, но возможный случай):
// 📁 [Link]
export default class User {
constructor(name) {
[Link] = name;
}
}
// 📁 [Link]
import {default as User, sayHi} from './[Link]';
new User('John');
И, наконец, если мы импортируем всё как объект import * , тогда его свойство default – как раз и будет
экспортом по умолчанию:
455/597
// 📁 [Link]
import * as user from './[Link]';
…В то время как для экспорта по умолчанию мы выбираем любое имя при импорте:
Так что члены команды могут использовать разные имена для импорта одной и той же вещи, и это не очень хорошо.
Обычно, чтобы избежать этого и соблюсти единообразие кода, есть правило: имена импортируемых переменных
должны соответствовать именам файлов. Вот так:
Тем не менее, в некоторых командах это считают серьёзным доводом против экспортов по умолчанию и
предпочитают использовать именованные экспорты везде. Даже если экспортируется только одна вещь, она всё
равно экспортируется с именем, без использования default .
Это также немного упрощает реэкспорт (смотрите ниже).
Реэкспорт
Синтаксис «реэкспорта» export ... from ... позволяет импортировать что-то и тут же экспортировать,
возможно под другим именем, вот так:
auth/
[Link]
[Link]
[Link]
tests/
[Link]
providers/
[Link]
456/597
[Link]
...
Мы бы хотели сделать функциональность нашего пакета доступной через единую точку входа: «главный файл»
auth/[Link] . Чтобы можно было использовать её следующим образом:
Идея в том, что внешние разработчики, которые будут использовать наш пакет, не должны разбираться с его
внутренней структурой, рыться в файлах внутри нашего пакета. Всё, что нужно, мы экспортируем в auth/[Link] ,
а остальное скрываем от любопытных взглядов.
Так как нужная функциональность может быть разбросана по модулям нашего пакета, мы можем импортировать их в
auth/[Link] и тут же экспортировать наружу.
// 📁 auth/[Link]
Теперь пользователи нашего пакета могут писать import {login} from "auth/[Link]" .
Запись export ... from ... – это просто более короткий вариант такого импорта-экспорта:
// 📁 auth/[Link]
// 📁 [Link]
export default class User {
// ...
}
1. export User from './[Link]' не будет работать. Казалось бы, что такого? Но возникнет синтаксическая
ошибка!
Чтобы реэкспортировать экспорт по умолчанию, мы должны написать export {default as User} , как в
примере выше. Такая вот особенность синтаксиса.
2. export * from './[Link]' реэкспортирует только именованные экспорты, исключая экспорт по умолчанию.
Такое особое поведение реэкспорта с экспортом по умолчанию – одна из причин того, почему некоторые
разработчики их не любят.
457/597
Итого
Импорт:
● Именованные экспорты из модуля:
● import {x [as y], ...} from "module"
● Импорт по умолчанию:
● import x from "module"
● import {default as x} from "module"
●
Всё сразу:
● import * as obj from "module"
● Только подключить модуль (его код запустится), но не присваивать его переменной:
●
import "module"
Мы можем поставить import/export в начало или в конец скрипта, это не имеет значения.
То есть, технически, такая запись вполне корректна:
sayHi();
// ...
На практике импорты, чаще всего, располагаются в начале файла. Но это только для большего удобства.
Обратите внимание, что инструкции import/export не работают внутри {...} .
Условный импорт, такой как ниже, работать не будет:
if (something) {
import {sayHi} from "./[Link]"; // Ошибка: импорт должен быть на верхнем уровне
}
…Но что, если нам в самом деле нужно импортировать что-либо в зависимости от условий? Или в определённое
время? Например, загрузить модуль, только когда он станет нужен?
Мы рассмотрим динамические импорты в следующей главе.
Динамические импорты
Инструкции экспорта и импорта, которые мы рассматривали в предыдущей главе, называются «статическими».
Синтаксис у них весьма простой и строгий.
Во-первых, мы не можем динамически задавать никакие из параметров import .
Путь к модулю должен быть строковым примитивом и не может быть вызовом функции. Вот так работать не будет:
458/597
Во-вторых, мы не можем делать импорт в зависимости от условий или в процессе выполнения.
if(...) {
import ...; // Ошибка, запрещено
}
{
import ...; // Ошибка, мы не можем ставить импорт в блок
}
Всё это следствие того, что цель директив import/export – задать костяк структуры кода. Благодаря им она может
быть проанализирована, модули могут быть собраны в один файл специальными инструментами, а неиспользуемые
экспорты удалены. Это возможно только благодаря тому, что всё статично.
Но как мы можем импортировать модуль динамически, по запросу?
Выражение import()
Выражение import(module) загружает модуль и возвращает промис, результатом которого становится объект
модуля, содержащий все его экспорты.
Использовать его мы можем динамически в любом месте кода, например, так:
import(modulePath)
.then(obj => <объект модуля>)
.catch(err => <ошибка загрузки, например если нет такого модуля>)
Или если внутри асинхронной функции, то можно let module = await import(modulePath) .
Например, если у нас есть такой модуль [Link] :
// 📁 [Link]
export function hi() {
alert(`Привет`);
}
hi();
bye();
// 📁 [Link]
export default function() {
alert("Module loaded (export default)!");
}
…То для доступа к нему нам следует взять свойство default объекта модуля:
say();
459/597
Вот полный пример:
[Link]
На заметку:
Динамический импорт работает в обычных скриптах, он не требует указания script type="module" .
На заметку:
Хотя import() и выглядит похоже на вызов функции, на самом деле это специальный синтаксис, так же, как,
например, super() .
Так что мы не можем скопировать import в другую переменную или вызвать при помощи .call/apply . Это не
функция.
Разное
Proxy и Reflect
Объект Proxy «оборачивается» вокруг другого объекта и может перехватывать (и, при желании, самостоятельно
обрабатывать) разные действия с ним, например чтение/запись свойств и другие. Далее мы будем называть такие
объекты «прокси».
Прокси используются во многих библиотеках и некоторых браузерных фреймворках. В этой главе мы увидим много
случаев применения прокси в решении реальных задач.
Синтаксис:
●
target – это объект, для которого нужно сделать прокси, может быть чем угодно, включая функции.
● handler – конфигурация прокси: объект с «ловушками» («traps»): методами, которые перехватывают разные
операции, например, ловушка get – для чтения свойства из target , ловушка set – для записи свойства в
target и так далее.
При операциях над proxy , если в handler имеется соответствующая «ловушка», то она срабатывает, и прокси
имеет возможность по-своему обработать её, иначе операция будет совершена над оригинальным объектом
target .
В качестве начального примера создадим прокси без всяких ловушек:
Так как нет ловушек, то все операции на proxy применяются к оригинальному объекту target .
1. Запись свойства [Link]= устанавливает значение на target .
2. Чтение свойства [Link] возвращает значение из target .
3. Итерация по proxy возвращает значения из target .
Как мы видим, без ловушек proxy является прозрачной обёрткой над target .
460/597
proxy
Proxy – это особый, «экзотический», объект, у него нет собственных свойств. С пустым handler он просто
перенаправляет все операции на target .
Чтобы активировать другие его возможности, добавим ловушки.
Что именно мы можем ими перехватить?
Для большинства действий с объектами в спецификации JavaScript есть так называемый «внутренний метод»,
который на самом низком уровне описывает, как его выполнять. Например, [[Get]] – внутренний метод для чтения
свойства, [[Set]] – для записи свойства, и так далее. Эти методы используются только в спецификации, мы не
можем обратиться напрямую к ним по имени.
Ловушки как раз перехватывают вызовы этих внутренних методов. Полный список методов, которые можно
перехватывать, перечислен в спецификации Proxy , а также в таблице ниже.
Для каждого внутреннего метода в этой таблице указана ловушка, то есть имя метода, который мы можем добавить в
параметр handler при создании new Proxy , чтобы перехватывать данную операцию:
Инварианты
JavaScript налагает некоторые условия – инварианты на реализацию внутренних методов и ловушек.
Большинство из них касаются возвращаемых значений:
● Метод [[Set]] должен возвращать true , если значение было успешно записано, иначе false .
● Метод [[Delete]] должен возвращать true , если значение было успешно удалено, иначе false .
● …и так далее, мы увидим больше в примерах ниже.
Ловушки могут перехватывать вызовы этих методов, но должны выполнять указанные условия.
Инварианты гарантируют корректное и последовательное поведение конструкций и методов языка. Полный
список инвариантов можно найти в спецификации , хотя скорее всего вы не нарушите эти условия, если только
не соберётесь делать что-то совсем уж странное.
461/597
Теперь давайте посмотрим, как это всё работает, на реальных примерах.
Давайте применим ловушку get , чтобы реализовать «значения по умолчанию» для свойств объекта.
Например, сделаем числовой массив, так чтобы при чтении из него несуществующего элемента возвращался 0 .
Обычно при чтении из массива несуществующего свойства возвращается undefined , но мы обернём обычный
массив в прокси, который перехватывает операцию чтения свойства из массива и возвращает 0 , если такого
элемента нет:
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (нет такого элемента)
Как видно, это очень легко сделать при помощи ловушки get .
Мы можем использовать Proxy для реализации любой логики возврата значений по умолчанию.
Представим, что у нас есть объект-словарь с фразами на английском и их переводом на испанский:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
Сейчас, если фразы в dictionary нет, при чтении возвращается undefined . Но на практике оставлять фразы
непереведёнными лучше, чем использовать undefined . Поэтому давайте сделаем так, чтобы при отсутствии
перевода возвращалась оригинальная фраза на английском вместо undefined .
Чтобы достичь этого, обернём dictionary в прокси, перехватывающий операцию чтения:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
462/597
// иначе возвращаем непереведённую фразу
return phrase;
}
}
});
Прокси должен заменить собой оригинальный объект повсюду. Никто не должен ссылаться на оригинальный
объект после того, как он был проксирован. Иначе очень легко запутаться.
Допустим, мы хотим сделать массив исключительно для чисел. Если в него добавляется значение иного типа, то это
должно приводить к ошибке.
Ловушка set срабатывает, когда происходит запись свойства.
set(target, property, value, receiver) :
● target – это оригинальный объект, который передавался первым аргументом в конструктор new Proxy ,
●
property – имя свойства,
● value – значение свойства,
● receiver – аналогично ловушке get , этот аргумент имеет значение, только если свойство – сеттер.
Ловушка set должна вернуть true , если запись прошла успешно, и false в противном случае (будет
сгенерирована ошибка TypeError ).
Давайте применим её для проверки новых значений:
Обратите внимание, что встроенная функциональность массива по-прежнему работает! Значения добавляются
методом push . Свойство length при этом увеличивается. Наш прокси ничего не ломает.
Нам не нужно переопределять методы массива push и unshift и другие, чтобы добавлять туда проверку на тип,
так как внутри себя они используют операцию [[Set]] , которая перехватывается прокси.
Таким образом, код остаётся чистым и прозрачным.
463/597
Не забывайте вернуть true
Как сказано ранее, нужно соблюдать инварианты.
Для set реализация ловушки должна возвращать true в случае успешной записи свойства.
Если забыть это сделать или возвратить любое ложное значение, это приведёт к ошибке TypeError .
[Link] , цикл for..in и большинство других методов, которые работают со списком свойств объекта,
используют внутренний метод [[OwnPropertyKeys]] (перехватываемый ловушкой ownKeys ) для их получения.
Такие методы различаются в деталях:
● [Link](obj) возвращает не-символьные ключи.
● [Link](obj) возвращает символьные ключи.
● [Link]/values() возвращает не-символьные ключи/значения с флагом enumerable (подробнее про
флаги свойств было в главе Флаги и дескрипторы свойств).
● for..in перебирает не-символьные ключи с флагом enumerable , а также ключи прототипов.
В примере ниже мы используем ловушку ownKeys , чтобы цикл for..in по объекту, равно как [Link] и
[Link] пропускали свойства, начинающиеся с подчёркивания _ :
let user = {
name: "Вася",
age: 30,
_password: "***"
};
let user = { };
Почему? Причина проста: [Link] возвращает только свойства с флагом enumerable . Для того, чтобы
определить, есть ли этот флаг, он для каждого свойства вызывает внутренний метод [[GetOwnProperty]] ,
который получает его дескриптор. А в данном случае свойство отсутствует, его дескриптор пуст, флага enumerable
нет, поэтому оно пропускается.
Чтобы [Link] возвращал свойство, нужно либо чтобы свойство в объекте физически было, причём с флагом
enumerable , либо перехватить вызовы [[GetOwnProperty]] (это делает ловушка
getOwnPropertyDescriptor ), и там вернуть дескриптор с enumerable: true .
464/597
Вот так будет работать:
let user = { };
});
alert( [Link](user) ); // a, b, c
Ещё раз заметим, что получение дескриптора нужно перехватывать только если свойство отсутствует в самом
объекте.
Существует широко распространённое соглашение о том, что свойства и методы, название которых начинается с
символа подчёркивания _ , следует считать внутренними. К ним не следует обращаться снаружи объекта.
Однако технически это всё равно возможно:
let user = {
name: "Вася",
_password: "secret"
};
alert(user._password); // secret
let user = {
name: "Вася",
_password: "***"
};
465/597
}
},
deleteProperty(target, prop) { // перехватываем удаление свойства
if ([Link]('_')) {
throw new Error("Отказано в доступе");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // перехватываем попытку итерации
return [Link](target).filter(key => );
}
});
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? [Link](target) : value; // (*)
}
user = {
// ...
checkPassword(value) {
// метод объекта должен иметь доступ на чтение _password
return value === this._password;
}
}
Вызов [Link]() получает проксированный объект user в качестве this (объект перед точкой
становится this ), так что когда такой вызов обращается к this._password , ловушка get вступает в действие
(она срабатывает при любом чтении свойства), и выбрасывается ошибка.
Поэтому мы привязываем контекст к методам объекта – оригинальный объект target в строке (*) . Тогда их
дальнейшие вызовы будут использовать target в качестве this , без всяких ловушек.
Такое решение обычно работает, но не является идеальным, поскольку метод может передать оригинальный объект
куда-то ещё, и возможна путаница: где изначальный объект, а где – проксированный.
К тому же, объект может проксироваться несколько раз (для добавления различных возможностей), и если
передавать методу исходный, то могут быть неожиданности.
Так что везде использовать такой прокси не стоит.
466/597
Приватные свойства в классах
Современные интерпретаторы JavaScript поддерживают приватные свойства в классах. Названия таких свойств
должны начинаться с символа # . Они подробно описаны в главе Приватные и защищённые методы и свойства.
Для них не нужны подобные прокси.
Впрочем, приватные свойства имеют свои недостатки. В частности, они не наследуются.
let range = {
start: 1,
end: 10
};
Мы бы хотели использовать оператор in , чтобы проверить, что некоторое число находится в указанном диапазоне.
Ловушка has перехватывает вызовы in .
has(target, property)
● target – это оригинальный объект, который передавался первым аргументом в конструктор new Proxy ,
●
property – имя свойства
Вот демо:
let range = {
start: 1,
end: 10
};
Ловушка apply(target, thisArg, args) активируется при вызове прокси как функции:
● target – это оригинальный объект (как мы помним, функция – это объект в языке JavaScript),
● thisArg – это контекст this .
● args – список аргументов.
Например, давайте вспомним декоратор delay(f, ms) , созданный нами в главе Декораторы и переадресация
вызова, call/apply.
Тогда мы обошлись без создания прокси. Вызов delay(f, ms) возвращал функцию, которая передавала вызовы f
после ms миллисекунд.
467/597
setTimeout(() => [Link](this, arguments), ms);
};
}
function sayHi(user) {
alert(`Привет, ${user}!`);
}
Как мы уже видели, это в целом работает. Функция-обёртка в строке (*) вызывает нужную функцию с указанной
задержкой.
Но наша функция-обёртка не перенаправляет операции чтения/записи свойства и другие. После обёртывания доступ
к свойствам оригинальной функции, таким как name , length , и другим, будет потерян.
function sayHi(user) {
alert(`Привет, ${user}!`);
}
Прокси куда более мощные в этом смысле, поскольку они перенаправляют всё к оригинальному объекту.
Давайте используем прокси вместо функции-обёртки:
function sayHi(user) {
alert(`Привет, ${user}!`);
}
Результат такой же, но сейчас не только вызовы, но и другие операции на прокси перенаправляются к оригинальной
функции. Таким образом, операция чтения свойства [Link] возвращает корректное значение в строке (*)
после проксирования.
Мы получили лучшую обёртку.
Существуют и другие ловушки: полный список есть в начале этой главы. Использовать их можно по аналогии с
вышеописанными.
Reflect
468/597
Ранее мы говорили о том, что внутренние методы, такие как [[Get]] , [[Set]] и другие, существуют только в
спецификации, что к ним нельзя обратиться напрямую.
Объект Reflect делает это возможным. Его методы – минимальные обёртки вокруг внутренних методов.
Вот примеры операций и вызовы Reflect , которые делают то же самое:
… … …
Например:
alert([Link]); // Вася
В частности, Reflect позволяет вызвать операторы ( new , delete …) как функции ( [Link] ,
[Link] , …). Это интересная возможность, но здесь нам важно другое.
Для каждого внутреннего метода, перехватываемого Proxy , есть соответствующий метод в Reflect ,
который имеет такое же имя и те же аргументы, что и у ловушки Proxy .
Поэтому мы можем использовать Reflect , чтобы перенаправить операцию на исходный объект.
В этом примере обе ловушки get и set прозрачно (как будто их нет) перенаправляют операции чтения и записи на
объект, при этом выводя сообщение:
let user = {
name: "Вася",
};
Здесь:
1. [Link] читает свойство объекта.
2. [Link] записывает свойство и возвращает true при успехе, иначе false .
То есть, всё очень просто – если ловушка хочет перенаправить вызов на объект, то достаточно вызвать Reflect.
<метод> с теми же аргументами.
В большинстве случаев мы можем сделать всё то же самое и без Reflect , например, чтение свойства
[Link](target, prop, receiver) можно заменить на target[prop] . Но некоторые нюансы легко
упустить.
469/597
Сделаем вокруг user прокси:
let user = {
_name: "Гость",
get name() {
return this._name;
}
};
alert([Link]); // Гость
Ловушка get здесь «прозрачная», она возвращает свойство исходного объекта и больше ничего не делает. Для
нашего примера этого вполне достаточно.
let user = {
_name: "Гость",
get name() {
return this._name;
}
};
let admin = {
__proto__: userProxy,
_name: "Админ"
};
// Ожидается: Админ
alert([Link]); // выводится Гость (?!?)
Именно для исправления таких ситуаций нужен receiver , третий аргумент ловушки get . В нём хранится ссылка
на правильный контекст this , который нужно передать геттеру. В данном случае это admin .
Как передать геттеру контекст? Для обычной функции мы могли бы использовать call/apply , но это же геттер, его
не вызывают, просто читают значение.
Это может сделать [Link] . Всё будет работать верно, если использовать его.
Вот исправленный вариант:
470/597
let user = {
_name: "Гость",
get name() {
return this._name;
}
};
let admin = {
__proto__: userProxy,
_name: "Админ"
};
alert([Link]); // Админ
Сейчас receiver , содержащий ссылку на корректный this (то есть на admin ), передаётся геттеру посредством
[Link] в строке (*) .
Можно переписать ловушку и короче:
Методы в Reflect имеют те же названия, что и соответствующие ловушки, и принимают такие же аргументы. Это
было специально задумано при разработке спецификации JavaScript.
Так что return Reflect... даёт простую и безопасную возможность перенаправить операцию на оригинальный
объект и при этом предохраняет нас от возможных ошибок, связанных с этим действием.
Ограничения прокси
Прокси – уникальное средство для настройки поведения объектов на самом низком уровне. Но они не идеальны, есть
некоторые ограничения.
Внутри себя объект типа Map хранит все данные во внутреннем слоте [[MapData]] . Прокси не имеет такого слота.
Встроенный метод [Link] пытается получить доступ к своему внутреннему свойству this.
[[MapData]] , но так как this=proxy , то не может его найти и завершается с ошибкой.
К счастью, есть способ исправить это:
471/597
let map = new Map();
[Link]('test', 1);
alert([Link]('test')); // 1 (работает!)
Сейчас всё сработало, потому что get привязывает свойства-функции, такие как [Link] , к оригинальному
объекту map . Таким образом, когда реализация метода set попытается получить доступ к внутреннему слоту
this.[[MapData]] , то всё пройдёт благополучно.
Приватные поля
Нечто похожее происходит и с приватными полями классов.
Например, метод getName() осуществляет доступ к приватному полю #name , после проксирования он перестаёт
работать:
class User {
#name = "Гость";
getName() {
return this.#name;
}
}
alert([Link]()); // Ошибка
Причина всё та же: приватные поля реализованы с использованием внутренних слотов. JavaScript не использует
[[Get]]/[[Set]] при доступе к ним.
В вызове getName() значением this является проксированный user , в котором нет внутреннего слота с
приватными полями.
Решением, как и в предыдущем случае, является привязка контекста к методу:
class User {
#name = "Гость";
getName() {
return this.#name;
}
}
alert([Link]()); // Гость
472/597
Однако, такое решение имеет ряд недостатков, о которых уже говорилось: методу передаётся оригинальный объект,
который может быть передан куда-то ещё, и это может поломать всю функциональность проксирования.
class User {
constructor(name) {
[Link] = name;
[Link](this);
}
}
alert([Link](user)); // true
alert([Link](user)); // false
Как мы видим, после проксирования не получается найти объект user внутри множества allUsers , потому что
прокси – это другой объект.
Отключаемые прокси
Отключаемый (revocable) прокси – это прокси, который может быть отключён вызовом специальной функции.
Допустим, у нас есть какой-то ресурс, и мы бы хотели иметь возможность закрыть к нему доступ в любой момент.
Для того, чтобы решить поставленную задачу, мы можем использовать отключаемый прокси, без ловушек. Такой
прокси будет передавать все операции на проксируемый объект, и у нас будет возможность в любой момент
отключить это.
Синтаксис:
let object = {
data: "Важные данные"
};
// позже в коде
revoke();
473/597
// прокси больше не работает (отключён)
alert([Link]); // Ошибка
Вызов revoke() удаляет все внутренние ссылки на оригинальный объект из прокси, так что между ними больше нет
связи, и оригинальный объект теперь может быть очищен сборщиком мусора.
Мы можем хранить функцию revoke в WeakMap , чтобы легко найти её по объекту прокси:
let object = {
data: "Важные данные"
};
[Link](proxy, revoke);
// ..позже в коде..
revoke = [Link](proxy);
revoke();
Преимущество такого подхода в том, что мы не должны таскать функцию revoke повсюду. Мы получаем её при
необходимости из revokes по объекту прокси.
Мы использовали WeakMap вместо Map , чтобы не блокировать сборку мусора. Если прокси объект становится
недостижимым (то есть на него больше нет ссылок), то WeakMap позволяет сборщику мусора удалить его из памяти
вместе с соответствующей функцией revoke , которая в этом случае больше не нужна.
Ссылки
Итого
Прокси – это обёртка вокруг объекта, которая «по умолчанию» перенаправляет операции над ней на объект, но имеет
возможность перехватывать их.
Проксировать можно любой объект, включая классы и функции.
Синтаксис:
…Затем обычно используют прокси везде вместо оригинального объекта target . Прокси не имеет собственных
свойств или методов. Он просто перехватывает операцию, если имеется соответствующая ловушка, а иначе
перенаправляет её сразу на объект target .
Мы можем перехватывать:
● Чтение ( get ), запись ( set ), удаление ( deleteProperty ) свойства (даже несуществующего).
● Вызов функции ( apply ).
● Оператор new (ловушка construct ).
● И многие другие операции (полный список приведён в начале статьи, а также в документации ).
Это позволяет нам создавать «виртуальные» свойства и методы, реализовывать значения по умолчанию,
наблюдаемые объекты, функции-декораторы и многое другое.
Мы также можем оборачивать один и тот же объект много раз в разные прокси, добавляя ему различные аспекты
функциональности.
474/597
Reflect API создано как дополнение к Proxy . Для любой ловушки из Proxy существует метод в Reflect с теми
же аргументами. Нам следует использовать его, если нужно перенаправить вызов на оригинальный объект.
Прокси имеют некоторые ограничения:
● Встроенные объекты используют так называемые «внутренние слоты», доступ к которым нельзя проксировать.
Однако, ранее в этой главе был показан один способ, как обойти это ограничение.
● То же самое можно сказать и о приватных полях классов, так как они реализованы на основе слотов. То есть
вызовы проксированных методов должны иметь оригинальный объект в качестве this , чтобы получить к ним
доступ.
● Проверка объектов на строгое равенство === не может быть перехвачена.
● Производительность: конкретные показатели зависят от интерпретатора, но в целом получение свойства с
помощью простейшего прокси занимает в несколько раз больше времени. В реальности это имеет значение только
для некоторых «особо нагруженных» объектов.
Задачи
Создайте прокси, который генерирует ошибку при попытке прочитать несуществующее свойство.
Напишите функцию wrap(target) , которая берёт объект target и возвращает прокси, добавляющий в него этот
аспект функциональности.
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
/* ваш код */
});
}
user = wrap(user);
alert([Link]); // John
alert([Link]); // Ошибка: такого свойства не существует
К решению
В некоторых языках программирования возможно получать элементы массива, используя отрицательные индексы,
отсчитываемые с конца.
Вот так:
475/597
let array = [1, 2, 3];
alert( array[-1] ); // 3
alert( array[-2] ); // 2
К решению
Observable
function makeObservable(target) {
/* ваш код */
}
Другими словами, возвращаемый makeObservable объект аналогичен исходному, но также имеет метод
observe(handler) , который позволяет запускать handler при любом изменении свойств.
При изменении любого свойства вызывается handler(key, value) с именем и значением свойства.
P.S. В этой задаче ограничьтесь, пожалуйста, только записью свойства. Остальные операции могут быть реализованы
похожим образом.
К решению
Например:
Строка кода может быть большой, содержать переводы строк, объявления функций, переменные и т.п.
476/597
let value = eval('let i = 0; ++i');
alert(value); // 1
Код в eval выполняется в текущем лексическом окружении, поэтому ему доступны внешние переменные:
let a = 1;
function f() {
let a = 2;
eval('alert(a)'); // 2
}
f();
let x = 5;
eval("x = 10");
alert(x); // 10, значение изменено
В строгом режиме у eval имеется своё лексическое окружение. Поэтому функции и переменные, объявленные
внутри eval , нельзя увидеть снаружи:
Без use strict у eval не будет отдельного лексического окружения, поэтому x и f будут видны из внешнего
кода.
Использование «eval»
В современной разработке на JavaScript eval используется весьма редко. Есть даже известное выражение – «eval is
evil» («eval – это зло»).
Причина такого отношения достаточно проста: давным-давно JavaScript был не очень развитым языком, и многие
вещи можно было сделать только с помощью eval . Но та эпоха закончилась более десяти лет назад.
На данный момент нет никаких причин, чтобы продолжать использовать eval . Если кто-то всё ещё делает это, то
очень вероятно, что они легко смогут заменить eval более современными конструкциями или JavaScript-модулями.
Пожалуйста, имейте в виду, что код в eval способен получать доступ к внешним переменным, и это может иметь
побочные эффекты.
Минификаторы кода (инструменты, используемые для сжатия JS-кода перед тем, как отправить его конечным
пользователям) заменяют локальные переменные на другие с более короткими именами для оптимизации. Обычно
это безопасная манипуляция, но не тогда, когда в коде используется eval , так как код из eval может изменять
значения локальных переменных. Поэтому минификаторы не трогают имена переменных, которые могут быть
доступны из eval . Это ухудшает степень сжатия кода.
Использование внутри eval локальных переменных из внешнего кода считается плохим решением, так как это
усложняет задачу по поддержке такого кода.
let x = 1;
{
let x = 5;
477/597
[Link]('alert(x)'); // 1 (глобальная переменная)
}
Если коду внутри eval нужны локальные переменные, поменяйте eval на new Function и передавайте
необходимые данные как аргументы:
f(5); // 5
Конструкция new Function объясняется в главе Синтаксис "new Function". Она создаёт функцию из строки в
глобальной области видимости. Так что локальные переменные для неё невидимы, но всегда можно передать их как
аргументы. Получается очень аккуратный код, как в примере выше.
Итого
Задачи
Eval-калькулятор
важность: 4
В этой задаче нет необходимости проверять полученное выражение на корректность, просто вычислить и вернуть
результат.
Запустить демо
К решению
Каррирование
Каррирование – продвинутая техника для работы с функциями. Она используется не только в JavaScript, но и в
других языках.
Каррирование – это трансформация функций таким образом, чтобы они принимали аргументы не как f(a, b, c) , а
как f(a)(b)(c) .
Каррирование не вызывает функцию. Оно просто трансформирует её.
Давайте сначала посмотрим на пример, чтобы лучше понять, о чём речь, а потом на практическое применение
каррирования.
Создадим вспомогательную функцию curry(f) , которая выполняет каррирование функции f с двумя аргументами.
Другими словами, curry(f) для функции f(a, b) трансформирует её в f(a)(b) .
478/597
function curry(f) { // curry(f) выполняет каррирование
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// использование
function sum(a, b) {
return a + b;
}
alert( curriedSum(1)(2) ); // 3
Более продвинутые реализации каррирования, как например _.curry из библиотеки lodash, возвращают обёртку,
которая позволяет запустить функцию как обычным образом, так и частично.
function sum(a, b) {
return a + b;
}
Каррирование? Зачем?
Чтобы понять пользу от каррирования, нам определённо нужен пример из реальной жизни.
Например, у нас есть функция логирования log(date, importance, message) , которая форматирует и выводит
информацию. В реальных проектах у таких функций есть много полезных возможностей, например, посылать логи по
сети, здесь для простоты используем alert :
log = _.curry(log);
479/597
// logNow будет частичным применением функции log с фиксированным первым аргументом
let logNow = log(new Date());
// используем её
logNow("INFO", "message"); // [HH:mm] INFO message
Теперь logNow – это log с фиксированным первым аргументом, иначе говоря, «частично применённая» или
«частичная» функция.
Мы можем пойти дальше и сделать удобную функцию для именно отладочных логов с текущим временем:
Итак:
1. Мы ничего не потеряли после каррирования: log всё так же можно вызывать нормально.
2. Мы можем легко создавать частично применённые функции, как сделали для логов с текущим временем.
В случае, если вам интересны детали, вот «продвинутая» реализация каррирования для функций с множеством
аргументов, которую мы могли бы использовать выше.
Она очень короткая:
function curry(func) {
Примеры использования:
function sum(a, b, c) {
return a + b + c;
}
480/597
}
};
Например, давайте посмотрим, что произойдёт в случае sum(a, b, c) . У неё три аргумента, так что [Link]
= 3.
Для вызова curried(1)(2)(3) :
1. Первый вызов curried(1) запоминает 1 в своём лексическом окружении и возвращает обёртку pass .
2. Обёртка pass вызывается с (2) : она берёт предыдущие аргументы ( 1 ), объединяет их с тем, что получила сама
(2) и вызывает curried(1, 2) со всеми ними. Так как число аргументов всё ещё меньше 3-х, curry
возвращает pass .
3. Обёртка pass вызывается снова с (3) . Для следующего вызова pass(3) берёт предыдущие аргументы ( 1 , 2 )
и добавляет к ним 3 , делая вызов curried(1, 2, 3) – наконец 3 аргумента, и они передаются оригинальной
функции.
Итого
Ссылочный тип
Некоторые хитрые способы вызова метода приводят к потере значения this , например:
let user = {
name: "Джон",
hi() { alert([Link]); },
bye() { alert("Пока"); }
481/597
};
В последней строчке кода используется условный оператор ? , который определяет, какой будет вызван метод
( [Link] или [Link] ) в зависимости от выполнения условия. В данном случае будет выбран [Link] .
Затем метод тут же вызывается с помощью скобок () . Но вызов не работает как положено!
Вы можете видеть, что при вызове будет ошибка, потому что значением "this" внутри функции становится
undefined (полагаем, что у нас строгий режим).
Так работает (доступ к методу объекта через точку):
[Link]();
Почему? Если мы хотим понять, почему так происходит, давайте разберёмся (заглянем под капот), как работает
вызов методов ( [Link]() ).
let user = {
name: "John",
hi() { alert([Link]); }
};
Здесь hi = [Link] сохраняет функцию в переменной, и далее в последней строке она вызывается полностью
сама по себе, без объекта, так что нет this .
Для работы вызовов типа [Link]() , JavaScript использует трюк – точка '.' возвращает не саму функцию,
а специальное значение «ссылочного типа», называемого Reference Type .
Этот ссылочный тип (Reference Type) является внутренним типом. Мы не можем явно использовать его, но он
используется внутри языка.
Значение ссылочного типа – это «триплет»: комбинация из трёх значений (base, name, strict) , где:
● base – это объект.
● name – это имя свойства объекта.
● strict – это режим исполнения. Является true, если действует строгий режим ( use strict ).
Результатом доступа к свойству [Link] является не функция, а значение ссылочного типа. Для [Link] в
строгом режиме оно будет таким:
482/597
// значение ссылочного типа (Reference Type)
(user, "hi", true)
Когда скобки () применяются к значению ссылочного типа (происходит вызов), то они получают полную
информацию об объекте и его методе, и могут поставить правильный this ( user в данном случае, по base ).
Ссылочный тип – исключительно внутренний, промежуточный, используемый, чтобы передать информацию от точки
. до вызывающих скобок () .
При любой другой операции, например, присваивании hi = [Link] , ссылочный тип заменяется на собственно
значение [Link] (функцию), и дальше работа уже идёт только с ней. Поэтому дальнейший вызов происходит уже
без this .
Таким образом, значение this передаётся правильно, только если функция вызывается напрямую с
использованием синтаксиса точки [Link]() или квадратных скобок obj['method']() (они делают то же
самое). Существуют различные способы решения этой проблемы: одним из таких является [Link]().
Итого
Вся механика скрыта от наших глаз. Это имеет значение только в особых случаях, например, когда метод
динамически извлекается из объекта с использованием выражения.
Задачи
Проверка синтаксиса
важность: 2
let user = {
name: "John",
go: function() { alert([Link]) }
}
([Link])()
К решению
obj = {
go: function() { alert(this); }
};
483/597
(method = [Link])(); // (3) undefined
К решению
Побитовые операторы
Побитовые операторы интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Они
производят операции, используя двоичное представление числа, и возвращают новую последовательность из 32 бит
(число) в качестве результата.
Эта глава требует дополнительных знаний в программировании и не очень важная, при первом чтении вы можете
пропустить её и вернуться потом, когда захотите понять, как побитовые операторы работают.
a = 0; // 00000000000000000000000000000000
a = 1; // 00000000000000000000000000000001
a = 2; // 00000000000000000000000000000010
a = 3; // 00000000000000000000000000000011
a = 255;// 00000000000000000000000011111111
00000000000000000000000100111010
11111111111111111111111011000101
Второй шаг – к полученному двоичному числу прибавить единицу, обычным двоичным сложением:
11111111111111111111111011000101 + 1 = 11111111111111111111111011000110 .
Итак, мы получили:
-314 = 11111111111111111111111011000110
Принцип дополнения до двойки делит все двоичные представления на два множества: если крайний-левый бит
равен 0 – число положительное, если 1 – число отрицательное. Поэтому этот бит называется знаковым битом.
484/597
Список операторов
В следующей таблице перечислены все побитовые операторы. Далее операторы разобраны более подробно.
Побитовое И (AND) a & b Ставит 1 на бит результата, для которого соответствующие биты операндов равны 1.
Побитовое ИЛИ (OR) a | b Ставит 1 на бит результата, для которого хотя бы один из соответствующих битов операндов равен 1.
Побитовое исключающее ИЛИ Ставит 1 на бит результата, для которого только один из соответствующих битов операндов равен 1 (но
a ^ b
(XOR) не оба).
Левый сдвиг a << b Сдвигает двоичное представление a на b битов влево, добавляя справа нули.
Правый сдвиг, переносящий знак a >> b Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты.
Правый сдвиг с заполнением Сдвигает двоичное представление a на b битов вправо, отбрасывая сдвигаемые биты и добавляя
a >>> b
нулями нули слева.
Например:
Без них перевод в двоичную систему и обратно был бы куда менее удобен. Более подробно они разбираются в
главе Числа.
& (Побитовое И)
a b a & b
0 0 0
0 1 0
1 0 0
1 1 1
485/597
Пример:
| (Побитовое ИЛИ)
Выполняет операцию ИЛИ над каждой парой бит. Результат a | b равен 1, если хотя бы один бит из a,b равен 1.
Таблица истинности для | :
a b a | b
0 0 0
0 1 1
1 0 1
1 1 1
Пример:
^ (Исключающее ИЛИ)
a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0
Как видно, оно даёт 1, если ЛИБО слева 1 , ЛИБО справа 1 , но не одновременно. Поэтому его и называют
«исключающее ИЛИ».
Пример:
486/597
Исключающее ИЛИ в шифровании
Исключающее или можно использовать для шифрования, так как эта операция полностью обратима. Двойное
применение исключающего ИЛИ с тем же аргументом даёт исходное число.
Иначе говоря, верна формула: a ^ b ^ b == a .
Пусть Вася хочет передать Пете секретную информацию data . Эта информация заранее превращена в число,
например строка интерпретируется как последовательность кодов символов.
Вася и Петя заранее договариваются о числовом ключе шифрования key .
Алгоритм:
● Вася берёт двоичное представление data и делает операцию data ^ key . При необходимости data бьётся
на части, равные по длине key , чтобы можно было провести побитовое ИЛИ ^ для каждой части. В JavaScript
оператор ^ работает с 32-битными целыми числами, так что data нужно разбить на последовательность
таких чисел.
● Результат data ^ key отправляется Пете, это шифровка.
Например, пусть в data очередное число равно 9 , а ключ key равен 1220461917 .
● Петя, получив очередное число шифровки 1220461908 , применяет к нему такую же операцию ^ key .
● Результатом будет исходное число data .
В нашем случае:
Конечно, такое шифрование поддаётся частотному анализу и другим методам дешифровки, поэтому современные
алгоритмы используют операцию XOR ^ как одну из важных частей более сложной многоступенчатой схемы.
~ (Побитовое НЕ)
a ~a
0 1
1 0
Пример:
487/597
9 (по осн. 10)
= 00000000000000000000000000001001 (по осн. 2)
--------------------------------
~9 (по осн. 10)
= 11111111111111111111111111110110 (по осн. 2)
= -10 (по осн. 10)
alert( ~3 ); // -4
alert( ~-1 ); // 0
9 (по осн.10)
= 00000000000000000000000000001001 (по осн.2)
--------------------------------
9 << 2 (по осн.10)
= 00000000000000000000000000100100 (по осн.2)
= 36 (по осн.10)
Операция << 2 сдвинула и отбросила два левых нулевых бита и добавила справа два новых нулевых.
Конечно, следует иметь в виду, что побитовые операторы работают только с 32-битными числами, поэтому
верхний порог такого «умножения» ограничен:
9 (по осн.10)
= 00000000000000000000000000001001 (по осн.2)
--------------------------------
9 >> 2 (по осн.10)
= 00000000000000000000000000000010 (по осн.2)
= 2 (по осн.10)
488/597
Операция >> 2 сдвинула вправо и отбросила два правых бита 01 и добавила слева две копии первого бита 00 .
Аналогично, -9 >> 2 даст -3 :
-9 (по осн.10)
= 11111111111111111111111111110111 (по осн.2)
--------------------------------
-9 >> 2 (по осн.10)
= 11111111111111111111111111111101 (по осн.2) = -3 (по осн.10)
Здесь операция >> 2 сдвинула вправо и отбросила два правых бита 11 и добавила слева две копии первого бита
11 . , Знак числа сохранён, так как крайний-левый (знаковый) бит сохранил значение 1 .
-9 (по осн.10)
= 11111111111111111111111111110111 (по осн.2)
--------------------------------
-9 >>> 2 (по осн.10)
= 00111111111111111111111111111101 (по осн.2)
= 1073741821 (по осн.10)
Осторожно, приоритеты!
В JavaScript побитовые операторы ^ , & , | выполняются после сравнений == .
Например, в сравнении a == b^0 будет сначала выполнено сравнение a == b , а потом уже операция ^0 , как
будто стоят скобки (a == b)^0 .
Обычно это не то, чего мы хотим. Чтобы гарантировать желаемый порядок, нужно ставить скобки: a == (b^0) .
Маска
Для этого примера представим, что наш скрипт работает с пользователями.
У них могут быть различные роли в проекте:
● Гость
● Редактор
● Админ
489/597
Например, Гость может лишь просматривать статьи сайта, а Редактор – ещё и редактировать их, и тому
подобное.
Что-то в таком духе:
Пользователь Просмотр статей Изменение статей Просмотр товаров Изменение товаров Управление правами
Редактор Да Да Да Да Нет
Админ Да Да Да Да Да
Если вместо «Да» поставить 1 , а вместо «Нет» – 0 , то каждый набор доступов описывается числом:
Пользователь Просмотр статей Изменение статей Просмотр товаров Изменение товаров Управление правами В 10-ной системе
Гость 1 0 1 0 0 = 20
Редактор 1 1 1 1 0 = 30
Админ 1 1 1 1 1 = 31
В последней колонке находится десятичное число, которое получится, если прочитать строку доступов в двоичном
виде.
Например, доступ гостя 10100 = 20 .
Такая интерпретация доступов позволяет «упаковать» много информации в одно число. Это экономит память, а
кроме этого – это удобно, поскольку в дополнение к экономии – по такому значению очень легко проверить, имеет ли
посетитель заданную комбинацию доступов!
Для этого посмотрим, как в 2-ной системе представляется каждый доступ в отдельности.
● Доступ, соответствующий только управлению правами: 00001 (=1) (все нули кроме 1 на позиции,
соответствующей этому доступу).
● Доступ, соответствующий только изменению товаров: 00010 (=2) .
● Доступ, соответствующий только просмотру товаров: 00100 (=4) .
● Доступ, соответствующий только изменению статей: 01000 (=8) .
● Доступ, соответствующий только просмотру статей: 10000 (=16) .
Доступ одновременно на просмотр и изменение статей – это двоичное число с 1 на соответствующих позициях, то
есть access = 11000 .
Как правило, доступы задаются в виде констант:
Из этих констант получить нужную комбинацию доступов можно при помощи операции | .
Теперь, чтобы понять, есть ли в доступе editor нужный доступ, например управление правами – достаточно
применить к нему побитовый оператор И ( & ) с соответствующей константой.
Ненулевой результат будет означать, что доступ есть:
Такая проверка работает, потому что оператор И ставит 1 на те позиции результата, на которых в обоих операндах
стоит 1 .
490/597
Можно проверить один из нескольких доступов.
Например, проверим, есть ли права на просмотр ИЛИ изменение товаров. Соответствующие права задаются битом
1 на втором и третьем месте с конца, что даёт число 00110 (= 6 в 10-ной системе).
alert( admin & check ); // не 0, значит есть доступ к просмотру ИЛИ изменению
Битовой маской называют как раз комбинацию двоичных значений ( check в примере выше), которая используется
для проверки и выборки единиц на нужных позициях.
Маски могут быть весьма удобны.
В частности, их используют в функциях, чтобы одним параметром передать несколько «флагов», т.е. однобитных
значений.
Пример вызова функции с маской:
Это довольно-таки коротко и элегантно, но, вместе с тем, применение масок налагает определённые ограничения. В
частности, побитовые операторы в JavaScript работают только с 32-битными числами, а значит, к примеру, 33 доступа
уже в число не упакуешь. Да и работа с двоичной системой счисления – как ни крути, менее удобна, чем с десятичной
или с обычными логическими значениями true/false .
Поэтому основная сфера применения масок – это быстрые вычисления, экономия памяти, низкоуровневые операции,
связанные с рисованием из JavaScript (3d-графика), интеграция с некоторыми функциями ОС (для серверного
JavaScript), и другие ситуации, когда уже существуют функции, требующие битовую маску.
Округление
Так как битовые операции отбрасывают десятичную часть, то их можно использовать для округления. Достаточно
взять любую операцию, которая не меняет значение числа.
Например, двойное НЕ ( ~ ):
alert( ~~12.345 ); // 12
alert( 12.345 ^ 0 ); // 12
Проверка на −1
Внутренний формат 32-битных чисел устроен так, что для смены знака нужно все биты заменить на
противоположные («обратить») и прибавить 1 .
Обращение битов – это побитовое НЕ ( ~ ). То есть, при таком формате представления числа -n = ~n + 1 . Или,
если перенести единицу: ~n = -(n+1) .
Как видно из последнего равенства, ~n == 0 только если n == -1 . Поэтому можно легко проверить равенство n
== -1 :
491/597
let n = 5;
let n = -1;
При этом следует иметь в виду, что максимальный верхний порог такого умножения меньше, чем обычно, так как
побитовый оператор манипулирует 32-битными целыми, в то время как обычные операторы работают с числами
длиной 64 бита.
Оператор сдвига в другую сторону a >> b , производит обратную операцию – целочисленное деление a на 2b .
Итого
● Бинарные побитовые операторы: & | ^ << >> >>> .
● Унарный побитовый оператор один: ~ .
Задачи
Почему побитовые операции в примерах ниже не меняют число? Что они делают внутри?
492/597
alert( 123 ^ 0 ); // 123
alert( 0 ^ 123 ); // 123
alert( ~~123 ); // 123
К решению
Напишите функцию isInteger(num) , которая возвращает true , если num – целое число, иначе false .
Например:
К решению
● (a ^ b) == (b ^ a)
● (a & b) == (b & a)
● (a | b) == (b | a)
Иными словами, при перемене мест – всегда ли результат останется тем же?
К решению
К решению
BigInt
Новая возможность
Эта возможность была добавлена в язык недавно. Узнать, где есть поддержка, можно на
[Link] .
BigInt – это специальный числовой тип, который предоставляет возможность работать с целыми числами
произвольной длины.
Чтобы создать значение типа BigInt , необходимо добавить n в конец числового литерала или вызвать функцию
BigInt , которая создаст число типа BigInt из переданного аргумента. Аргументом может быть число, строка и др.
493/597
const bigintFromNumber = BigInt(10); // то же самое, что и 10n
Математические операторы
alert(1n + 2n); // 3
alert(5n / 2n); // 2
Обратите внимание: операция деления 5/2 возвращает округлённый результат, без дробной части. Все операции с
числами типа bigint возвращают bigint .
В математических операциях мы не можем смешивать bigint и обычные числа:
Конвертирование bigint в число всегда происходит неявно и без генерации ошибок, но если значение bigint
слишком велико и не подходит под тип number , то дополнительные биты будут отброшены, так что следует быть
осторожными с такими преобразованиями.
Операции сравнения
Операции сравнения, такие как < , > , работают с bigint и обычными числами как обычно:
Пожалуйста, обратите внимание, что обычные и bigint числа принадлежат к разным типам, они могут быть равны
только при нестрогом сравнении == :
alert( 1 == 1n ); // true
494/597
Логические операции
В if или любом другом логическом операторе bigint число ведёт себя как обычное число.
К примеру, в if bigint 0n преобразуется в false , другие значения преобразуются в true :
if (0n) {
// никогда не выполнится
}
Логические операторы || , && и другие также работают с bigint числами как с обычными числами:
alert( 1n || 2 ); // 1
alert( 0n || 2 ); // 2
Полифилы
Создание полифила для BigInt – достаточно непростая задача. Причина в том, что многие операторы в JavaScript,
такие как + , - и др., ведут себя по-разному с bigint по сравнению с обычными числами.
К примеру, деление bigint числа всегда возвращает bigint (округлённое при необходимости).
Чтобы эмулировать такое поведение, полифил должен будет проанализировать код и заменить все такие операторы
на свои вызовы. Такая реализация будет тяжеловесной, не очень хорошей с точки зрения производительности.
Вот почему на данный момент нет хорошо реализованного полифила.
Существует обратное решение, предложеное разработчиками библиотеки JSBI .
Эта библиотека реализует большие числа, используя собственные методы. Мы можем использовать их вместо
встроенных BigInt :
Сложение c = a + b c = [Link](a, b)
Вычитание c = a - b c = [Link](a, b)
… … …
…А затем использовать полифил (плагин Babel) для замены вызовов JSBI на встроенные Bigint для браузеров,
которые их поддерживают.
Другими словами, данный подход предлагает использовать JSBI вместо встроенных BigInt . JSBI внутри себя
работает с числами как с BigInt , эмулирует их с соблюдением всех требований спецификации. Таким образом, мы
можем выполнять JSBI-код в интерпретаторах, которые не поддерживают Bigint , а для тех, которые поддерживают
– полифил преобразует вызовы в обычные Bigint .
Ссылки
● MDN: BigInt .
● Спецификация: BigInt .
Как мы уже знаем, строки в JavaScript основаны на Юникоде : каждый символ представляет из себя
последовательность байтов из 1-4 байтов.
495/597
JavaScript позволяет нам вставить символ в строку, указав его шестнадцатеричный Юникод с помощью одной из этих
трех нотаций:
● \xXX
Вместо XX должны быть указаны две шестнадцатеричные цифры со значением от 00 до FF . В этом случае
\xXX – это символ, Юникод которого равен XX .
Поскольку нотация \xXX поддерживает только две шестнадцатеричные цифры, ее можно использовать только
для первых 256 символов Юникода.
Эти 256 символов включают в себя латинский алфавит, большинство основных синтаксических символов и
некоторые другие. Например, "\x7A" – это то же самое, что "z" (Юникод U+007A ).
alert( "\x7A" ); // z
alert( "\xA9" ); // ©, символ авторского права
● \uXXXX
Вместо XXXX должны быть указаны ровно 4 шестнадцатеричные цифры со значением от 0000 до FFFF . В этом
случае \uXXXX – это символ, Юникод которого равен XXXX .
Символы со значениями Юникода, превышающими U+FFFF , также могут быть представлены с помощью этой
нотации, но в таком случае нам придется использовать так называемую суррогатную пару (о ней мы поговорим
позже в этой главе).
● \u{X…XXXXXX}
Вместо X…XXXXXX должно быть шестнадцатеричное значение от 1 до 6 байт от 0 до 10FFFF (максимальная
точка кода, определенная стандартом Юникод). Эта нотация позволяет нам легко представлять все существующие
символы Юникода.
Суррогатные пары
Все часто используемые символы имеют 2-байтовые коды (4 шестнадцатеричные цифры). В большинстве
европейских языков буквы, цифры и основные унифицированные идеографические наборы CJK (CJK – от китайской,
японской и корейской систем письма) имеют 2-байтовое представление.
Изначально JavaScript был основан на кодировке UTF-16, которая предусматривала только 2 байта на один символ.
Однако 2 байта допускают только 65536 комбинаций, и этого недостаточно для всех возможных символов Юникода.
Поэтому редкие символы, требующие более 2 байт, кодируются парой 2-байтовых символов, которые называются
«суррогатной парой».
Побочным эффектом является то, что длина таких символов равна 2 :
Это происходит потому, что суррогатные пары не существовали в то время, когда был создан JavaScript, и поэтому
они не обрабатываются языком корректно.
На самом деле в каждой из приведенных строк у нас по одному символу, но свойство length показывает длину 2 .
Получить такой символ также может быть непросто, поскольку большинство языковых функций рассматривают
суррогатные пары как два символа.
Например, здесь мы видим два странных символа в выводе:
496/597
alert( '𝒳'[0] ); // показывает странные символы...
alert( '𝒳'[1] ); // ...части суррогатной пары
// charCodeAt не учитывает суррогатные пары, поэтому он выдает коды для 1-й части 𝒳:
При этом, если брать с позиции 1 (а это здесь скорее неверно), то они оба возвращают только 2-ю часть пары:
Другие способы работы с суррогатными парами вы найдете в главе Перебираемые объекты. Возможно, для этого
тоже существуют специальные библиотеки, но они не настолько известные, чтобы предлагать их в учебнике.
Во многих языках есть символы, состоящие из основного символа и знака над/под ним.
Например, буква a может быть основой для этих символов: àáâäãåā .
Большинство распространенных «составных» символов имеют свой собственный код в таблице Юникода. Но не все,
потому что существует слишком большое количество возможных комбинаций.
Для поддержки любых комбинаций стандарт Юникод позволяет нам использовать несколько Юникодных символов:
основной символ, за которым следует один или много символов-«меток», которые «украшают» его.
Например, если за S следует специальный символ «точка сверху» (код \u0307 ), то он отобразится как Ṡ.
alert( 'S\u0307' ); // Ṡ
Если нам нужен дополнительный знак над буквой (или под ней) – нет проблем, просто добавляем соответствующий
символ.
497/597
Например, если мы добавим символ «точка снизу» (код \u0323 ), то получим «S с точками сверху и снизу»: Ṩ .
Вот, как это будет выглядеть:
Это обеспечивает большую гибкость, но при этом возникает определенная проблема: два символа могут визуально
выглядеть одинаково, но при этом они будут представлены разными комбинациями Юникода.
Например:
Для решения этой проблемы предусмотрен алгоритм «Юникодной нормализации», приводящий каждую строку к
единому «нормальному» виду.
Его реализует метод [Link]() .
Забавно, но в нашем случае normalize() «схлопывает» последовательность из трёх символов в один: \u1e68 —
S с двумя точками.
alert( "S\u0307\u0323".normalize().length ); // 1
В действительности это не всегда так. Причина в том, что символ Ṩ является «достаточно распространенным»,
поэтому создатели стандарта Юникод включили его в основную таблицу и присвоили ему код.
Если вы хотите узнать больше о правилах и вариантах нормализации – они описаны в дополнении к стандарту
Юникод: Unicode Normalization Forms , но для большинства практических целей достаточно информации из этого
раздела.
Общая проблема строк, дат, чисел в JavaScript – они «не в курсе» языка и особенностей стран, где находится
посетитель.
В частности:
Строки
При сравнении сравниваются коды символов, а это неправильно, к примеру, в русском языке оказывается, что "ё" >
"я" и "а" > "Я" , хотя всем известно, что я – последняя буква алфавита и это она должна быть больше любой
другой.
Даты
В разных странах принята разная запись дат. Где-то пишут 31.12.2014 (Россия), а где-то 12/31/2014 (США), где-то
иначе.
Числа
В одних странах выводятся цифрами, в других – иероглифами, длинные числа разделяются где-то пробелом, где-то
запятой.
Все современные браузеры, кроме IE10 (но есть библиотеки и для него) поддерживают стандарт ECMA 402 ,
предназначенный решить эти проблемы навсегда.
498/597
Основные объекты
[Link]
Умеет правильно сравнивать и сортировать строки.
[Link]
Умеет форматировать дату и время в соответствии с нужным языком.
[Link]
Умеет форматировать числа в соответствии с нужным языком.
Локаль
Также через суффикс -u-* можно указать расширения локалей, например "th-TH-u-nu-thai" – тайский язык
( th ), используемый в Таиланде ( TH ), с записью чисел тайскими буквами (๐, ๑, ๒, ๓, ๔, ๕, ๖, ๗, ๘, ๙) .
Стандарт, который описывает локали – RFC 5646 , языки описаны в IANA language registry .
Все методы принимают локаль в виде строки или массива, содержащего несколько локалей в порядке предпочтения.
Если локаль не указана или undefined – берётся локаль по умолчанию, установленная в окружении (браузере).
Строки, [Link]
Синтаксис:
// создание
let collator = new [Link]([locales, [options]])
Параметры:
499/597
locales
Локаль, одна или массив в порядке предпочтения.
options
Объект с дополнительными настройками:
● localeMatcher – алгоритм выбора подходящей локали.
●
usage – цель сравнения: сортировка "sort" или поиск "search" , по умолчанию "sort" .
● sensitivity – чувствительность: какие различия в символах учитывать, а какие – нет, варианты:
● base – учитывать только разные символы, без диакритических знаков и регистра, например: а ≠ б , е = ё , а
= А.
● accent – учитывать символы и диакритические знаки, например: а ≠ б , е ≠ ё , а = А .
● case – учитывать символы и регистр, например: а ≠ б , е = ё , а ≠ А .
● variant – учитывать всё: символ, диакритические знаки, регистр, например: а ≠ б , е ≠ ё , а ≠ А ,
используется по умолчанию.
● ignorePunctuation – игнорировать знаки пунктуации: true/false , по умолчанию false .
●
numeric – использовать ли численное сравнение: true/false , если true , то будет 12 > 2 , иначе 12 < 2 .
●
caseFirst – в сортировке должны идти первыми прописные или строчные буквы, варианты: "upper"
(прописные), "lower" (строчные) или "false" (стандартное для локали, также является значением по
умолчанию). Не поддерживается IE11.
В подавляющем большинстве случаев подходят стандартные параметры, то есть options указывать не нужно.
Использование:
Выше были использованы полностью стандартные настройки. Они различают регистр символа, но это различие
можно убрать, если настроить чувствительность sensitivity :
Даты, [Link]
Синтаксис:
// создание
let formatter = new [Link]([locales, [options]])
Первый аргумент – такой же, как и в Collator , а в объекте options мы можем определить, какие именно части
даты показывать (часы, месяц, год…) и в каком формате.
Полный список свойств options :
500/597
Свойство Описание Возможные значения По умолчанию
hour12 Включать ли время в 12-часовом формате true -- 12-часовой формат, false -- 24-часовой
month Месяц 2-digit , numeric , narrow , short , long undefined или numeric
Если указанный формат не поддерживается, то настройка formatMatcher задаёт алгоритм подбора наиболее
близкого формата: basic – по стандартным правилам и best fit – по умолчанию, на усмотрение окружения
(браузера).
Использование:
Например:
Только время:
501/597
hour: "numeric",
minute: "numeric",
second: "numeric"
});
Числа, [Link]
Форматтер [Link] умеет красиво форматировать не только числа, но и валюту, а также проценты.
Синтаксис:
[Link](number); // форматирование
Список опций:
Минимальное количество
minimumSignificantDigits от 1 до 21 1
значимых цифр
502/597
alert( [Link](1234.5) ); // 1 234,5 £
[Link]([locales [, options]])
Форматирует дату в соответствии с локалью, например:
[Link]([locales [, options]])
То же, что и выше, но опции по умолчанию включают в себя год, месяц, день
[Link]([locales [, options]])
То же, что и выше, но опции по умолчанию включают в себя часы, минуты, секунды
[Link]([locales [, options]])
Форматирует число, используя опции [Link] .
Все эти методы при запуске создают соответствующий объект Intl.* и передают ему опции, можно рассматривать
их как укороченные варианты вызова.
Старые IE
Задачи
503/597
В этом примере порядок сортировки не должен зависеть от регистра.
Что касается буквы "ё" , то мы следуем обычным правилам сортировки буквы ё , по которым «е» и «ё» считаются
одной и той же буквой, за исключением случая, когда два слова отличаются только в позиции буквы «е» / «ё» – тогда
слово с «е» ставится первым.
К решению
WeakRef и FinalizationRegistry
Вспоминая основную концепцию принципа достижимости из главы Сборка мусора, мы можем отметить, что движок
JavaScript гарантированно хранит в памяти значения, которые доступны или используются.
Например:
Объект { name: "John" } удалился бы из памяти только в случае отсутствия сильных ссылок на него (если бы мы
также перезаписали значение переменной admin ).
В JavaScript существует концепция под названием WeakRef , которая ведёт себя немного иначе в этом случае.
Слабая ссылка – это ссылка на объект или значение, которая не предотвращает их удаление сборщиком мусора.
Объект или значение могут быть удалены сборщиком мусора в случае, если на них существуют только слабые
ссылки.
504/597
WeakRef
Предостережение
Прежде чем мы перейдём к изучению, стоит отметить, что правильное применение структур, о которых пойдёт
речь в этой статье, требует очень тщательного обдумывания, и по возможности их использования лучше избегать.
WeakRef – это объект, содержащий слабую ссылку на другой объект, называемый target или referent .
Особенность WeakRef заключается в том, что он не препятствует сборщику мусора удалять свой объект-референт.
Другими словами, он просто не удерживает его «в живых».
Теперь давайте возьмём переменную user в качестве «референта» и создадим слабую ссылку от неё к переменной
admin . Чтобы создать слабую ссылку, необходимо использовать конструктор WeakRef , передав целевой объект
(объект, на который вы хотите создать слабую ссылку).
В нашем случае — это переменная user :
На схеме ниже изображены два типа ссылок: сильная ссылка с использованием переменной user и слабая ссылка с
использованием переменной admin :
<global>
user admin
Object
name: "John"
Затем, в какой-то момент, мы перестаём использовать переменную user – она перезаписывается, выходит из
области видимости и т.д., при этом сохраняя экземпляр WeakRef в переменной admin :
Слабой ссылки на объект недостаточно, чтобы сохранить его «в живых». Когда единственными оставшимися
ссылками на объект-референт являются слабые ссылки, сборщик мусора вправе уничтожить этот объект и
использовать его память для чего-то другого.
Однако до тех пор, пока объект фактически не уничтожен, слабая ссылка может вернуть его, даже если на данный
объект больше нет сильных ссылок. То есть наш объект становится своеобразным «котом Шрёдингера » – мы не
можем знать точно, «жив» он или «мёртв»:
<global>
admin
Object
name: "John"
505/597
На этом этапе, чтобы получить объект из экземпляра WeakRef , мы воспользуемся его методом deref() .
Метод deref() возвращает объект-референт, на который ссылается WeakRef , в случае, если объект всё ещё
находится в памяти. Если объект был удалён сборщиком мусора, – метод deref() вернёт undefined :
if (ref) {
// объект всё ещё доступен: можем произвести какие-либо манипуляции с ним
} else {
// объект был удалён сборщиком мусора
}
WeakRef обычно используется для создания кешей или ассоциативных массивов , в которых хранятся
ресурсоёмкие объекты. Это позволяет избежать предотвращение удаления этих объектов сборщиком мусора только
на основе их присутствия в кеше или ассоциативном массиве.
Один из основных примеров – это ситуация, когда у нас есть большое количество объектов бинарных изображений
(например, представленных в виде ArrayBuffer или Blob ), и мы хотим связать имя или путь с каждым
изображением. Существующие структуры данных не совсем подходят для этих целей:
● Использование Map для создания ассоциаций между именами и изображениями, или наоборот, сохранит объекты
изображений в памяти, поскольку они фигурируют в Map в качестве ключей или значений.
● WeakMap также не подойдёт в этом случае: из-за того, что объекты, представленные в качестве ключей WeakMap
используют слабые ссылки, и не защищены от удаления сборщиком мусора.
Но, в данной ситуации нам нужна структура данных, которая бы использовала слабые ссылки в своих значениях.
Для этого мы можем использовать коллекцию Map , значениями которой являются экземпляры WeakRef ,
ссылающиеся на нужные нам большие объекты. Следовательно, мы не будем хранить в памяти эти большие и
ненужные объекты дольше, чем требуется.
В противном случае это способ получить объект изображения из кеша, если он всё ещё доступен. Если же он был
удалён сборщиком мусора, мы сгенерируем или скачаем его заново.
Таким образом, в некоторых ситуациях используется меньше памяти.
function fetchImg() {
// абстрактная функция для загрузки изображений...
}
if (cachedImg?.deref()) { // (5)
return cachedImg?.deref();
}
return newImg;
};
}
506/597
Давайте подробно разберём всё, что тут произошло:
1. weakRefCache – функция высшего порядка, которая принимает другую функцию fetchImg в качестве
аргумента. В данном примере мы можем пренебречь подробным описанием функции fetchImg , так как это может
быть абсолютно любая логика скачивания изображений.
2. imgCache – кеш изображений, который хранит кешированные результаты функции fetchImg , в виде строковых
ключей (имя изображения) и объектов WeakRef в качестве их значений.
3. Возвращаем анонимную функцию, которая принимает имя изображения в качестве аргумента. Данный аргумент
будет использоваться в качестве ключа для кешированного изображения.
4. Пытаемся получить кешированный результат из кеша, используя предоставленный ключ (имя изображения).
5. Если кеш содержит значение по указанному ключу, и объект WeakRef не был удалён сборщиком мусора,
возвращаем кешированный результат.
6. Если в кеше нет записи с запрошенным ключом, либо метод deref() возвращает undefined (что означает, что
объект WeakRef был удалён сборщиком мусора), функция fetchImg скачивает изображение заново.
7. Помещаем скачанное изображение в кеш в виде WeakRef объекта.
Теперь у нас есть коллекция Map , в которой ключи – это имена изображений в виде строк, а значения – это объекты
WeakRef , содержащие сами изображения.
Эта техника помогает избежать выделения большого объёма памяти на ресурсоёмкие объекты, которые больше
никто не использует. Также она экономит память и время в случае повторного использования кешированных
объектов.
Вот визуальное представление того, как выглядит этот код:
ключ значение
Но, у данной реализации есть свои недостатки: со временем Map будет заполняться строками в качестве ключей,
которые указывают на WeakRef , чей объект-референт уже был удалён сборщиком мусора:
ключ значение
Один из способов справиться с этой проблемой – это периодически проверять кеш и удалять «мёртвые» записи.
Другой способ – использовать финализаторы, с которыми мы ознакомимся далее.
507/597
[Link]
При нажатии на кнопку «Начать отправку сообщений», в так называемом «окне отображения логов» (элемент с
классом .window__body ) начинают появляться надписи (логи).
Но, как только этот элемент удалится из DOM, логгер должен перестать присылать сообщения. Чтобы воспроизвести
удаление данного элемента, достаточно лишь нажать на кнопку «Закрыть» в правом верхнем углу.
Для того, чтобы нам не усложнять работу, и не уведомлять сторонний код каждый раз, когда наш DOM-элемент
доступен, а когда – нет, достаточно будет создать на него слабую ссылку с помощью WeakRef .
После того как элемент будет удалён из DOM, логгер это увидит и перестанет присылать сообщения.
Теперь давайте рассмотрим исходный код детальнее (вкладка [Link] ):
1. Получаем DOM-элемент кнопки «Начать отправку сообщений».
2. Получаем DOM-элемент кнопки «Закрыть».
3. Получаем DOM-элемент окна отображения логов с использованием конструктора new WeakRef() . Таким образом
переменная windowElementRef хранит слабую ссылку на DOM-элемент.
4. Добавляем обработчик событий на кнопку «Начать отправку сообщений», отвечающий за запуск логгера по
нажатию.
5. Добавляем обработчик событий на кнопку «Закрыть», отвечающий за закрытие окна отображения логов по
нажатию.
6. С помощью setInterval запускаем отображение нового сообщения каждую секунду.
7. Если DOM-элемент окна отображения логов всё ещё доступен и находится в памяти, создаём и отправляем новое
сообщение.
8. Если метод deref() возвращает undefined , это значит, что DOM-элемент был удалён из памяти. В таком
случае логгер прекращает показ сообщений и сбрасывает таймер.
9. alert , который будет вызван после того, как DOM-элемент окна отображения логов удалится из памяти (т.е.
после нажатия на кнопку «Закрыть»). Обратите внимание, что удаление из памяти может произойти не сразу,
т.к оно зависит только от внутренних механизмов сборщика мусора.
Мы не можем контролировать этот процесс напрямую из кода. Но, несмотря на это, у нас всё ещё есть
возможность выполнить принудительную сборку мусора из бразуера.
В Google Chrome, например, для этого нужно открыть инструменты разработчика ( Ctrl + Shift + J на
Windows/Linux или Option + ⌘ + J на macOS), перейти во вкладку «Производительность (Performance)» и
нажать на кнопку с иконкой урны – «Собрать мусор (Collect garbage)»:
FinalizationRegistry
А теперь пришло время поговорить о финализаторах. Прежде чем мы перейдём дальше, давайте разберёмся с
терминологией:
Колбэк очистки (финализатор) – это функция, которая выполняется в случае, если объект, зарегистрированный в
FinalizationRegistry , удаляется из памяти сборщиком мусора.
508/597
Его цель – предоставить возможность выполнения дополнительных операций, связанных с объектом, после его
окончательного удаления из памяти.
Реестр (или FinalizationRegistry ) – это специальный объект в JavaScript, который управляет регистрацией и
отменой регистрации объектов и их колбэков очистки.
Этот механизм позволяет зарегистрировать объект для отслеживания и связать с ним колбэк очистки. По сути, это
структура, которая хранит информацию о зарегистрированных объектах и их колбэках очистки, а затем
автоматически вызывает эти колбэки при удалении объектов из памяти.
Для создания экземпляра реестра FinalizationRegistry , необходимо вызвать его конструктор, который
принимает единственный аргумент – колбэк очистки (финализатор).
Синтаксис:
function cleanupCallback(heldValue) {
// код колбэка очистки
}
Здесь:
● cleanupCallback – колбэк очистки, который будет автоматически вызван при удалении зарегистрированного
объекта из памяти.
● heldValue – значение, которое передаётся в качестве аргумента для колбэка очистки. Если heldValue
является объектом, реестр сохраняет на него сильную ссылку.
● registry – экземпляр FinalizationRegistry .
Методы FinalizationRegistry :
● register(target, heldValue [, unregisterToken]) – используется для регистрации объектов в реестре.
target – регистрируемый для отслеживания объект. Если target будет удалён сборщиком мусора, колбэк
очистки будет вызван с heldValue в качестве аргумента.
Опциональный unregisterToken – токен отмены регистрации. Может быть передан для отмены регистрации до
удаления объекта сборщиком мусора. Обычно в качестве unregisterToken используется объект target , что
является стандартной практикой.
● unregister(unregisterToken) – метод unregister используется для отмены регистрации объекта в
реестре. Он принимает один аргумент – unregisterToken (токен отмены регистрации, который был получен при
регистрации объекта).
Теперь перейдём к простому примеру. Воспользуемся уже известным нам объектом user и создадим экземпляр
FinalizationRegistry :
Затем зарегистрируем объект, для которого требуется колбэк очистки, вызвав метод register :
[Link](user, [Link]);
Реестр не хранит сильную ссылку на регистрируемый объект, так как это бы противоречило его предназначению.
Если бы реестр сохранял сильную ссылку, то объект никогда бы не был очищен сборщиком мусора.
Если же объект удаляется сборщиком мусора, наш колбэк очистки может быть вызван в какой-то момент в будущем, с
переданным ему heldValue :
// Когда объект user удалится сборщиком мусора, в консоль будет выведено сообщение:
"John был собран сборщиком мусора."
509/597
Также существуют ситуации, когда даже в реализациях, где используется колбэк очистки, есть вероятность, что он не
будет вызван.
Например:
● Когда программа полностью завершает свою работу (например, при закрытии вкладки в браузере).
● Когда сам экземпляр FinalizationRegistry больше не доступен для JavaScript кода. Если объект, создающий
экземпляр FinalizationRegistry , выходит из области видимости или удаляется, то колбэки очистки,
зарегистрированные в этом реестре, также могут быть не вызваны.
Кеширование с FinalizationRegistry
function fetchImg() {
// абстрактная функция для загрузки изображений...
}
function weakRefCache(fetchImg) {
const imgCache = new Map();
if (cachedImg?.deref()) {
return cachedImg?.deref();
}
return newImg;
};
}
1. Для управления очисткой «мёртвых» записей в кеше, когда связанные с ними объекты WeakRef собираются
сборщиком мусора, создаём реестр очистки FinalizationRegistry .
Важным моментом здесь является то, что в колбэке очистки должно проверяться, была ли запись удалена
сборщиком мусора и не была ли добавлена заново, чтобы не удалить «живую» запись.
2. После загрузки и установки нового значения (изображения) в кеш, регистрируем его в реестре финализатора для
отслеживания объекта WeakRef .
Данная реализация содержит только актуальные или «живые» пары ключ/значение. В этом случае каждый объект
WeakRef зарегистрирован в FinalizationRegistry . А после того, как объекты будут очищены сборщиком
мусора, колбэк очистки удалит все значения undefined .
510/597
ключ значение
Ключевым аспектом в обновлённой реализации является то, что финализаторы позволяют создавать параллельные
процессы между «основной» программой и колбэками очистки. В контексте JavaScript, «основная» программа – это
наш JavaScript-код, который запускается и выполняется в нашем приложении или на веб-странице.
Следовательно, с момента, когда объект помечается для удаления сборщиком мусора, до фактического выполнения
колбэка очистки, может возникнуть определённый промежуток времени. Важно понимать, что в этом временном
интервале основная программа может внести любые изменения в объект или даже вернуть его обратно в память.
Поэтому, в колбэке очистки мы должны проверить, не была ли запись добавлена обратно в кеш основной
программой, чтобы избежать удаления «живых» записей. Аналогично, при поиске ключа в кеше существует
вероятность того, что значение было удалено сборщиком мусора, но колбэк очистки ещё не был выполнен.
Такие ситуации требуют особого внимания, если вы работаете с FinalizationRegistry .
Переходя от теории к практике, представьте себе реальный сценарий, когда пользователь синхронизирует свои
фотографии на мобильном устройстве с каким-либо облачным сервисом (таким как iCloud или Google Photos ), и
хочет просматривать их с других устройств. Подобные сервисы помимо основного функционала просмотра
фотографий, предлагают массу дополнительных возможностей, например:
● Редактирование фотографий и видео эффекты.
● Создание «воспоминаний» и альбомов.
● Монтаж видео из серии фотографий.
●
…и многое другое.
В качестве примера здесь мы будем использовать достаточно примитивную реализацию подобного сервиса.
Основная суть — показать возможный сценарий совместного использования WeakRef и FinalizationRegistry в
реальной жизни.
Вот как это выглядит:
511/597
В левой части находится облачная библиотека фотографий (они отображаются в виде миниатюр). Мы можем выбрать
нужные нам изображения и создать коллаж, нажав на кнопку «Create collage» в правой части страницы. Затем,
получившийся результат можно будет скачать в виде изображения.
Для увеличения скорости загрузки страницы разумно будет загружать и показывать миниатюры фотографий именно в
сжатом качестве. Но, для создания коллажа из выбранных фотографий, загружать и использовать их в
полноразмерном качестве.
Ниже мы видим, что внутренний размер миниатюр составляет 240×240 пикселей. Размер был выбран специально
для увеличения скорости загрузки. Кроме того, нам не нужны полноразмерные фотографии в режиме предпросмотра.
512/597
Предположим, что нам нужно создать коллаж из 4 фотографий: мы выбираем их, после чего нажимаем кнопку
«Create collage». На этом этапе уже известная нам функция weakRefCache проверяет, есть ли нужное изображение
в кеше. Если нет, то скачивает его из облака и помещает в кеш для возможности дальнейшего использования. И так
происходит для каждого выбранного изображения:
513/597
Обратив внимание на вывод в консоли можно увидеть, какие из фотографий были загружены из облака – на это
указывает FETCHED_IMAGE. Так как это первая попытка создания коллажа, это означает, что на данном этапе
«слабый кеш» ещё был пуст, а все фотографии были скачаны из облака и помещены в него.
Но, наряду с процессом загрузки изображений, происходит ещё и процесс очистки памяти сборщиком мусора. Это
означает, что хранящийся в кеше объект, на который мы ссылаемся используя слабую ссылку, удаляется сборщиком
мусора. И наш финализатор выполняется успешно, тем самым удаляя ключ, по которому изображение хранилось в
кеше. Об этом нас уведомляет CLEANED_IMAGE:
514/597
Далее мы понимаем, что нам не нравится получившийся коллаж, и решаем изменить одно из изображений и создать
новый. Для этого достаточно снять выделение с ненужного изображения, выбрать другое, и ещё раз нажать на кнопку
«Create collage»:
515/597
Но, на этот раз не все изображения были скачаны из сети, и одно из них было взято из слабого кеша: об этом нам
говорит сообщение CACHED_IMAGE. Это означает, что на момент создания коллажа сборщик мусора ещё не удалил
наше изображение, и мы смело взяли его из кеша, тем самым сократив количество сетевых запросов и ускорив
общее время процесса создания коллажа:
516/597
Давайте ещё немного «поиграем», заменив одно из изображений ещё раз и создав новый коллаж:
517/597
На этот раз результат ещё более внушительный. Из 4 выбранных изображений, 3 из них были взяты из слабого кеша,
и только одно пришлось скачать из сети. Снижение нагрузки на сеть составило около 75%. Впечатляет, не правда ли?
518/597
Конечно, не следует забывать, что такое поведение не является гарантированным, и зависит от конкретной
реализации и работы сборщика мусора.
Исходя из этого, сразу же возникает вполне логичный вопрос: почему бы нам не использовать обычный кеш, где мы
можем сами управлять его сущностями, а не полагаться на сборщик мусора? Всё верно, в большинстве случаев нет
необходимости использовать WeakRef и FinalizationRegistry .
Здесь мы просто продемонстрировали альтернативную реализацию подобного функционала, используя
нетривиальный подход с интересными особенностями языка. Всё же, мы не можем полагаться на этот пример, если
нам необходим постоянный и предсказуемый результат.
Вы можете открыть данный пример в песочнице .
Итого
WeakRef – предназначен для создания слабых ссылок на объекты, что позволяет им быть удалёнными из памяти
сборщиком мусора, если на них больше нет сильных ссылок. Это полезно для решения проблемы чрезмерного
использования памяти и оптимизации использования системных ресурсов в приложениях.
FinalizationRegistry – это средство регистрации колбэков, которые выполняются при уничтожении объектов, на
которые больше нет сильных ссылок. Это позволяет освобождать связанные с объектом ресурсы или выполнять
другие необходимые операции перед удалением объекта из памяти.
519/597
Решения
Привет, мир!
Вызвать alert
К условию
HTML-код:
<!DOCTYPE html>
<html>
<body>
<script src="[Link]"></script>
</body>
</html>
alert("Я JavaScript!");
К условию
Переменные
Работа с переменными
В коде ниже каждая строка решения соответствует одному элементу в списке задач.
name = "Джон";
admin = name;
К условию
Например:
Обратите внимание, мы могли бы использовать короткое имя planet , но тогда будет непонятно, о какой
планете мы говорим. Лучше описать содержимое переменной подробнее, по крайней мере, до тех пор, пока
имя переменной неСтанетСлишкомДлинным.
520/597
Имя текущего посетителя:
Опять же, мы могли бы укоротить название до userName , если мы точно знаем, что это текущий
пользователь.
Современные редакторы и автодополнение ввода в них позволяют легко писать длинные названия
переменных. Не экономьте буквы. Имена, состоящие из трёх слов, вполне нормальны.
К условию
Обычно мы используем буквы в верхнем регистре для констант, которые «жёстко закодированы». Или,
другими словами, когда значение известно до выполнения скрипта и записывается непосредственно в код.
В нашем примере, birthday именно такая переменная. Поэтому мы можем использовать заглавные буквы.
В отличие от предыдущей, переменная age вычисляется во время выполнения скрипта. Сегодня у нас один
возраст, а через год уже совсем другой. Она является константой, потому что не изменяется при выполнении
кода. Но она является «менее константной», чем birthday : она вычисляется, поэтому мы должны сохранить
её в нижнем регистре.
К условию
Типы данных
Шаблонные строки
// выражение - число 1
alert( `hello ${1}` ); // hello 1
К условию
Простая страница
JavaScript-код:
Вся страница:
521/597
<!DOCTYPE html>
<html>
<body>
<script>
'use strict';
</body>
</html>
К условию
Ответ:
● a = 2
● b = 2
● c = 2
● d = 1
let a = 1, b = 1;
К условию
Результат присваивания
Ответ:
● a = 4 (умножено на 2)
● x = 5 (вычислено как 1 + 4)
К условию
Преобразование типов
522/597
undefined + 1 = NaN // (6)
" \t \n" - 2 = -2 // (7)
1. Сложение со строкой "" + 1 преобразует 1 к строке: "" + 1 = "1" , и в следующем случае "1" +
0 работает то же самое правило.
2. Вычитание - (как и большинство математических операторов) работает только с числами, пустая строка
"" приводится к 0 .
3. Сложение со строкой превращает число 5 в строку и добавляет к строке.
4. Вычитание всегда преобразует к числу, значит строка " -9 " становится числом -9 (пробелы по краям
обрезаются).
5. null становится 0 после численного преобразования.
6. undefined становится NaN после численного преобразования.
7. Пробельные символы, такие как \t и \n , по краям строки игнорируются при преобразовании в число,
так что строка " \t \n" , аналогично пустой строке, становится 0 после численного преобразования.
К условию
Исправьте сложение
Причина в том, что окно запроса возвращает пользовательский ввод как строку.
alert(a + b); // 12
Нам нужно привести строки к числам перед применением оператора + . Например, с помощью Number() или
вставки + перед ними.
alert(a + b); // 3
alert(+a + +b); // 3
К условию
Операторы сравнения
Операторы сравнения
5 > 4 → true
"ананас" > "яблоко" → false
"2" > "12" → true
undefined == null → true
undefined === null → false
523/597
null == "\n0\n" → false
null === +"\n0\n" → false
Разъяснения:
1. Очевидно, true .
2. Используется посимвольное сравнение, поэтому false . "а" меньше, чем "я" .
3. Снова посимвольное сравнение. Первый символ первой строки "2" больше, чем первый символ второй
"1" .
4. Специальный случай. Значения null и undefined равны только друг другу при нестрогом сравнении.
5. Строгое сравнение разных типов, поэтому false .
6. Аналогично (4) , null равен только undefined .
7. Строгое сравнение разных типов.
К условию
if (строка с нулём)
Да, выведется.
Любая строка, кроме пустой (а строка "0" – не пустая), в логическом контексте становится true .
if ("0") {
alert( 'Привет' );
}
К условию
Название JavaScript
<!DOCTYPE html>
<html>
<body>
<script>
'use strict';
if (value == 'ECMAScript') {
alert('Верно!');
} else {
alert('Не знаете? ECMAScript!');
}
</script>
</body>
</html>
К условию
524/597
let value = prompt('Введите число', 0);
if (value > 0) {
alert( 1 );
} else if (value < 0) {
alert( -1 );
} else {
alert( 0 );
}
К условию
К условию
К условию
Логические операторы
К условию
Второй оператор || не будет выполнен, выполнение до alert(3) не дойдёт, поэтому 3 выведено не будет.
К условию
525/597
Ответ: null , потому что это первое «ложное» значение из списка.
К условию
К условию
Ответ: 3 .
null || 3 || 4
К условию
Ответ: 30 .
alert(value);
1. value &&= 10
● value=NaN
● NaN конвертируется в логическое значение false
● value ложно, поэтому присваивание не срабатывает
2. value ||= 20
● value=NaN
● NaN конвертируется в логическое значение false
●
value ложно, поэтому присваивание срабатывает
3. value &&= 30
526/597
● value=20
● 20 конвертируется в логическое значение true
●
value истинно, поэтому присваивание срабатывает
4. value ||= 40
● value=30
● 30 конвертируется в логическое значение true
● value истинно, поэтому присваивание не срабатывает
К условию
К условию
Первый вариант:
Второй вариант:
К условию
Вопрос об "if"
Подробности:
// Выполнится.
// Результат -1 || 0 = -1, в логическом контексте true
if (-1 || 0) alert( 'first' );
// Не выполнится
// -1 && 0 = 0, в логическом контексте false
if (-1 && 0) alert( 'second' );
// Выполнится
// оператор && имеет больший приоритет, чем ||
// так что -1 && 1 выполнится раньше
// вычисления: null || -1 && 1 -> null || 1 -> 1
if (null || -1 && 1) alert( 'third' );
К условию
Проверка логина
527/597
alert( 'Здравствуйте!' );
} else if (pass === '' || pass === null) {
alert( 'Отменено' );
} else {
alert( 'Неверный пароль' );
}
Обратите внимание на вертикальные отступы внутри блоков if . Они технически не требуются, но делают код
более читаемым.
К условию
К условию
Ответ: "Берлин" .
Первое присваивание city ??= "Берлин" срабатывает, поскольку изначально city — это null . После
присваивания все остальные действия с оператором ??= становятся бессмысленными, так как теперь city
содержит «определённое» значение.
alert(city);
К условию
К условию
528/597
Циклы while и for
Ответ: 1 .
let i = 3;
while (i) {
alert( i-- );
}
let i = 3;
К условию
1.
От 1 до 4
let i = 0;
while (++i < 5) alert( i );
Первое значение: i = 1 , так как операция ++i сначала увеличит i , а потом уже произойдёт сравнение и
выполнение alert .
Далее 2, 3, 4… Значения выводятся одно за другим. Для каждого значения сначала происходит
увеличение, а потом – сравнение, так как ++ стоит перед переменной.
При i = 4 произойдёт увеличение i до 5 , а потом сравнение while (5 < 5) – это неверно. Поэтому
на этом цикл остановится, и значение 5 выведено не будет.
2.
От 1 до 5
let i = 0;
while (i++ < 5) alert( i );
Но последующий вызов alert уже не относится к этому выражению, так что получит новый i = 1 .
Далее следуют 2, 3, 4… .
529/597
Остановимся на i = 4 . Префиксная форма ++i увеличила бы i и использовала бы в сравнении 5 . Но
здесь мы имеем постфиксную форму i++ , поэтому она увеличивает i до 5 , но возвращает старое
значение. Таким образом, сравнение фактически равно while (4 < 5) – true , поэтому срабатывает
alert .
К условию
Увеличение i++ выполняется отдельно от проверки условия (2) , значение i при этом не используется,
поэтому нет никакой разницы между i++ и ++i .
К условию
К условию
let i = 0;
while (i < 3) {
alert( `number ${i}!` );
i++;
}
К условию
let num;
do {
num = prompt("Введите число больше 100?", 0);
} while (num <= 100 && num);
530/597
1. Проверка num <= 100 – то есть, введённое число всё ещё меньше 100 .
2. Проверка && num вычисляется в false , когда num имеет значение null или пустая строка '' . В
этом случае цикл while тоже нужно прекратить.
Кстати, сравнение num <= 100 при вводе null даст true , так что вторая проверка необходима.
К условию
Для всех i от 1 до 10 {
проверить, делится ли число i на какое-либо из чисел до него
если делится, то это i не подходит, берём следующее
если не делится, то i - простое число
}
let n = 10;
nextPrime:
for (let i = 2; i <= n; i++) { // Для всех i...
Конечно же, его можно оптимизировать с точки зрения производительности. Например, проверять все j не от
2 до i , а от 2 до квадратного корня из i . А для очень больших чисел – существуют более эффективные
специализированные алгоритмы проверки простоты числа, например квадратичное решето и решето
числового поля .
К условию
Конструкция "switch"
Если совсем точно следовать работе switch , то if должен выполнять строгое сравнение '===' .
if(browser == 'Edge') {
alert("You've got the Edge!");
} else if (browser == 'Chrome'
|| browser == 'Firefox'
|| browser == 'Safari'
|| browser == 'Opera') {
alert( 'Okay we support these browsers too' );
} else {
alert( 'We hope that this page looks ok!' );
}
531/597
Но всё равно запись через switch нагляднее.
К условию
switch (number) {
case 0:
alert('Вы ввели число 0');
break;
case 1:
alert('Вы ввели число 1');
break;
case 2:
case 3:
alert('Вы ввели число 2, а может и 3');
break;
}
Допустим, он не стоит. Есть шанс, что в будущем нам понадобится добавить в конец ещё один case ,
например case 4 , и мы, вполне вероятно, забудем этот break поставить. В результате выполнение case
2/case 3 продолжится на case 4 и будет ошибка.
К условию
Функции
Обязателен ли "else"?
К условию
Используя оператор ? :
function checkAge(age) {
return (age > 18) ? true : confirm('Родители разрешили?');
}
function checkAge(age) {
return (age > 18) || confirm('Родители разрешили?');
}
Обратите внимание, что круглые скобки вокруг age > 18 не обязательны. Они здесь для лучшей читаемости
кода.
К условию
532/597
Функция min(a, b)
function min(a, b) {
if (a < b) {
return a;
} else {
return b;
}
}
function min(a, b) {
return a < b ? a : b;
}
К условию
Функция pow(x,n)
function pow(x, n) {
let result = x;
return result;
}
if (n >= 1 && n % 1 == 0) {
alert( pow(x, n) );
} else {
alert(`Степень ${n} не поддерживается, используйте натуральное число`);
}
К условию
ask(
"Вы согласны?",
() => alert("Вы согласились."),
() => alert("Вы отменили выполнение.")
);
533/597
К условию
Плохой стиль
Исправленный вариант:
function pow(x, n) {
let result = 1;
return result;
}
if (n <= 0) {
alert(`Степень ${n} не поддерживается,
введите целую степень, большую 0`);
} else {
alert( pow(x, n) );
}
К условию
У нас тут, по сути, три теста, но они написаны как одна функция с тремя проверками.
Иногда так проще писать, но если произойдёт ошибка, то гораздо сложнее понять, что пошло не так.
Если ошибка происходит посередине сложного потока выполнения, то нам придётся выяснять, какие данные
были в этом месте. По сути, придётся отлаживать тест.
534/597
Гораздо лучше разбить тест на несколько блоков it и ясно описать входные и ожидаемые на выходе данные.
Примерно так:
Мы заменили один it на describe и группу блоков it . Теперь, если какой-либо из блоков завершится
неудачно, мы точно увидим, с какими данными это произошло.
Также мы можем изолировать один тест и запускать только его, написав [Link] вместо it :
К условию
Объекты
Привет, object
К условию
Проверка на пустоту
Просто в цикле перебираем свойства объекта и возвращаем false , как только встречаем свойство.
function isEmpty(obj) {
for (let key in obj) {
// если тело цикла начнет выполняться - значит в объекте есть свойства
return false;
}
return true;
}
535/597
Открыть решение с тестами в песочнице.
К условию
Объекты-константы?
Другими словами, user хранит ссылку на объект. И это не может быть изменено. Но содержимое объекта
менять можно.
const user = {
name: "John"
};
// Работает!
[Link] = "Pete";
// Ошибка
user = 123;
К условию
let salaries = {
John: 100,
Ann: 160,
Pete: 130
};
let sum = 0;
for (let key in salaries) {
sum += salaries[key];
}
alert(sum); // 390
К условию
function multiplyNumeric(obj) {
for (let key in obj) {
if (typeof obj[key] == 'number') {
obj[key] *= 2;
}
}
}
К условию
Ответ: ошибка.
536/597
Проверьте:
function makeUser() {
return {
name: "John",
ref: this
};
}
Это потому, что правила, которые определяют значение this , никак не смотрят на объявление объекта.
Важен лишь момент вызова.
Здесь значение this внутри makeUser() равно undefined , потому что оно вызывается как функция, а не
через «точечный» синтаксис как метод.
Значение this одно для всей функции, блоки кода и объектные литералы на него не влияют.
Таким образом, ref: this фактически принимает текущее this функции makeUser() .
function makeUser(){
return this; // на этот раз нет литерала объекта
}
function makeUser() {
return {
name: "John",
ref() {
return this;
}
};
}
Теперь это работает, поскольку [Link]() – это метод. И значением this становится объект перед точкой
..
К условию
Создайте калькулятор
let calculator = {
sum() {
return this.a + this.b;
},
mul() {
return this.a * this.b;
},
read() {
this.a = +prompt('a?', 0);
537/597
this.b = +prompt('b?', 0);
}
};
[Link]();
alert( [Link]() );
alert( [Link]() );
К условию
Цепь вызовов
let ladder = {
step: 0,
up() {
[Link]++;
return this;
},
down() {
[Link]--;
return this;
},
showStep() {
alert( [Link] );
return this;
}
};
Мы также можем записать один вызов на одной строке. Для длинных цепей вызовов это более читабельно:
ladder
.up()
.up()
.down()
.showStep() // 1
.down()
.showStep(); // 0
К условию
Да, возможно.
Таким образом, они могут, к примеру, возвращать один и тот же внешне определённый объект obj :
538/597
К условию
function Calculator() {
[Link] = function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
};
[Link] = function() {
return this.a + this.b;
};
[Link] = function() {
return this.a * this.b;
};
}
К условию
function Accumulator(startingValue) {
[Link] = startingValue;
[Link] = function() {
[Link] += +prompt('Сколько нужно добавить?', 0);
};
К условию
Методы примитивов
[Link] = 5; // (*)
alert([Link]);
В зависимости от того, используете ли вы строгий режим ( use strict ) или нет, результат может быть:
539/597
1. undefined (без strict)
2. Ошибка (strict mode)
К условию
Числа
alert( a + b );
Обратите внимание, что мы использовали унарный оператор + перед prompt , он преобразует значение в
числовой формат.
В противном случае, a и b будут строками, и после суммирования произойдёт конкатенация двух строк, а
именно: "1" + "2" = "12" .
К условию
Во внутреннем двоичном представлении 6.35 является бесконечной двоичной дробью. Хранится она с потерей
точности…
Давайте посмотрим:
Потеря точности может как увеличивать, так и уменьшать число. В данном случае число становится чуть
меньше, поэтому оно округляется в меньшую сторону.
Тут потеря точности приводит к увеличению числа, поэтому округление произойдёт в большую сторону.
Обратите внимание, что для числа 63.5 не происходит потери точности. Дело в том, что десятичная часть
0.5 на самом деле 1/2 . Дробные числа, делённые на степень 2 , точно представлены в двоичной системе,
540/597
теперь мы можем округлить число:
alert( [Link](6.35 * 10) / 10 ); // 6.35 -> 63.5 -> 64(rounded) -> 6.4
К условию
function readNumber() {
let num;
do {
num = prompt("Введите число", 0);
} while ( !isFinite(num) );
return +num;
}
alert(`Число: ${readNumber()}`);
Решение немного сложнее, чем могло бы быть, потому что нам надо обрабатывать null и пустую строку.
Следовательно, запрашиваем ввод числового значения, пока посетитель его не введёт. И null (отмена) и
пустая строка также соответствуют данному условию, потому что при приведении к числу они равны 0 .
После того, как цикл завершится, нам нужно проверить введённое значение на null и пустую строку (вернуть
null ), потому что после преобразования null в число, функция вернёт 0 .
К условию
let i = 0;
while (i < 11) {
i += 0.2;
if (i > 9.8 && i < 10.2) alert( i );
}
Это происходит из-за потери точности, при прибавлении таких дробей как 0.2 .
К условию
Нам нужно преобразовать каждое значение из интервала 0…1 в значения от min до max .
1. Если мы умножим случайное число от 0…1 на max-min , тогда интервал возможных значений от 0..1
увеличивается до 0..max-min .
2. И, если мы прибавим min , то интервал станет от min до max .
541/597
Функция:
alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
К условию
Самое простое, но неправильное решение – генерировать случайное число от min до max и округлять его:
alert( randomInteger(1, 3) );
Функция будет работать, но неправильно. Вероятность получить min и max значения в 2 раза меньше, чем
любое другое число.
Если вы запустите приведённый выше пример, то заметите, что 2 появляется чаще всего.
Это происходит потому, что метод [Link]() получает случайные числа из интервала 1..3 и округляет
их следующим образом:
Теперь становится понятно, что 1 получает в 2 раза меньше значений, чем 2 . То же самое с 3 .
Есть много правильных решений этой задачи. Одно из них – использовать [Link] для получения
случайного числа от min до max+1 :
alert( randomInteger(1, 3) );
Все интервалы имеют одинаковую длину, что выравнивает вероятность получения случайных чисел.
К условию
542/597
Строки
Мы не можем просто заменить первый символ, так как строки в JavaScript неизменяемы.
Однако есть небольшая проблемка. Если строка пуста, str[0] вернёт undefined , а у undefined нет
метода toUpperCase() , поэтому мы получим ошибку.
Выхода два:
1. Использовать [Link](0) , поскольку этот метод всегда возвращает строку (для пустой строки —
пустую).
2. Добавить проверку на пустую строку.
function ucFirst(str) {
if (!str) return str;
К условию
Проверка на спам
Для поиска без учёта регистра символов переведём всю строку в нижний регистр, а потом проверим, есть ли в
ней искомые подстроки:
function checkSpam(str) {
let lowerStr = [Link]();
К условию
Усечение строки
Строка, которую мы возвращаем, должна быть не длиннее maxlength , поэтому, если мы обрезаем строку, то
мы должны убрать на один символ больше, чем maxlength — чтобы хватило места на многоточие.
Имейте в виду, что в качестве многоточия здесь используется … — ровно один специальный Юникодный
символ. Это не то же самое, что ... — три точки.
543/597
[Link](0, maxlength - 1) + '…' : str;
}
К условию
Выделить число
function extractCurrencyValue(str) {
return +[Link](1);
}
К условию
Массивы
Скопирован ли массив?
Выведется 4 :
[Link]("Банан");
alert( [Link] ); // 4
Потому, что массивы – это объекты. Обе переменные shoppingCart и fruits являются ссылками на один
и тот же массив.
К условию
Операции с массивами
К условию
Вызов arr[2]() синтаксически – старый добрый obj[method]() , в роли obj – arr , а в роли method –
2.
Итак, у нас есть вызов функции arr[2] как метода объекта. Соответственно, он получает в качестве this
объект arr и выводит массив:
[Link](function() {
alert( this );
})
544/597
arr[2](); // a,b,function(){...}
К условию
Обратите внимание на малозаметную, но важную деталь решения. Мы не преобразуем value в число сразу
после prompt , потому что после value = +value мы не сможем отличить пустую строку (конец записи) от
«0» (разрешённое число). Мы сделаем это позже.
function sumInput() {
while (true) {
// Прекращаем ввод?
if (value === "" || value === null || !isFinite(value)) break;
[Link](+value);
}
let sum = 0;
for (let number of numbers) {
sum += number;
}
return sum;
}
alert( sumInput() );
К условию
Медленное решение
Самый простой путь – посчитать суммы подмассивов, начиная с каждого элемента по очереди.
// Начиная с -1:
-1
-1 + 2
-1 + 2 + 3
-1 + 2 + 3 + (-9)
-1 + 2 + 3 + (-9) + 11
// Начиная с 2:
2
2 + 3
2 + 3 + (-9)
2 + 3 + (-9) + 11
// Начиная с 3:
3
3 + (-9)
3 + (-9) + 11
// Начиная с -9
-9
-9 + 11
545/597
// Начиная с 11
11
Реализуется с помощью вложенного цикла: внешний цикл проходит по элементам массива, а внутренний
считает подсумму, начиная с текущего элемента.
function getMaxSubSum(arr) {
let maxSum = 0; // если элементов не будет - возвращаем 0
return maxSum;
}
Это решение имеет оценку сложности O(n2). Другими словами, если мы увеличим размер массива в 2 раза,
время выполнения алгоритма увеличится в 4 раза.
Для больших массивов(1000, 10000 или больше элементов) такие алгоритмы могут приводить к серьёзным
«тормозам».
Быстрое решение
Идём по массиву и накапливаем текущую частичную сумму элементов в переменной s . Если s в какой-то
момент становится отрицательной – присваиваем s=0 . Максимальный из всех s и будет ответом.
function getMaxSubSum(arr) {
let maxSum = 0;
let partialSum = 0;
return maxSum;
}
Этот алгоритм требует ровно 1 проход по массиву и его оценка сложности O(n).
Больше информации об алгоритме тут: Задача поиска максимальной суммы подмассива . Если всё ещё не
очевидно как это работает, просмотрите алгоритм в примерах выше, это будет лучше всяких слов.
К условию
546/597
Методы массивов
function camelize(str) {
return str
.split('-') // разбивает 'my-long-word' на массив ['my', 'long', 'word']
.map(
// Переводит в верхний регистр первые буквы всех элементом массива за исключением первого
// превращает ['my', 'long', 'word'] в ['my', 'Long', 'Word']
(word, index) => index == 0 ? word : word[0].toUpperCase() + [Link](1)
)
.join(''); // соединяет ['my', 'Long', 'Word'] в 'myLongWord'
}
К условию
Фильтрация по диапазону
function filterRange(arr, a, b) {
// добавлены скобки вокруг выражения для улучшения читабельности
return [Link](item => (a <= item && item <= b));
}
К условию
function filterRangeInPlace(arr, a, b) {
К условию
547/597
let arr = [5, 2, 1, -10, 8];
alert( arr );
К условию
function copySorted(arr) {
return [Link]().sort();
}
alert( sorted );
alert( arr );
К условию
● Обратите внимание, как хранятся методы. Они просто добавляются к внутреннему объекту.
● Все тесты и числовые преобразования выполняются в методе calculate . В будущем он может быть
расширен для поддержки более сложных выражений.
function Calculator() {
[Link] = {
"-": (a, b) => a - b,
"+": (a, b) => a + b
};
[Link] = function(str) {
К условию
548/597
let masha = { name: "Маша", age: 28 };
К условию
Трансформировать в объекты
/*
usersMapped = [
{ fullName: "Вася Пупкин", id: 1 },
{ fullName: "Петя Иванов", id: 2 },
{ fullName: "Маша Петрова", id: 3 }
]
*/
alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // Вася Пупкин
Обратите внимание, что для стрелочных функций мы должны использовать дополнительные скобки.
Как мы помним, есть две функции со стрелками: без тела value => expr и с телом value => {...} .
Здесь JavaScript будет трактовать { как начало тела функции, а не начало объекта. Чтобы обойти это, нужно
заключить их в «нормальные» скобки:
К условию
function sortByAge(arr) {
[Link]((a, b) => [Link] - [Link]);
}
549/597
let arr = [ vasya, petya, masha ];
sortByAge(arr);
К условию
Перемешайте массив
function shuffle(array) {
[Link](() => [Link]() - 0.5);
}
Это, конечно, будет работать, потому что [Link]() - 0.5 отдаёт случайное число, которое может
быть положительным или отрицательным, следовательно, функция сортировки меняет порядок элементов
случайным образом.
Но поскольку метод sort не предназначен для использования в таких случаях, не все возможные варианты
имеют одинаковую вероятность.
Например, рассмотрим код ниже. Он запускает shuffle 1000000 раз и считает вероятность появления для
всех возможных вариантов arr :
function shuffle(array) {
[Link](() => [Link]() - 0.5);
}
123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223
550/597
Теперь мы отчётливо видим допущенное отклонение: 123 и 213 появляются намного чаще, чем остальные
варианты.
Результаты этого кода могут варьироваться при запуске на разных движках JavaScript, но очевидно, что такой
подход не надёжен.
Так почему это не работает? Если говорить простыми словами, то sort это «чёрный ящик»: мы бросаем в
него массив и функцию сравнения, ожидая получить отсортированный массив. Но из-за абсолютной
хаотичности сравнений чёрный ящик сходит с ума, и как именно он сходит с ума, зависит от конкретной его
реализации, которая различна в разных движках JavaScript.
Есть и другие хорошие способы решить эту задачу. Например, есть отличный алгоритм под названием
Тасование Фишера — Йетса . Суть заключается в том, чтобы проходить по массиву в обратном порядке и
менять местами каждый элемент со случайным элементом, который находится перед ним.
function shuffle(array) {
for (let i = [Link] - 1; i > 0; i--) {
let j = [Link]([Link]() * (i + 1)); // случайный индекс от 0 до i
function shuffle(array) {
for (let i = [Link] - 1; i > 0; i--) {
let j = [Link]([Link]() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
Пример вывода:
123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316
551/597
Кроме того, если посмотреть с точки зрения производительности, то алгоритм «Тасование Фишера — Йетса»
намного быстрее, так как в нём нет лишних затрат на сортировку.
К условию
function getAverageAge(users) {
return [Link]((prev, user) => prev + [Link], 0) / [Link];
}
alert( getAverageAge(arr) ); // 28
К условию
function unique(arr) {
let result = [];
return result;
}
Метод [Link](str) внутри себя обходит массив result и сравнивает каждый элемент с str ,
чтобы найти совпадение.
Таким образом, если result содержит 100 элементов и ни один не совпадает со str , тогда он обойдёт
весь result и сделает ровно 100 сравнений. А если result большой, например, 10000 , то будет
произведено 10000 сравнений.
Само по себе это не проблема, потому что движки JavaScript очень быстрые, поэтому обход 10000 элементов
массива занимает считанные микросекунды.
Поэтому, если [Link] равен 10000 , у нас будет что-то вроде 10000*10000 = 100 миллионов
сравнений. Это многовато.
552/597
Открыть решение с тестами в песочнице.
К условию
function groupById(array) {
return [Link]((obj, value) => {
obj[[Link]] = value;
return obj;
}, {})
}
К условию
Map и Set
function unique(arr) {
return [Link](new Set(arr));
}
К условию
Отфильтруйте анаграммы
Чтобы найти все анаграммы, давайте разобьём каждое слово на буквы и отсортируем их, а потом объединим
получившийся массив снова в строку. После этого все анаграммы будут одинаковы.
Например:
Мы будем использовать отсортированные строки как ключи в коллекции Map, для того чтобы сопоставить
каждому ключу только одно значение:
function aclean(arr) {
let map = new Map();
return [Link]([Link]());
}
alert( aclean(arr) );
553/597
Для удобства, давайте разделим это на несколько строк:
Два разных слова 'PAN' и 'nap' принимают ту же самую форму после сортировки букв – 'anp' .
[Link](sorted, word);
Если мы когда-либо ещё встретим слово в той же отсортированной форме, тогда это слово перезапишет
значение с тем же ключом в объекте. Таким образом, нескольким словам у нас будет всегда соответствовать
одна отсортированная форма.
Также в этом случае вместо Map мы можем использовать простой объект, потому что ключи являются
строками.
function aclean(arr) {
let obj = {};
return [Link](obj);
}
alert( aclean(arr) );
К условию
Перебираемые ключи
[Link]("name", "John");
[Link]("more");
К условию
554/597
WeakMap и WeakSet
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
[Link]();
// теперь readMessages содержит 1 элемент (хотя технически память может быть очищена позже)
WeakSet позволяет хранить набор сообщений и легко проверять наличие сообщения в нём.
Он очищается автоматически. Минус в том, что мы не можем перебрать его содержимое, не можем получить
«все прочитанные сообщения» напрямую. Но мы можем сделать это, перебирая все сообщения и фильтруя те,
которые находятся в WeakSet .
Например:
Хотя символы и позволяют уменьшить вероятность проблем, использование здесь WeakSet лучше с
архитектурной точки зрения.
К условию
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
555/597
К условию
function sumSalaries(salaries) {
let sum = 0;
for (let salary of [Link](salaries)) {
sum += salary;
}
let salaries = {
"John": 100,
"Pete": 300,
"Mary": 250
};
Или, как вариант, мы можем получить сумму, используя методы [Link] и reduce :
К условию
function count(obj) {
return [Link](obj).length;
}
К условию
Деструктурирующее присваивание
Деструктурирующее присваивание
let user = {
name: "John",
years: 30
};
556/597
alert( age ); // 30
alert( isAdmin ); // false
К условию
Максимальная зарплата
function topSalary(salaries) {
let max = 0;
let maxName = null;
return maxName;
}
К условию
Дата и время
Создайте дату
Конструктор new Date стандартно использует местную временную зону. Единственная важная вещь, которую
нужно запомнить – это то, что месяцы начинаются с нуля.
К условию
Создадим массив дней недели, чтобы получить имя нужного дня по его номеру:
function getWeekDay(date) {
let days = ['ВС', 'ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ'];
return days[[Link]()];
}
К условию
557/597
function getLocalDay(date) {
return day;
}
К условию
…Но функция не должна изменять объект date . Это очень важно, поскольку внешний код, передающий нам
объект, не ожидает его изменения.
[Link]([Link]() - days);
return [Link]();
}
К условию
alert( getLastDayOfMonth(2012, 0) ); // 31
alert( getLastDayOfMonth(2012, 1) ); // 29
alert( getLastDayOfMonth(2013, 1) ); // 28
Обычно даты начинаются с 1, но технически возможно передать любое число, и дата сама себя поправит. Так
что если передать 0, то это значение будет соответствовать «один день перед первым числом месяца»,
другими словами: «последнее число прошлого месяца».
558/597
К условию
Чтобы получить количество секунд, нужно сгенерировать объект date на самое начало текущего дня –
[Link], а затем вычесть полученное значение из «сейчас».
Разность даст нам количество миллисекунд с начала дня, делим его на 1000 и получаем секунды:
function getSecondsToday() {
let now = new Date();
alert( getSecondsToday() );
function getSecondsToday() {
let d = new Date();
return [Link]() * 3600 + [Link]() * 60 + [Link]();
}
К условию
Чтобы получить количество миллисекунд до завтра, можно из «завтра [Link]» вычесть текущую дату.
function getSecondsToTomorrow() {
let now = new Date();
// завтрашняя дата
let tomorrow = new Date([Link](), [Link](), [Link]()+1);
Альтернативное решение:
function getSecondsToTomorrow() {
let now = new Date();
let hour = [Link]();
let minutes = [Link]();
let seconds = [Link]();
let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds;
let totalSecondsInADay = 86400;
Учтите, что многие страны переходят с зимнего времени на летнее и обратно, так что могут быть дни
длительностью в 23 или 25 часов. Такие дни, если это важно, можно обрабатывать отдельно.
К условию
559/597
Чтобы получить время с date по текущий момент, нужно вычесть даты.
function formatDate(date) {
let diff = new Date() - date; // разница в миллисекундах
// отформатировать дату
// добавить ведущие нули к единственной цифре дню/месяцу/часам/минутам
let d = date;
d = [
'0' + [Link](),
'0' + ([Link]() + 1),
'' + [Link](),
'0' + [Link](),
'0' + [Link]()
].map(component => [Link](-2)); // взять последние 2 цифры из каждой компоненты
Альтернативное решение:
function formatDate(date) {
let dayOfMonth = [Link]();
let month = [Link]() + 1;
let year = [Link]();
let hour = [Link]();
let minutes = [Link]();
let diffMs = new Date() - date;
let diffSec = [Link](diffMs / 1000);
let diffMin = diffSec / 60;
let diffHour = diffMin / 60;
// форматирование
year = [Link]().slice(-2);
month = month < 10 ? '0' + month : month;
dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;
hour = hour < 10 ? '0' + hour : hour;
minutes = minutes < 10 ? '0' + minutes : minutes;
if (diffSec < 1) {
return 'прямо сейчас';
} else if (diffMin < 1) {
return `${diffSec} сек. назад`
} else if (diffHour < 1) {
return `${diffMin} мин. назад`
} else {
return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}`
}
}
560/597
Открыть решение с тестами в песочнице.
К условию
let user = {
name: "Василий Иванович",
age: 35
};
К условию
let room = {
number: 23
};
let meetup = {
title: "Совещание",
occupiedBy: [{name: "Иванов"}, {name: "Петров"}],
place: room
};
[Link] = meetup;
[Link] = meetup;
/*
{
"title":"Совещание",
"occupiedBy":[{"name":"Иванов"},{"name":"Петров"}],
"place":{"number":23}
}
*/
Функция replacer будет вызвана для каждой пары (key, value) , и в первом вызове будет передан
специальный «объект-обёртка»: {"": meetup} .
Если мы реализуем только проверку value == meetup , то в результате получим undefined . Чтобы в
первом вызове replacer не было удалено свойство, ссылающееся на meetup , нам также нужно добавить
проверку key != "" .
К условию
Рекурсия и стек
function sumTo(n) {
let sum = 0;
561/597
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
alert( sumTo(100) );
function sumTo(n) {
if (n == 1) return 1;
return n + sumTo(n - 1);
}
alert( sumTo(100) );
function sumTo(n) {
return n * (n + 1) / 2;
}
alert( sumTo(100) );
P.S. Надо ли говорить, что решение по формуле работает быстрее всех? Это очевидно. Оно использует всего
три операции для любого n, а цикл и рекурсия требуют как минимум n операций сложения.
Вариант с циклом – второй по скорости. Он быстрее рекурсии, так как операций сложения столько же, но нет
дополнительных вычислительных затрат на организацию вложенных вызовов. Поэтому рекурсия в данном
случае работает медленнее всех.
P.P.S. Некоторые движки поддерживают оптимизацию «хвостового вызова»: если рекурсивный вызов является
самым последним в функции, без каких-либо других вычислений, то внешней функции не нужно будет
возобновлять выполнение и не нужно запоминать контекст его выполнения. В итоге требования к памяти
снижаются. Но если JavaScript-движок не поддерживает это (большинство не поддерживают), будет ошибка:
максимальный размер стека превышен, так как обычно существует ограничение на максимальный размер
стека.
К условию
Вычислить факториал
function factorial(n) {
return (n != 1) ? n * factorial(n - 1) : 1;
}
Базисом рекурсии является значение 1 . А можно было бы сделать базисом и 0 , однако это добавило
рекурсии дополнительный шаг:
function factorial(n) {
return n ? n * factorial(n - 1) : 1;
}
К условию
562/597
Числа Фибоначчи
function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
alert( fib(3) ); // 2
alert( fib(7) ); // 13
// fib(77); // вычисляется очень долго
При больших значениях n такое решение будет работать очень долго. Например, fib(77) может повесить
браузер на некоторое время, съев все ресурсы процессора.
Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений
вычисляется много раз снова и снова.
...
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
...
Здесь видно, что значение fib(3) нужно одновременно и для fib(5) и для fib(4) . В коде оно будет
вычислено два раза, совершенно независимо.
fib ( 5 )
fib(4) fib(3)
fib(1) fib(0)
Можно заметить, что fib(3) вычисляется дважды, а fib(2) – трижды. Общее количество вычислений
растёт намного быстрее, чем n , что делает его огромным даже для n=77 .
Можно это оптимизировать, запоминая уже вычисленные значения: если значение, скажем, fib(3)
вычислено однажды, затем мы просто переиспользуем это значение для последующих вычислений.
Другим вариантом было бы отказаться от рекурсии и использовать совершенно другой алгоритм на основе
цикла.
Вместо того, чтобы начинать с n и вычислять необходимые предыдущие значения, можно написать цикл,
который начнёт с 1 и 2 , затем из них получит fib(3) как их сумму, затем fib(4) как сумму предыдущих
значений, затем fib(5) и так далее, до финального результата. На каждом шаге нам нужно помнить только
значения двух предыдущих чисел последовательности.
Начало:
563/597
let c = a + b;
Переставим переменные: a,b , присвоим значения fib(2),fib(3) , тогда c можно получить как их сумму:
a = b; // теперь a = fib(2)
b = c; // теперь b = fib(3)
c = a + b; // c = fib(4)
/* имеем последовательность:
a b c
1, 1, 2, 3
*/
a = b; // now a = fib(3)
b = c; // now b = fib(4)
c = a + b; // c = fib(5)
…И так далее, пока не получим искомое значение. Это намного быстрее рекурсии и не требует повторных
вычислений.
Полный код:
function fib(n) {
let a = 1;
let b = 1;
for (let i = 3; i <= n; i++) {
let c = a + b;
a = b;
b = c;
}
return b;
}
alert( fib(3) ); // 2
alert( fib(7) ); // 13
alert( fib(77) ); // 5527939700884757
Цикл начинается с i=3 , потому что первое и второе значения последовательности заданы a=1 , b=1 .
К условию
let list = {
value: 1,
next: {
value: 2,
next: {
564/597
value: 3,
next: {
value: 4,
next: null
}
}
}
};
function printList(list) {
let tmp = list;
while (tmp) {
alert([Link]);
tmp = [Link];
}
printList(list);
Обратите внимание, что мы используем временную переменную tmp для перемещения по списку.
Технически, мы могли бы использовать параметр функции list вместо неё:
function printList(list) {
while(list) {
alert([Link]);
list = [Link];
}
…Но это было бы неблагоразумно. В будущем нам может понадобиться расширить функцию, сделать что-
нибудь ещё со списком. Если мы меняем list , то теряем такую возможность.
Говоря о хороших именах для переменных, list здесь – это сам список, его первый элемент. Так и должно
быть, это просто и понятно.
С другой стороны, tmp используется исключительно для обхода списка, как i в цикле for .
Рекурсивный вариант printList(list) следует простой логике: для вывода списка мы должны вывести
текущий list , затем сделать то же самое для [Link] :
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
function printList(list) {
if ([Link]) {
printList([Link]); // делаем то же самое для остальной части списка
}
printList(list);
565/597
Какой способ лучше?
Технически, способ с циклом более эффективный. В обеих реализациях делается то же самое, но для цикла
не тратятся ресурсы для вложенных вызовов.
С другой стороны, рекурсивный вариант более короткий и, возможно, более простой для понимания.
К условию
С использованием рекурсии
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
function printReverseList(list) {
if ([Link]) {
printReverseList([Link]);
}
alert([Link]);
}
printReverseList(list);
С использованием цикла
Нет способа сразу получить последнее значение в списке list . Мы также не можем «вернуться назад», к
предыдущему элементу списка.
Поэтому мы можем сначала перебрать элементы в прямом порядке и запомнить их в массиве, а затем
вывести то, что мы запомнили, в обратном порядке:
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
function printReverseList(list) {
let arr = [];
let tmp = list;
566/597
while (tmp) {
[Link]([Link]);
tmp = [Link];
}
printReverseList(list);
Обратите внимание, что рекурсивное решение на самом деле делает то же самое: проходит список,
запоминает элементы в цепочке вложенных вызовов (в контексте выполнения), а затем выводит их.
К условию
Ответ: Pete.
Функция получает внешние переменные в том виде, в котором они находятся сейчас, она использует самые
последние значения.
Старые значения переменных нигде не сохраняются. Когда функция обращается к переменной, она берет
текущее значение из своего или внешнего лексического окружения.
К условию
Ответ: Pete.
Функция work() в приведенном ниже коде получает name из места его происхождения через ссылку на
внешнее лексическое окружение:
Но если бы в makeWorker() не было let name , то поиск шел бы снаружи и брал глобальную переменную,
что мы видим из приведенной выше цепочки. В этом случае результатом было бы "John" .
К условию
Независимы ли счётчики?
Ответ: 0,1.
Так что у них независимые внешние лексические окружения, у каждого из которых свой собственный count .
567/597
К условию
Объект счётчика
Обе вложенные функции были созданы с одним и тем же внешним лексическим окружением, так что они
имеют доступ к одной и той же переменной count :
function Counter() {
let count = 0;
[Link] = function() {
return ++count;
};
[Link] = function() {
return --count;
};
}
alert( [Link]() ); // 1
alert( [Link]() ); // 2
alert( [Link]() ); // 1
К условию
Функция внутри if
Функция sayHi объявлена внутри if , так что она живёт только внутри этого блока. Снаружи нет sayHi .
К условию
Вот так:
function sum(a) {
return function(b) {
return a + b; // берёт "a" из внешнего лексического окружения
};
alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4
К условию
Видна ли переменная?
Ответ: ошибка.
let x = 1;
function func() {
568/597
[Link](x); // ReferenceError: Cannot access 'x' before initialization
let x = 2;
}
func();
Как вы могли прочитать в статье Область видимости переменных, замыкание, переменная находится в
неинициализированном состоянии с момента входа в блок кода (или функцию). И остается
неинициализированной до соответствующего оператора let .
function func() {
// локальная переменная x известна движку с самого начала выполнения функции,
// но она является неинициализированной до let ("мёртвая зона")
// следовательно, ошибка
let x = 2;
}
Эту зону временной непригодности переменной (от начала блока кода до let ) иногда называют «мёртвой
зоной».
К условию
Фильтр inBetween
function inBetween(a, b) {
return function(x) {
return x >= a && x <= b;
};
}
Фильтр inArray
function inArray(arr) {
return function(x) {
return [Link](x);
};
}
К условию
Сортировать по полю
function byField(fieldName){
return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
569/597
}
К условию
Армия функций
1.
2.
shooters = [
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); }
];
3.
Позже вызов army[5]() получит элемент army[5] из массива (это будет функция) и вызовет её.
Всё потому, что внутри функций shooter нет локальной переменной i . Когда вызывается такая функция,
она берёт i из своего внешнего лексического окружения.
function makeArmy() {
...
let i = 0;
while (i < 10) {
let shooter = function() { // функция shooter
alert( i ); // должна выводить порядковый номер
};
[Link](shooter); // и добавлять стрелка в массив
i++;
}
...
}
…Мы увидим, что оно живёт в лексическом окружении, связанном с текущим вызовом makeArmy() . Но,
когда вызывается army[5]() , makeArmy уже завершила свою работу, и последнее значение i : 10
(конец цикла while ).
570/597
Как результат, все функции shooter получат одно и то же значение из внешнего лексического окружения:
последнее значение i=10 .
итерация while
LexicalEnvironment
<пусто>
makeArmy()
<пусто> outer LexicalEnvironment
<пусто> i: 10
<пусто>
Как вы можете видеть выше, на каждой итерации блока while {...} создается новое лексическое
окружение. Чтобы исправить это, мы можем скопировать значение i в переменную внутри блока while
{...} , например, так:
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let j = i;
let shooter = function() { // функция shooter
alert( j ); // должна выводить порядковый номер
};
[Link](shooter);
i++;
}
return shooters;
}
Функции shooter работают правильно, потому что значение i теперь живет чуть ближе. Не в
лексическом окружении makeArmy() , а в лексическом окружении, соответствующем текущей итерации
цикла:
итерация while
LexicalEnvironment
j: 0
makeArmy()
j: 1 outer LexicalEnvironment
j: 2 ...
j: 10
Этой проблемы также можно было бы избежать, если бы мы использовали for в начале, например, так:
function makeArmy() {
return shooters;
571/597
}
army[0](); // 0
army[5](); // 5
По сути, это то же самое, поскольку for на каждой итерации создает новое лексическое окружение со
своей переменной i . Поэтому функция shooter , создаваемая на каждой итерации, ссылается на свою
собственную переменную i , причем именно с этой итерации.
итерация for
LexicalEnvironment
i: 0
makeArmy()
i: 1 outer LexicalEnvironment
i: 2 ...
i: 10
Теперь, когда вы приложили столько усилий, чтобы прочитать это объяснение, а конечный вариант оказался
так прост – использовать for , вы можете задаться вопросом – стоило ли оно того?
Что ж, если бы вы могли легко ответить на вопрос из задачи, вы бы не стали читать решение. Так что, должно
быть, эта задача помогла вам лучше понять суть дела.
Кроме того, действительно встречаются случаи, когда человек предпочитает while , а не for , и другие
сценарии, где такие проблемы реальны.
К условию
В решении использована локальная переменная count , а методы сложения записаны прямо в counter .
Они разделяют одно и то же лексическое окружение и также имеют доступ к текущей переменной count .
function makeCounter() {
let count = 0;
function counter() {
return count++;
}
return counter;
}
К условию
1. В общем, чтобы это хоть как-нибудь заработало, результат, возвращаемый sum , должен быть функцией.
2. Между вызовами эта функция должна удерживать в памяти текущее значение счётчика.
572/597
3. Согласно заданию, функция должна преобразовываться в число, когда она используется с оператором
== . Функции – объекты, так что преобразование происходит, как описано в главе Преобразование объектов
в примитивы, поэтому можно создать наш собственный метод, возвращающий число.
Код:
function sum(a) {
let currentSum = a;
function f(b) {
currentSum += b;
return f;
}
[Link] = function() {
return currentSum;
};
return f;
}
alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15
Пожалуйста, обратите внимание на то, что функция sum выполняется лишь однажды и просто возвращает
функцию f .
Далее, при каждом последующем вызове, f суммирует свой аргумент со значением currentSum и
возвращает себя же.
function f(b) {
currentSum += b;
return f(); // <-- рекурсивный вызов
}
function f(b) {
currentSum += b;
return f; // <-- не вызывает себя. Просто возвращает
}
Функция f будет использоваться в последующем вызове и снова возвращать себя столько раз, сколько будет
необходимо. Затем, при использовании в качестве числа или строки, метод toString возвращает
currentSum – число. Также здесь мы можем использовать [Link] или valueOf для
преобразования.
К условию
Используем setInterval :
573/597
let timerId = setInterval(function() {
alert(current);
if (current == to) {
clearInterval(timerId);
}
current++;
}, 1000);
}
// использование:
printNumbers(5, 10);
setTimeout(function go() {
alert(current);
if (current < to) {
setTimeout(go, 1000);
}
current++;
}, 1000);
}
// использование:
printNumbers(5, 10);
Заметим, что в обоих решениях есть начальная задержка перед первым выводом. Она составляет одну
секунду (1000мс). Если мы хотим, чтобы функция запускалась сразу же, то надо добавить такой запуск
вручную на отдельной строке, вот так:
function go() {
alert(current);
if (current == to) {
clearInterval(timerId);
}
current++;
}
go();
let timerId = setInterval(go, 1000);
}
printNumbers(5, 10);
К условию
Любой вызов setTimeout будет выполнен только после того, как текущий код завершится.
let i = 0;
К условию
574/597
Декораторы и переадресация вызова, call/apply
Декоратор-шпион
Обертка, возвращаемая spy(f) , должна хранить все аргументы, и затем использовать [Link] для
переадресации вызова.
function spy(func) {
function wrapper(...args) {
// мы используем ...args вместо arguments для хранения "реального" массива в [Link]
[Link](args);
return [Link](this, args);
}
[Link] = [];
return wrapper;
}
К условию
Задерживающий декоратор
Решение:
return function() {
setTimeout(() => [Link](this, arguments), ms);
};
Обратите внимание, как здесь используется функция-стрелка. Как мы знаем, функция-стрелка не имеет
собственных this и arguments , поэтому [Link](this, arguments) берет this и arguments из
обёртки.
Если мы передадим обычную функцию, setTimeout вызовет её без аргументов и с this=window (при
условии, что код выполняется в браузере).
Мы всё ещё можем передать правильный this , используя промежуточную переменную, но это немного
громоздко:
return function(...args) {
let savedThis = this; // сохраняем this в промежуточную переменную
setTimeout(function() {
[Link](savedThis, args); // используем её
}, ms);
};
К условию
575/597
Декоратор debounce
Вызов debounce возвращает обёртку. При вызове он планирует вызов исходной функции через указанное
количество ms и отменяет предыдущий такой тайм-аут.
К условию
function wrapper() {
if (isThrottled) { // (2)
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
[Link](savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
1. Во время первого вызова обёртка просто вызывает func и устанавливает состояние задержки
( isThrottled = true ).
2. В этом состоянии все вызовы запоминаются в saveArgs / saveThis . Обратите внимание, что контекст
и аргументы одинаково важны и должны быть запомнены. Они нам нужны для того, чтобы воспроизвести
вызов позднее.
3. Затем по прошествии ms миллисекунд срабатывает setTimeout . Состояние задержки сбрасывается
( isThrottled = false ). И если мы проигнорировали вызовы, то «обёртка» выполняется с последними
запомненными аргументами и контекстом.
На третьем шаге выполняется не func , а wrapper , потому что нам нужно не только выполнить func , но и
ещё раз установить состояние задержки и таймаут для его сброса.
К условию
576/597
Привязка контекста к функции
Ответ: null .
function f() {
alert( this ); // null
}
let user = {
g: [Link](null)
};
user.g();
Контекст связанной функции жёстко фиксирован. Изменить однажды привязанный контекст уже нельзя.
Так что хоть мы и вызываем user.g() , внутри исходная функция будет вызвана с this=null . Однако,
функции g совершенно без разницы, какой this она получила. Её единственное предназначение – это
передать вызов в f вместе с аргументами и ранее указанным контекстом null , что она и делает.
К условию
Повторный bind
Ответ: Вася.
function f() {
alert([Link]);
}
f(); // Вася
Экзотический объект bound function , возвращаемый при первом вызове [Link](...) , запоминает
контекст (и аргументы, если они были переданы) только во время создания.
Следующий вызов bind будет устанавливать контекст уже для этого объекта. Это ни на что не повлияет.
К условию
Ответ: undefined .
Результатом работы bind является другой объект. У него уже нет свойства test .
К условию
Ошибка происходит потому, что askPassword получает функции loginOk/loginFail без контекста.
577/597
Используем bind , чтобы передать в askPassword функции loginOk/loginFail с уже привязанным
контекстом:
let user = {
name: 'Вася',
loginOk() {
alert(`${[Link]} logged in`);
},
loginFail() {
alert(`${[Link]} failed to log in`);
},
};
askPassword([Link](user), [Link](user));
//...
askPassword(() => [Link](), () => [Link]());
Обычно это также работает и хорошо выглядит. Но может не сработать в более сложных ситуациях, а именно
– когда значение переменной user меняется между вызовом askPassword и выполнением () =>
[Link]() .
К условию
1.
2.
Или же можно создать частично применённую функцию на основе [Link] , которая использует объект
user в качестве контекста и получает соответствующий первый аргумент:
К условию
Прототипное наследование
Работа с прототипами
578/597
2. null , берётся из animal .
3. undefined , такого свойства больше нет.
К условию
Алгоритм поиска
1.
let head = {
glasses: 1
};
let table = {
pen: 3,
__proto__: head
};
let bed = {
sheet: 1,
pillow: 2,
__proto__: table
};
let pockets = {
money: 2000,
__proto__: bed
};
alert( [Link] ); // 3
alert( [Link] ); // 1
alert( [Link] ); // undefined
2.
С точки зрения производительности, для современных движков неважно, откуда берётся свойство – из
объекта или из прототипа. Они запоминают, где было найдено свойство, и повторно используют его в
следующем запросе.
Например, при обращении к [Link] они запомнят, что нашли glasses в head , и в
следующий раз будут искать там же. Они достаточно умны, чтобы при изменениях обновлять внутренний
кеш, поэтому такая оптимизация безопасна.
К условию
Ответ: rabbit .
Поскольку this – это объект, который стоит перед точкой, [Link]() изменяет объект rabbit .
Поиск свойства и исполнение кода – два разных процесса. Сначала осуществляется поиск метода
[Link] в прототипе, а затем этот метод выполняется с this=rabbit .
К условию
1.
579/597
Сначала в прототипе ( =hamster ) находится метод [Link] , а затем он выполняется с this=speedy
(объект перед точкой).
2.
Затем в [Link]() нужно найти свойство stomach и вызвать для него push . Движок ищет
stomach в this ( =speedy ), но ничего не находит.
3.
4.
let hamster = {
stomach: [],
eat(food) {
// присвоение значения [Link] вместо вызова [Link]
[Link] = [food];
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
Теперь всё работает правильно, потому что [Link]= не ищет свойство stomach . Значение
записывается непосредственно в объект this .
Также мы можем полностью избежать проблемы, если у каждого хомяка будет собственный живот:
let hamster = {
stomach: [],
eat(food) {
[Link](food);
}
};
let speedy = {
__proto__: hamster,
stomach: []
};
let lazy = {
__proto__: hamster,
stomach: []
};
580/597
alert( [Link] ); // apple
Все свойства, описывающие состояние объекта (как свойство stomach в примере выше), рекомендуется
записывать в сам этот объект. Это позволяет избежать подобных проблем.
К условию
[Link]
Изменяем "prototype"
Ответы:
1.
true .
2.
false .
Объекты присваиваются по ссылке. Не создаётся копия [Link] , это всегда один объект, на
который ссылается и [Link] , и [[Prototype]] объекта rabbit .
Таким образом, когда мы изменяем этот объект по одной ссылке, изменения видны и по другой.
3.
true .
Операция delete применяется к свойствам конкретного объекта, на котором она вызвана. Здесь delete
[Link] пытается удалить свойство eats из объекта rabbit , но его там нет. Таким образом, просто
ничего не произойдёт.
4.
undefined .
К условию
Мы можем использовать такой способ, если мы уверены в том, что свойство "constructor" существующего
объекта имеет корректное значение.
Например, если мы не меняли "prototype" , используемый по умолчанию, то код ниже, без сомнений,
сработает:
function User(name) {
[Link] = name;
}
581/597
alert( [Link] ); // Pete (сработало!)
…Но если кто-то перезапишет [Link] и забудет заново назначить свойство "constructor" ,
чтобы оно указывало на User , то ничего не выйдет.
Например:
function User(name) {
[Link] = name;
}
[Link] = {}; // (*)
Вероятно, это не то, что нам нужно. Мы хотели создать new User , а не new Object . Это и есть результат
отсутствия конструктора.
(На всякий случай, если вам интересно, вызов new Object(...) преобразует свой аргумент в объект. Это
теоретическая вещь, на практике никто не вызывает new Object со значением, тем более, в основном мы
вообще не используем new Object для создания объектов).
К условию
Встроенные прототипы
[Link] = function(ms) {
setTimeout(this, ms);
};
function f() {
alert("Hello!");
}
К условию
582/597
[Link] = function(ms) {
let f = this;
return function(...args) {
setTimeout(() => [Link](this, args), ms);
}
};
// check it
function f(a, b) {
alert( a + b );
}
К условию
В методе можно получить все перечисляемые ключи с помощью [Link] и вывести их список.
Чтобы сделать toString неперечисляемым, давайте определим его, используя дескриптор свойства. Синтаксис
[Link] позволяет нам добавить в объект дескрипторы свойств как второй аргумент.
[Link] = "Apple";
dictionary.__proto__ = "test";
Когда мы создаём свойство с помощью дескриптора, все флаги по умолчанию имеют значение false . Таким
образом, в коде выше [Link] – неперечисляемое свойство.
К условию
В первом вызове this == rabbit , во всех остальных this равен [Link] , так как это объект
перед точкой.
function Rabbit(name) {
[Link] = name;
}
[Link] = function() {
alert( [Link] );
}
583/597
let rabbit = new Rabbit("Rabbit");
[Link](); // Rabbit
[Link](); // undefined
[Link](rabbit).sayHi(); // undefined
rabbit.__proto__.sayHi(); // undefined
К условию
Перепишите класс
class Clock {
constructor({ template }) {
[Link] = template;
}
render() {
let date = new Date();
[Link](output);
}
stop() {
clearInterval([Link]);
}
start() {
[Link]();
[Link] = setInterval(() => [Link](), 1000);
}
}
К условию
Наследование классов
Ошибка возникает потому, что конструктор дочернего класса должен вызывать super() .
584/597
class Animal {
constructor(name) {
[Link] = name;
}
К условию
Улучшенные часы
start() {
[Link]();
[Link] = setInterval(() => [Link](), [Link]);
}
};
К условию
Причина становится очевидна, если мы попытаемся запустить его. Унаследованный конструктор класса
должен вызывать super() . В противном случае "this" будет не определён.
Решение:
Даже после исправления есть важное различие между "class Rabbit extends Object" и class
Rabbit .
585/597
Как мы знаем, синтаксис «extends» устанавливает 2 прототипа:
Таким образом, Rabbit предоставляет доступ к статическим методам Object через Rabbit , например:
Пример:
class Rabbit {}
Таким образом, в этом случае у Rabbit нет доступа к статическим методам Object .
Кстати, у [Link] также есть «общие» методы, такие как call , bind и т. д. Они в конечном
итоге доступны в обоих случаях, потому что для встроенного конструктора Object Object.__proto__ ===
[Link] .
Пример на картинке:
[Link] [Link]
call: function call: function
bind: function bind: function
... ...
[[Prototype]] [[Prototype]]
Rabbit Object
constructor constructor
[[Prototype]]
Rabbit
constructor
К условию
586/597
Проверка класса: "instanceof"
Странный instanceof
Но instanceof не учитывает саму функцию при проверке, а только prototype , который проверяется на
совпадения в прототипной цепочке.
К условию
Например, когда есть return внутри try..catch . Секция finally работает в любом случае при любом
выходе из try..catch , даже через return : сразу после того как try..catch выполнится, но до того, как
вызывающий код получит контроль.
function f() {
try {
alert('начало');
return "result";
} catch (e) {
/// ...
} finally {
alert('очистка!');
}
}
f(); // очистка!
function f() {
try {
alert('начало');
throw new Error("ошибка");
} catch (e) {
// ...
if("не могу обработать ошибку") {
throw e;
}
} finally {
alert('очистка!')
}
}
f(); // очистка!
Именно finally гарантирует очистку. Если мы просто поместим код в конце f , то он не выполнится в
описанных ситуациях.
К условию
587/597
Пользовательские ошибки, расширение Error
Наследование от SyntaxError
К условию
Промисы
Вывод будет: 1 .
Второй вызов resolve будет проигнорирован, поскольку учитывается только первый вызов
reject/resolve . Все последующие вызовы – игнорируются.
К условию
Задержка на промисах
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Заметьте, что resolve вызывается без аргументов. Мы не возвращаем из delay ничего, просто
гарантируем задержку.
К условию
К условию
Цепочка промисов
588/597
Разница в том, что если ошибка произойдёт в f1 , то она будет обработана в .catch в этом примере:
promise
.then(f1)
.catch(f2);
…но не в этом:
promise
.then(f1, f2);
Другими словами, .then передаёт результат или ошибку следующему блоку .then/catch . Так как в первом
примере в цепочке далее имеется блок catch , а во втором – нет, то ошибка в нём останется необработанной.
К условию
Ошибка в setTimeout
Как было сказано в главе, здесь присутствует «скрытый try..catch » вокруг кода функции. Поэтому
обрабатываются все синхронные ошибки.
В данном примере ошибка генерируется не по ходу выполнения кода, а позже. Поэтому промис не может
обработать её.
К условию
Async/await
if ([Link] == 200) {
let json = await [Link](); // (3)
return json;
}
loadJson('[Link]')
.catch(alert); // Error: 404 (4)
Комментарии:
589/597
1.
2.
3.
Можно было бы просто вернуть промис во внешний код return [Link]() , вот так:
if ([Link] == 200) {
return [Link](); // (3)
}
Тогда внешнему коду пришлось бы получать результат промиса самостоятельно (через .then или await ).
В нашем варианте это не обязательно.
4.
К условию
В этой задаче нет ничего сложного. Нужно заменить .catch на try...catch внутри demoGithubUser и
добавить async/await , где необходимо:
let user;
while(true) {
let name = prompt("Введите логин?", "iliakan");
try {
user = await loadJson(`[Link]
break; // ошибок не было, выходим из цикла
} catch(err) {
if (err instanceof HttpError && [Link] == 404) {
// после alert начнётся новая итерация цикла
alert("Такого пользователя не существует, пожалуйста, повторите ввод.");
} else {
// неизвестная ошибка, пробрасываем её
throw err;
}
}
}
590/597
alert(`Полное имя: ${[Link]}.`);
return user;
}
demoGithubUser();
К условию
Это тот случай, когда понимание внутреннего устройства работы async/await очень кстати.
Здесь нужно думать о вызове функции async , как о промисе. И просто воспользоваться .then :
return 10;
}
function f() {
// покажет 10 через 1 секунду
wait().then(result => alert(result));
}
f();
К условию
Генераторы
Псевдослучайный генератор
function* pseudoRandom(seed) {
let value = seed;
while(true) {
value = value * 16807 % 2147483647
yield value;
}
};
alert([Link]().value); // 16807
alert([Link]().value); // 282475249
alert([Link]().value); // 1622650073
Пожалуйста, обратите внимание, то же самое можно сделать с помощью обычной функции, такой как эта:
function pseudoRandom(seed) {
let value = seed;
return function() {
value = value * 16807 % 2147483647;
return value;
}
}
alert(generator()); // 16807
alert(generator()); // 282475249
alert(generator()); // 1622650073
591/597
Это также работает. Но тогда мы потеряем возможность перебора с помощью for..of и использования
композиции генераторов, которая тоже может быть полезна.
К условию
Proxy и Reflect
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return [Link](target, prop, receiver);
} else {
throw new ReferenceError(`Свойство не существует: "${prop}"`)
}
}
});
}
user = wrap(user);
alert([Link]); // John
alert([Link]); // Ошибка: Свойство не существует
К условию
alert(array[-1]); // 3
alert(array[-2]); // 2
К условию
Observable
1. При вызове .observe(handler) нам нужно где-то сохранить обработчик, чтобы вызвать его позже.
Можно хранить обработчики прямо в объекте, создав в нём для этого свой символьный ключ.
2. Нам нужен прокси с ловушкой set , чтобы вызывать обработчики при изменении свойств.
592/597
let handlers = Symbol('handlers');
function makeObservable(target) {
// 1. Создадим хранилище обработчиков
target[handlers] = [];
user = makeObservable(user);
[Link] = "John";
К условию
Eval-калькулятор
alert( eval(expr) );
К условию
Ссылочный тип
Проверка синтаксиса
Ошибка!
Попробуйте запустить:
593/597
let user = {
name: "John",
go: function() { alert([Link]) }
}
([Link])() // ошибка!
Ошибка появляется, потому что точка с запятой пропущена после user = {...} .
JavaScript не вставляет автоматически точку с запятой перед круглой скобкой ([Link])() , поэтому читает
этот код так:
Теперь мы тоже можем увидеть, что такое объединённое выражение синтаксически является вызовом объекта
{ go: ... } как функции с аргументом ([Link]) . И это происходит в той же строчке с объявлением
переменной let user , т.е. объект user ещё даже не определён, поэтому получается ошибка.
let user = {
name: "John",
go: function() { alert([Link]) }
};
([Link])() // John
Обратите внимание, что круглые скобки вокруг ([Link]) ничего не значат. Обычно они определяют
последовательность операций (оператор группировки), но здесь вызов метода через точку . срабатывает
первым в любом случае, поэтому группировка ни на что не влияет. Только точка с запятой имеет значение.
К условию
1.
Это обычный вызов метода объекта через точку . , и this ссылается на объект перед точкой.
2.
Здесь то же самое. Круглые скобки (оператор группировки) тут не изменяют порядок выполнения операций
– доступ к методу через точку в любом случае срабатывает первым.
3.
Здесь мы имеем более сложный вызов (expression).method() . Такой вызов работает, как если бы он
был разделён на 2 строчки:
4.
Чтобы объяснить поведение в примерах (3) и (4) , нам нужно помнить, что доступ к свойству (через точку
или квадратные скобки) возвращает специальное значение ссылочного типа (Reference Type).
594/597
За исключением вызова метода, любая другая операция (подобно операции присваивания = или сравнения
через логические операторы, например || ) превращает это значение в обычное, которое не несёт
информации, позволяющей установить this .
К условию
Побитовые операторы
1.
Операция a^b ставит бит результата в 1 , если на соответствующей битовой позиции в a или b (но не
одновременно) стоит 1 .
Так как в 0 везде стоят нули, то биты берутся в точности как во втором аргументе.
2.
К условию
function isInteger(num) {
return (num ^ 0) === num;
}
Обратите внимание: num^0 – в скобках! Это потому, что приоритет операции ^ очень низкий. Если не
поставить скобку, то === сработает раньше. Получится num ^ (0 === num) , а это уже совсем другое дело.
К условию
a b результат
0 0 0
0 1 1
1 0 1
1 1 0
Случаи 0^0 и 1^1 заведомо не изменятся при перемене мест, поэтому нас не интересуют. А вот 0^1 и 1^0
эквивалентны и равны 1 .
595/597
Ответ: да.
К условию
Всё дело в том, что побитовые операции преобразуют число в 32-битное целое.
Обычно число в JavaScript имеет 64-битный формат с плавающей точкой. При этом часть битов ( 52 )
отведены под цифры, часть ( 11 ) отведены под хранение номера позиции, на которой стоит десятичная точка,
и один бит – знак числа.
Это означает, что максимальное целое число, которое можно хранить, занимает 52 бита.
Побитовый оператор ^ преобразует его в 32-битное путём отбрасывания десятичной точки и «лишних»
старших цифр. При этом, так как число большое и старшие биты здесь ненулевые, то, естественно, оно
изменится.
// пограничный случай
// в двоичном виде 10000000000000000000000000000000 (32 цифры)
alert( [Link](2, 31) ); // 2147483648
alert( [Link](2, 31) ^ 0 ); // -2147483648, ничего не отброшено,
// но первый бит 1 теперь стоит в начале числа и является знаковым
К условию
596/597
К условию
597/597