diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..ca5355a93c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Наш Discord-сервер + url: https://discord.gg/X8yWNWpTQs + about: Вопросы по JavaScript, HTML, CSS, Node.js, React, TypeScript, Vue.js, Angular, а также «code review» и обсуждение программирования в целом diff --git a/.github/ISSUE_TEMPLATE/tutorial_suggestion.yml b/.github/ISSUE_TEMPLATE/tutorial_suggestion.yml new file mode 100644 index 0000000000..fc2e5cfa77 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tutorial_suggestion.yml @@ -0,0 +1,52 @@ +name: 📙 Предложить идею по улучшению материала учебника +description: Предложения о добавлении новых разделов/статей в учебник; о переработке существующих статей; о добавлении новых материалов в статьи +title: "[Предложение: учебник]: " +labels: ["suggestion: tutorial"] +body: + - type: markdown + attributes: + value: | + ## Не открывайте Issue в этой категории, если хотите предложить: + * Исправление/изменение кода в какой-либо из статей + * Исправление неточности/устаревшей информации в какой-либо из статей + * Исправление орфографических/пунктуационных/стилистических ошибок в какой-либо из статей + * Добавление задач в какую-либо из статей + + ...А так же любые другие **небольшие** правки, которые, на ваш взгляд, не требуют обсуждения и могут быть сразу отправлены на рассмотрение. + * «[Как внести правки в учебник?](../blob/master/CONTRIBUTING.md#как-внести-правки-в-учебник)» + ## Открывайте Issue в этой категории, если хотите предложить: + * Переработку какой-либо статьи (или переработку определенной части статьи) + * Добавление новых разделов в учебник + * Добавление новых статей в учебник + * Добавление нового материала в какую-либо из статей + * ...а так же любые другие **крупные** правки + + Перечисленные выше темы требуют предварительного обсуждения с редакторами учебника. + - type: textarea + id: suggestion-description + attributes: + label: Описание + description: Что именно вы хотите предложить? + placeholder: | + Например: «в JavaScript недавно был добавлен [...] — на мой взгляд, было бы неплохо сделать новую статью на эту тему». + validations: + required: true + - type: textarea + id: suggestion-motivation + attributes: + label: Мотивация + description: Почему предлагаемые правки действительно должны быть внесены в учебник? + placeholder: | + Например: «я считаю, что любое представление о [...] без понимания концепции [...] будет недостаточным и неполным, у читателя может сложиться ложное впечатление». + validations: + required: true + - type: dropdown + id: suggestion-question + attributes: + label: Готовы ли вы принять участие в реализации данного предложения? + description: Например, если вы предлагаете добавить новую статью — готовы ли вы заняться её написанием? + options: + - Да, я готов(а) принять участие. + - Нет, я не готов(а) принимать участие. Оставляю эту возможность редакторам учебника. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/website_suggestion.yml b/.github/ISSUE_TEMPLATE/website_suggestion.yml new file mode 100644 index 0000000000..fb2f960925 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/website_suggestion.yml @@ -0,0 +1,23 @@ +name: 🌐 Предложить идею по улучшению веб-сайта +description: Предложения о добавлении нового функционала на сайт учебника; ошибки/опечатки во вкладках «Курсы», «Тесты знаний», «Скринкасты», «о проекте» и т.д; идеи по улучшению курсов +title: "[Предложение: веб-сайт]: " +labels: ["suggestion: website"] +body: + - type: markdown + attributes: + value: | + Всё, что касается именно **веб-сайта**, на котором опубликован учебник — [https://learn.javascript.ru](https://learn.javascript.ru). + + В этой категории вы можете: + * Предложить добавление нового функционала на веб-сайт учебника + * Сообщить об ошибках/опечатках во вкладках «Курсы», «Тесты знаний», «Скринкасты», «о проекте» и т.д. + * Предложить идеи по улучшению курсов + - type: textarea + id: suggestion-description + attributes: + label: Описание + description: Что именно вы хотите предложить (или о чём хотите сообщить)? + placeholder: | + Например: «мне кажется, было бы неплохо добавить возможность [...]». + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/wquestion.yml b/.github/ISSUE_TEMPLATE/wquestion.yml new file mode 100644 index 0000000000..c92886b37b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/wquestion.yml @@ -0,0 +1,20 @@ +name: 📨 Задать вопрос +description: Вопросы по материалу учебника; по веб-сайту; по редактированию учебника +title: "[Вопрос]: " +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Любые вопросы по материалу учебника; по веб-сайту; по редактированию учебника и т.д. + + ### **Мы не консультируем по вопросам о JavaScript здесь.** Для этих целей есть [Discord-сервер](https://discord.gg/X8yWNWpTQs). + - type: textarea + id: question-description + attributes: + label: Описание + description: О чём вы хотите спросить? + placeholder: | + Например: «почему раздел [...] идёт перед разделом [...], а не после него?». + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/ybug_report.yml b/.github/ISSUE_TEMPLATE/ybug_report.yml new file mode 100644 index 0000000000..d9846d42f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ybug_report.yml @@ -0,0 +1,36 @@ +name: 🛑 Сообщить о баге +description: На сайте учебника что-то неправильно отображается; не работает; работает не так, как нужно +title: "[Баг]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + **Пожалуйста, заполните форму ниже максимально точной информацией.**
Правильное описание бага поможет нам намного быстрее решить проблему. + - type: textarea + id: bug-description + attributes: + label: Описание + description: В чём именно заключается баг? + placeholder: | + Например: «в статье [...] неправильно отображается [...]». + validations: + required: true + - type: textarea + id: bug-reproduce + attributes: + label: Как воспроизвести? + description: Что нужно сделать, чтобы воспроизвести этот баг? + placeholder: | + Например: «чтобы воспроизвести баг, нужно: (1) открыть страницу [...] (2) перейти в раздел [...] (3) нажать на [...]». + validations: + required: true + - type: textarea + id: bug-browsers + attributes: + label: Браузер + description: В каком браузере у вас воспроизводится этот баг? Какая версия этого браузера? (можно указать несколько, если тестировали не в одном браузере) + placeholder: | + Например: «у меня воспроизводится этот баг в Chrome 116.0.5845.98» + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..5ad133a198 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ + + +## Описание + + + +## Ссылки + + + + + + + + + + +## Связанные Issue + + diff --git a/1-js/01-getting-started/1-intro/article.md b/1-js/01-getting-started/1-intro/article.md index bc9bdba39f..64ef99485d 100644 --- a/1-js/01-getting-started/1-intro/article.md +++ b/1-js/01-getting-started/1-intro/article.md @@ -1,6 +1,6 @@ # Введение в JavaScript -Давайте посмотрим, что такого особенного в JavaScript, чего можно достичь с его помощью, и какие другие технологии хорошо с ним работают. +Давайте посмотрим, что такого особенного в JavaScript, чего можно достичь с его помощью и какие другие технологии хорошо с ним работают. ## Что такое JavaScript? @@ -13,9 +13,9 @@ Это отличает JavaScript от другого языка - [Java](https://ru.wikipedia.org/wiki/Java). ```smart header="Почему JavaScript?" -Когда JavaScript создавался, у него было другое имя - "LiveScript". Однако, Java была очень популярная в то время и было решено, что позиционирование языка как "младшего брата" Java будет полезно. +Когда JavaScript создавался, у него было другое имя - "LiveScript". Однако, язык Java был очень популярен в то время, и было решено, что позиционирование JavaScript как "младшего брата" Java будет полезно. -Со временем JavaScript стал полностью независимым языком со своей собственной спецификацией, называющейся [ECMAScript](http://ru.wikipedia.org/wiki/ECMAScript), и сейчас не имеет никакого отношения к Java. +Со временем JavaScript стал полностью независимым языком со своей собственной спецификацией, называющейся [ECMAScript](https://ru.wikipedia.org/wiki/ECMAScript), и сейчас не имеет никакого отношения к Java. ``` Сегодня JavaScript может выполняться не только в браузере, но и на сервере или на любом другом устройстве, которое имеет специальную программу, называющуюся ["движком" JavaScript](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%B8%D0%B6%D0%BE%D0%BA_JavaScript). @@ -24,11 +24,11 @@ Разные движки имеют разные "кодовые имена". Например: -- [V8](https://ru.wikipedia.org/wiki/V8_(%D0%B4%D0%B2%D0%B8%D0%B6%D0%BE%D0%BA_JavaScript)) -- в Chrome и Opera. +- [V8](https://ru.wikipedia.org/wiki/V8_(%D0%B4%D0%B2%D0%B8%D0%B6%D0%BE%D0%BA_JavaScript)) -- в Chrome, Opera и Edge. - [SpiderMonkey](https://ru.wikipedia.org/wiki/SpiderMonkey) -- в Firefox. -- ...Ещё есть "Trident" и "Chakra" для разных версий IE, "ChakraCore" для Microsoft Edge, "Nitro" и "SquirrelFish" для Safari и т.д. +- ...Ещё есть "Chakra" для IE, "JavaScriptCore", "Nitro" и "SquirrelFish" для Safari и т.д. -Эти названия полезно знать, так как они часто используются в статьях для разработчиков. Мы тоже будем их использовать. Например, если "функциональность X поддерживается V8", тогда "Х", скорее всего, работает в Chrome и Opera. +Эти названия полезно знать, так как они часто используются в статьях для разработчиков. Мы тоже будем их использовать. Например, если "функциональность X поддерживается V8", тогда "Х", скорее всего, работает в Chrome, Opera и Edge. ```smart header="Как работают движки?" @@ -45,13 +45,13 @@ Современный JavaScript - это "безопасный" язык программирования. Он не предоставляет низкоуровневый доступ к памяти или процессору, потому что изначально был создан для браузеров, не требующих этого. -Возможности JavaScript сильно зависят от окружения, в котором он работает. Например, [Node.JS](https://ru.wikipedia.org/wiki/Node.js) поддерживает функции чтения/записи произвольных файлов, выполнения сетевых запросов и т.д. +Возможности JavaScript сильно зависят от окружения, в котором он работает. Например, [Node.js](https://ru.wikipedia.org/wiki/Node.js) поддерживает функции чтения/записи произвольных файлов, выполнения сетевых запросов и т.д. В браузере для JavaScript доступно всё, что связано с манипулированием веб-страницами, взаимодействием с пользователем и веб-сервером. Например, в браузере JavaScript может: -- Добавлять новый HTML на страницу, изменять существующее содержимое, модифицировать стили. +- Добавлять новый HTML-код на страницу, изменять существующее содержимое, модифицировать стили. - Реагировать на действия пользователя, щелчки мыши, перемещения указателя, нажатия клавиш. - Отправлять сетевые запросы на удалённые сервера, скачивать и загружать файлы (технологии [AJAX](https://ru.wikipedia.org/wiki/AJAX) и [COMET](https://ru.wikipedia.org/wiki/Comet_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5))). - Получать и устанавливать куки, задавать вопросы посетителю, показывать сообщения. @@ -72,8 +72,8 @@ Это называется "Политика одинакового источника" (Same Origin Policy). Чтобы обойти это ограничение, обе страницы должны согласиться с этим и содержать JavaScript-код, который специальным образом обменивается данными. - Это ограничение необходимо, опять же, для безопасности пользователя. Страница `http://anysite.com`, которую открыл пользователь, не должна иметь доступ к другой вкладке браузера с URL `http://gmail.com` и воровать информацию оттуда. -- JavaScript может легко взаимодействовать с сервером, с которого пришла текущая страница. Но его способность получать данные с других сайтов/доменов ограничена. Хотя это возможно, требуется явное согласие (выраженное в заголовках HTTP) с удалённой стороной. Опять же, это ограничение безопасности. + Это ограничение необходимо, опять же, для безопасности пользователя. Страница `https://anysite.com`, которую открыл пользователь, не должна иметь доступ к другой вкладке браузера с URL `https://gmail.com` и воровать информацию оттуда. +- JavaScript может легко взаимодействовать с сервером, с которого пришла текущая страница. Но его способность получать данные с других сайтов/доменов ограничена. Хотя это возможно в принципе, для чего требуется явное согласие (выраженное в заголовках HTTP) с удалённой стороной. Опять же, это ограничение безопасности. ![](limitations.svg) @@ -90,7 +90,7 @@ ``` JavaScript - это единственная браузерная технология, сочетающая в себе все эти три вещи. -Вот, что делает JavaScript особенным. Вот, почему это самый распространённый инструмент для создания интерфейсов в браузере. +Вот что делает JavaScript особенным. Вот почему это самый распространённый инструмент для создания интерфейсов в браузере. Хотя, конечно, JavaScript позволяет делать приложения не только в браузерах, но и на сервере, на мобильных устройствах и т.п. @@ -98,7 +98,7 @@ JavaScript - это единственная браузерная техноло Синтаксис JavaScript подходит не под все нужды. Разные люди хотят иметь разные возможности. -Это естественно, потому что проекты разные, и требования к ним тоже разные. +Это естественно, потому что проекты разные и требования к ним тоже разные. Так, в последнее время появилось много новых языков, которые *транспилируются* (конвертируются) в JavaScript, прежде чем запустятся в браузере. @@ -106,10 +106,11 @@ JavaScript - это единственная браузерная техноло Примеры таких языков: -- [CoffeeScript](http://coffeescript.org/) добавляет "синтаксический сахар" для JavaScript. Он вводит более короткий синтаксис, который позволяет писать чистый и лаконичный код. Обычно такое нравится Ruby-программистам. -- [TypeScript](http://www.typescriptlang.org/) концентрируется на добавлении "строгой типизации" для упрощения разработки и поддержки больших и сложных систем. Разработан Microsoft. -- [Flow](http://flow.org/) тоже добавляет типизацию, но иначе. Разработан Facebook. +- [CoffeeScript](https://coffeescript.org/) добавляет "синтаксический сахар" для JavaScript. Он вводит более короткий синтаксис, который позволяет писать чистый и лаконичный код. Обычно такое нравится Ruby-программистам. +- [TypeScript](https://www.typescriptlang.org/) концентрируется на добавлении "строгой типизации" для упрощения разработки и поддержки больших и сложных систем. Разработан Microsoft. +- [Flow](https://flow.org/) тоже добавляет типизацию, но иначе. Разработан Facebook. - [Dart](https://www.dartlang.org/) стоит особняком, потому что имеет собственный движок, работающий вне браузера (например, в мобильных приложениях). Первоначально был предложен Google, как замена JavaScript, но на данный момент необходима его транспиляция для запуска так же, как для вышеперечисленных языков. +- [Brython](https://brython.info/) транспилирует Python в JavaScript, что позволяет писать приложения на чистом Python без JavaScript. Есть и другие. Но даже если мы используем один из этих языков, мы должны знать JavaScript, чтобы действительно понимать, что мы делаем. diff --git a/1-js/01-getting-started/1-intro/limitations.svg b/1-js/01-getting-started/1-intro/limitations.svg index a7863c63cc..76ea43fd7a 100644 --- a/1-js/01-getting-started/1-intro/limitations.svg +++ b/1-js/01-getting-started/1-intro/limitations.svg @@ -1 +1 @@ -https://javascript.info<script> ... </script>https://gmail.comhttps://javascript.info \ No newline at end of file +https://javascript.info<script> ... </script>https://gmail.comhttps://javascript.info \ No newline at end of file diff --git a/1-js/01-getting-started/2-manuals-specifications/article.md b/1-js/01-getting-started/2-manuals-specifications/article.md index 2d1c66643c..df109cd933 100644 --- a/1-js/01-getting-started/2-manuals-specifications/article.md +++ b/1-js/01-getting-started/2-manuals-specifications/article.md @@ -5,11 +5,11 @@ ## Спецификация -[Спецификация ECMA-262](https://www.ecma-international.org/publications/standards/Ecma-262.htm) содержит самую глубокую, детальную и формализованную информацию о JavaScript. Она определяет сам язык. +[Спецификация ECMA-262](https://www.ecma-international.org/publications-and-standards/standards/ecma-262/) содержит самую глубокую, детальную и формализованную информацию о JavaScript. Она определяет сам язык. Вначале спецификация может показаться тяжеловатой для понимания из-за слишком формального стиля изложения. Если вы ищете источник самой достоверной информации, то это правильное место, но она не для ежедневного использования. -Новая версия спецификации появляется каждый год. Между этими релизами, последний "черновик" спецификации доступен на . +Новая версия спецификации появляется каждый год. А пока она не вышла официально, все желающие могут ознакомиться с текущим черновиком на . Чтобы почитать о самых последних возможностях, включая те, которые "почти в стандарте" (так называемые "stage 3 proposals"), посетите . @@ -23,18 +23,13 @@ Хотя зачастую вместо их сайта удобнее использовать какой-нибудь интернет-поисковик, вводя там запрос "MDN [что вы хотите найти]", например для поиска информации о функции `parseInt`. - -- **MSDN** – справочник от Microsoft, содержащий много информации, в том числе по JavaScript (который там часто обозначается как JScript). Если вам нужно найти что-то специфическое по браузеру Internet Explorer, лучше искать там: . - - Так же, как и в предыдущем случае, можно использовать интернет-поиск, набирая фразы типа "RegExp MSDN" или "RegExp MSDN jscript". - ## Таблицы совместимости JavaScript -- это развивающийся язык, в который постоянно добавляется что-то новое. Посмотреть, какие возможности поддерживаются в разных браузерах и других движках, можно в следующих источниках: -- - таблицы с информацией о поддержке по каждой возможности языка. Например, чтобы узнать, какие движки поддерживают современные криптографические функции, посетите: . +- - таблицы с информацией о поддержке по каждой возможности языка. Например, чтобы узнать, какие движки поддерживают современные криптографические функции, посетите: . - - таблица с возможностями языка и движками, которые их поддерживают и не поддерживают. Все эти ресурсы полезны в ежедневной работе программиста, так как они содержат ценную информацию о возможностях использования языка, их поддержке и так далее. diff --git a/1-js/01-getting-started/3-code-editors/article.md b/1-js/01-getting-started/3-code-editors/article.md index f2e518e4f8..c2078d5efa 100644 --- a/1-js/01-getting-started/3-code-editors/article.md +++ b/1-js/01-getting-started/3-code-editors/article.md @@ -8,15 +8,12 @@ Термином [IDE](https://ru.wikipedia.org/wiki/Integrated_development_environment) (Integrated Development Environment, "интегрированная среда разработки") называют мощные редакторы с множеством функций, которые работают в рамках целого проекта. Как видно из названия, это не просто редактор, а нечто большее. -IDE загружает проект (который может состоять из множества файлов), позволяет переключаться между файлами, предлагает автодополнение по коду всего проекта (а не только открытого файла), также она интегрирована с системой контроля версий (например, такой как [git](https://git-scm.com/)), средой для тестирования и другими инструментами на уровне всего проекта. +IDE загружает проект (который может состоять из множества файлов), позволяет переключаться между файлами, предлагает автодополнение по коду всего проекта (а не только открытого файла), также она интегрирована с системой контроля версий (например, такой как [git](https://git-scm.com/)), средой для тестирования и другими инструментами на уровне всего проекта. Если вы ещё не выбрали себе IDE, присмотритесь к этим: - -- [Visual Studio Code](https://code.visualstudio.com/) (бесплатно). -- [WebStorm](http://www.jetbrains.com/webstorm/) (платно). - -Обе IDE -- кроссплатформенные. +- [Visual Studio Code](https://code.visualstudio.com/) (кросс-платформенная, бесплатная). +- [WebStorm](https://www.jetbrains.com/webstorm/) (кросс-платформенная, бесплатная для некоммерческого использования). Для Windows есть ещё Visual Studio (не путать с Visual Studio Code). Visual Studio - это платная мощная среда разработки, которая работает только на Windows. Она хорошо подходит для .NET платформы. У неё есть бесплатная версия, которая называется [Visual Studio Community](https://www.visualstudio.com/vs/community/). @@ -34,10 +31,9 @@ IDE загружает проект (который может состоять Следующие варианты заслуживают вашего внимания: -- [Atom](https://atom.io/) (кроссплатформенный, бесплатный). -- [Sublime Text](http://www.sublimetext.com) (кроссплатформенный, условно-бесплатный). +- [Sublime Text](https://www.sublimetext.com) (кроссплатформенный, условно-бесплатный). - [Notepad++](https://notepad-plus-plus.org/) (Windows, бесплатный). -- [Vim](http://www.vim.org/) и [Emacs](https://www.gnu.org/software/emacs/) тоже хороши, если знать, как ими пользоваться. +- [Vim](https://www.vim.org/) и [Emacs](https://www.gnu.org/software/emacs/) тоже хороши, если знать, как ими пользоваться. ## Не будем ссориться diff --git a/1-js/01-getting-started/4-devtools/article.md b/1-js/01-getting-started/4-devtools/article.md index 1a20ffab5c..1d52219ffc 100644 --- a/1-js/01-getting-started/4-devtools/article.md +++ b/1-js/01-getting-started/4-devtools/article.md @@ -6,7 +6,7 @@ Для решения задач такого рода в браузер встроены так называемые "Инструменты разработки" (Developer tools или сокращённо — devtools). -Chrome и Firefox снискали любовь подавляющего большинства программистов во многом благодаря своим отменным инструментам разработчика. Остальные браузеры, хотя и оснащены подобными инструментами, но все же зачастую находятся в роли догоняющих и по качеству, и по количеству свойств и особенностей. В общем, почти у всех программистов есть свой "любимый" браузер. Другие используются только для отлова и исправления специфичных "браузерозависимых" ошибок. +Chrome и Firefox снискали любовь подавляющего большинства программистов во многом благодаря своим отменным инструментам разработчика. Остальные браузеры, хотя и оснащены подобными инструментами, но всё же зачастую находятся в роли догоняющих и по качеству, и по количеству свойств и особенностей. В общем, почти у всех программистов есть свой "любимый" браузер. Другие используются только для отлова и исправления специфичных "браузерозависимых" ошибок. Для начала знакомства с этими мощными инструментами давайте выясним, как их открывать, смотреть ошибки и запускать команды JavaScript. @@ -14,7 +14,7 @@ Chrome и Firefox снискали любовь подавляющего бол Откройте страницу [bug.html](bug.html). -В её JavaScript код закралась ошибка. Она не видна обычному посетителю, поэтому давайте найдём её при помощи инструментов разработки. +В её JavaScript-коде закралась ошибка. Она не видна обычному посетителю, поэтому давайте найдём её при помощи инструментов разработки. Нажмите `key:F12` или, если вы используете Mac, `key:Cmd+Opt+J`. @@ -22,23 +22,29 @@ Chrome и Firefox снискали любовь подавляющего бол Она выглядит приблизительно следующим образом: -![chrome](chrome.png) +![](chrome.webp) Точный внешний вид инструментов разработки зависит от используемой версии Chrome. Время от времени некоторые детали изменяются, но в целом внешний вид остаётся примерно похожим на предыдущие версии. - В консоли мы можем увидеть сообщение об ошибке, отрисованное красным цветом. В нашем случае скрипт содержит неизвестную команду "lalala". - Справа присутствует ссылка на исходный код `bug.html:12` с номером строки кода, в которой эта ошибка и произошла. -Под сообщением об ошибке находится синий символ `>`. Он обозначает командную строку, в ней мы можем редактировать и запускать JavaScript-команды. Для их запуска нажмите `key:Enter` (для переноса строки жмите `key:Shift+Enter`). +Под сообщением об ошибке находится синий символ `>`. Он обозначает командную строку, в ней мы можем редактировать и запускать JavaScript-команды. Для их запуска нажмите `key:Enter`. + +```smart header="Многострочный ввод" +Обычно при нажатии `key:Enter` введённая строка кода сразу выполняется. + +Чтобы перенести строку, нажмите `key:Shift+Enter`. Так можно вводить более длинный JS-код. +``` Теперь мы явно видим ошибки, для начала этого вполне достаточно. Мы ещё вернёмся к инструментам разработчика позже и более подробно рассмотрим отладку кода в главе . ## Firefox, Edge и другие -Инструменты разработчика в большинстве браузеров открываются при нажатии на `key:F12`. +Инструменты разработчика в большинстве браузеров открываются при нажатии на `key:F12`. -Их внешний вид и принципы работы мало, чем отличаются. Разобравшись с инструментами в одном браузере, вы без труда сможете работать с ними и в другом. +Их внешний вид и принципы работы мало чем отличаются. Разобравшись с инструментами в одном браузере, вы без труда сможете работать с ними и в другом. ## Safari @@ -46,16 +52,10 @@ Safari (браузер для Mac, не поддерживается в сист Откройте Настройки (Preferences) и перейдите к панели "Продвинутые" (Advanced). В самом низу вы найдёте чекбокс: -![safari](safari.png) +![](safari.png) Теперь консоль можно активировать нажатием клавиш `key:Cmd+Opt+C`. Также обратите внимание на новый элемент меню "Разработка" ("Develop"). В нем содержится большое количество команд и настроек. -```smart header="Многострочный ввод" -Обычно, при нажатии `key:Enter` введённая строка кода сразу выполняется. - -Чтобы перенести строку нажмите `key:Shift+Enter`. Так можно вводить более длинный JS-код. -``` - ## Итого - Инструменты разработчика позволяют нам смотреть ошибки, выполнять команды, проверять значение переменных и ещё много всего полезного. diff --git a/1-js/01-getting-started/4-devtools/bug.html b/1-js/01-getting-started/4-devtools/bug.html index edb02375ca..6e25cf9cd8 100644 --- a/1-js/01-getting-started/4-devtools/bug.html +++ b/1-js/01-getting-started/4-devtools/bug.html @@ -7,7 +7,7 @@ - There is an error in the script on this page. + На этой странице есть ошибка в скрипте. diff --git a/1-js/01-getting-started/4-devtools/chrome.png b/1-js/01-getting-started/4-devtools/chrome.png deleted file mode 100644 index b73056bcce..0000000000 Binary files a/1-js/01-getting-started/4-devtools/chrome.png and /dev/null differ diff --git a/1-js/01-getting-started/4-devtools/chrome.webp b/1-js/01-getting-started/4-devtools/chrome.webp new file mode 100644 index 0000000000..bdf067079e Binary files /dev/null and b/1-js/01-getting-started/4-devtools/chrome.webp differ diff --git a/1-js/01-getting-started/4-devtools/chrome@2x.png b/1-js/01-getting-started/4-devtools/chrome@2x.png deleted file mode 100644 index 2b63f5695b..0000000000 Binary files a/1-js/01-getting-started/4-devtools/chrome@2x.png and /dev/null differ diff --git a/1-js/01-getting-started/4-devtools/safari.png b/1-js/01-getting-started/4-devtools/safari.png index 64c7a3f6ca..4538827eb0 100644 Binary files a/1-js/01-getting-started/4-devtools/safari.png and b/1-js/01-getting-started/4-devtools/safari.png differ diff --git a/1-js/01-getting-started/4-devtools/safari@2x.png b/1-js/01-getting-started/4-devtools/safari@2x.png deleted file mode 100644 index 1c3611f50a..0000000000 Binary files a/1-js/01-getting-started/4-devtools/safari@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js index 4de725971d..3d5c74a758 100644 --- a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js +++ b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/alert.js @@ -1 +1 @@ -alert("I'm JavaScript!"); \ No newline at end of file +alert("Я JavaScript!"); diff --git a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md index 2f7b89b6d1..fd843c8593 100644 --- a/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md +++ b/1-js/02-first-steps/01-hello-world/2-hello-alert-ext/task.md @@ -4,6 +4,6 @@ importance: 5 # Покажите сообщение с помощью внешнего скрипта -Возьмите решение предыдущей задачи , и измените его. Извлеките содержимого скрипта во внешний файл `alert.js`, лежащий в той же папке. +Возьмите решение предыдущей задачи , и измените его. Извлеките содержимое скрипта во внешний файл `alert.js`, лежащий в той же папке. Откройте страницу, убедитесь, что оповещение работает. diff --git a/1-js/02-first-steps/01-hello-world/article.md b/1-js/02-first-steps/01-hello-world/article.md index 8c0a37d14a..b42e30a558 100644 --- a/1-js/02-first-steps/01-hello-world/article.md +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -41,7 +41,7 @@ ## Современная разметка -Тег ` ``` -Здесь, `/path/to/script.js` - это абсолютный путь до скрипта от корня сайта. Также можно указать относительный путь от текущей страницы. Например, `src="script.js"` будет означать, что файл `"script.js"` находится в текущей папке. +Здесь `/path/to/script.js` - это абсолютный путь от корневой папки до необходимого файла. Корневой папкой может быть корень диска или корень сайта, в зависимости от условий работы сайта. Также можно указать относительный путь от текущей страницы. Например, `src="script.js"` или `src="./script.js"` будет означать, что файл `"script.js"` находится в текущей папке. Можно указать и полный URL-адрес. Например: @@ -89,9 +89,9 @@ ```smart Как правило, только простейшие скрипты помещаются в HTML. Более сложные выделяются в отдельные файлы. -Польза от отдельных файлов в том, что браузер загрузит скрипт отдельно и сможет хранить его в [кеше](https://en.wikipedia.org/wiki/Web_cache). +Польза отдельных файлов в том, что браузер загрузит скрипт отдельно и сможет хранить его в [кеше](https://en.wikipedia.org/wiki/Web_cache). -Другие страницы, которые подключают тот же скрипт, смогут брать его из кэша вместо повторной загрузки его из сети. И таким образом файл будет загружаться с сервера только один раз. +Другие страницы, которые подключают тот же скрипт, смогут брать его из кеша вместо повторной загрузки из сети. И таким образом файл будет загружаться с сервера только один раз. Это сокращает расход трафика и ускоряет загрузку страниц. ``` diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md index ba68335707..88a6e2bcea 100644 --- a/1-js/02-first-steps/02-structure/article.md +++ b/1-js/02-first-steps/02-structure/article.md @@ -25,7 +25,7 @@ alert('Мир'); ## Точка с запятой [#semicolon] -В большинстве случаев точку с запятой можно опустить, если есть переход на новую строку. +В большинстве случаев точку с запятой можно не ставить, если есть переход на новую строку. Так тоже будет работать: @@ -34,11 +34,11 @@ alert('Привет') alert('Мир') ``` -В этом случае JavaScript интерпретирует перенос строки, как "неявную" точку с запятой. Это называется [автоматическая вставка точки с запятой](https://tc39.github.io/ecma262/#sec-automatic-semicolon-insertion). +В этом случае JavaScript интерпретирует перенос строки как "неявную" точку с запятой. Это называется [автоматическая вставка точки с запятой](https://tc39.github.io/ecma262/#sec-automatic-semicolon-insertion). **В большинстве случаев новая строка подразумевает точку с запятой. Но "в большинстве случаев" не значит "всегда"!** -В некоторых ситуациях новая строка не означает точку с запятой. Например: +В некоторых ситуациях новая строка всё же не означает точку с запятой. Например: ```js run no-beautify alert(3 + @@ -56,41 +56,39 @@ alert(3 + Если вы хотите увидеть конкретный пример такой ошибки, обратите внимание на этот код: ```js run -[1, 2].forEach(alert) +alert('Hello'); + +[1, 2].forEach(alert); ``` -Пока нет необходимости знать значение скобок `[]` и `forEach`. Мы изучим их позже. Пока что просто запомните результат выполнения этого кода: выводится `1`, а затем `2`. +Пока нет необходимости знать значение скобок `[]` и `forEach`. Мы изучим их позже. Пока что просто запомните результат выполнения этого кода: выводится `Hello`, затем `1`, затем `2`. -А теперь добавим `alert` перед кодом и *не* поставим в конце точку с запятой: +А теперь давайте уберем точку с запятой после `alert`: ```js run no-beautify -alert("Сейчас будет ошибка") +alert('Hello') -[1, 2].forEach(alert) +[1, 2].forEach(alert); ``` -Теперь, если запустить код, выведется только первый `alert`, а затем мы получим ошибку! - -Всё исправится, если мы поставим точку с запятой после `alert`: -```js run -alert("Теперь всё в порядке"); - -[1, 2].forEach(alert) -``` +Этот код отличается от кода, приведенного выше, только в одном: пропала точка с запятой в конце первой строки. -Теперь мы получим сообщение "Теперь всё в порядке", следом за которым будут `1` и `2`. +Если мы запустим этот код, выведется только первый `alert`, а затем мы получим ошибку (вам может потребоваться открыть консоль, чтобы увидеть её)! +Это потому что JavaScript не вставляет точку с запятой перед квадратными скобками `[...]`. И поэтому код в последнем примере выполняется, как одна инструкция. -В первом примере без точки с запятой возникает ошибка, потому что JavaScript не вставляет точку с запятой перед квадратными скобками `[...]`. И поэтому код в первом примере выполняется, как одна инструкция. Вот, как движок видит его: +Вот как движок видит его: ```js run no-beautify -alert("Сейчас будет ошибка")[1, 2].forEach(alert) +alert('Hello')[1, 2].forEach(alert); ``` -Но это должны быть две отдельные инструкции, а не одна. Такое слияние в данном случае неправильное, оттого и ошибка. Это может произойти и в некоторых других ситуациях. +Выглядит странно, правда? Такое слияние в данном случае неправильное. Мы должны поставить точку с запятой после `alert`, чтобы код работал правильно. + +Это может произойти и в некоторых других ситуациях. ```` -Мы рекомендуем ставить точку с запятой между инструкциями, даже если они отделены переносами строк. Это правило широко используется в сообществе разработчиков. Стоит отметить ещё раз -- в большинстве случаев *можно* опускать точку с запятой. Но безопаснее, особенно для новичка -- ставить её. +Мы рекомендуем ставить точку с запятой между инструкциями, даже если они отделены переносами строк. Это правило широко используется в сообществе разработчиков. Стоит отметить ещё раз -- в большинстве случаев *можно* не ставить точку с запятой. Но безопаснее, особенно для новичка, ставить её. ## Комментарии @@ -100,7 +98,7 @@ alert("Сейчас будет ошибка")[1, 2].forEach(alert) **Однострочные комментарии начинаются с двойной косой черты `//`.** -Часть строки после `//` считается комментарием. Такой комментарий может, как занимать строку целиком, так и находиться после инструкции. +Часть строки после `//` считается комментарием. Такой комментарий может как занимать строку целиком, так и находиться после инструкции. Как здесь: @@ -123,7 +121,7 @@ alert('Привет'); alert('Мир'); ``` -Содержимое комментария игнорируется, поэтому, если мы поместим внутри код /* ... */, он не будет исполняться. +Содержимое комментария игнорируется, поэтому, если мы поместим код внутри /* ... */, он не будет исполняться. Это бывает удобно для временного отключения участка кода: @@ -135,7 +133,7 @@ alert('Мир'); ``` ```smart header="Используйте горячие клавиши!" -В большинстве редакторов строку кода можно закомментировать, нажав комбинацию клавиш `key:Ctrl+/` для однострочного комментария и что-то, вроде `key:Ctrl+Shift+/` -- для многострочных комментариев (выделите кусок кода и нажмите комбинацию клавиш). В системе Mac попробуйте `key:Cmd` вместо `key:Ctrl`. +В большинстве редакторов строку кода можно закомментировать, нажав комбинацию клавиш `key:Ctrl+/` для однострочного комментария и что-то вроде `key:Ctrl+Shift+/` -- для многострочных комментариев (выделите кусок кода и нажмите комбинацию клавиш). В системе Mac попробуйте `key:Cmd` вместо `key:Ctrl` и `key:Option` вместо `key:Shift`. ``` ````warn header="Вложенные комментарии не поддерживаются!" diff --git a/1-js/02-first-steps/03-strict-mode/article.md b/1-js/02-first-steps/03-strict-mode/article.md index 7199addb1e..15290f9dd4 100644 --- a/1-js/02-first-steps/03-strict-mode/article.md +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -4,7 +4,7 @@ Преимуществом данного подхода было то, что существующий код продолжал работать. А недостатком -- что любая ошибка или несовершенное решение, принятое создателями JavaScript, застревали в языке навсегда. -Так было до 2009 года, когда появился ECMAScript 5 (ES5). Он добавил новые возможности в язык и изменил некоторые из существующих. Чтобы старый код работал, большинство таких модификаций по умолчанию отключены. Вы должны явно включить их с помощью специальной директивы: `"use strict"`. +Так было до 2009 года, когда появился ECMAScript 5 (ES5). Он добавил новые возможности в язык и изменил некоторые из существующих. Чтобы устаревший код работал, как и раньше, по умолчанию подобные изменения не применяются. Поэтому нам нужно явно их активировать с помощью специальной директивы: `"use strict"`. ## "use strict" @@ -19,7 +19,7 @@ ... ``` -Позже мы изучим функции (способ группировки команд). Забегая вперёд, заметим, что вместо всего скрипта `"use strict"` можно поставить в начале большинства видов функций. Это позволяет включить строгий режим только в конкретной функции. Но обычно люди используют его для всего файла. +Совсем скоро мы начнём изучать функции (способ группировки команд), поэтому заранее отметим, что в начале большинства видов функций можно поставить `"use strict"`. Это позволяет включить строгий режим только в конкретной функции. Но обычно люди используют его для всего файла. ````warn header="Убедитесь, что \"use strict\" находится в начале" @@ -40,17 +40,19 @@ alert("some code"); ```` ```warn header="Нет никакого способа отменить `use strict`" -Нет директивы типа `"no use strict"`, которая возвращает движок к старому поведению. +Нет директивы типа `"no use strict"`, которая возвращала бы движок к старому поведению. Как только мы входим в строгий режим, отменить это невозможно. ``` ## Консоль браузера -В будущем, когда вы будете использовать консоль браузера для тестирования функций, обратите внимание, что `use strict` по умолчанию в ней выключен. +В дальнейшем, когда вы будете использовать [консоль браузера](info:devtools) для тестирования функций, обратите внимание, что `use strict` по умолчанию в ней выключен. Иногда, когда `use strict` имеет значение, вы можете получить неправильные результаты. +Итак, как можно включить `use strict` в консоли? + Можно использовать `key:Shift+Enter` для ввода нескольких строк и написать в верхней строке `use strict`: ```js @@ -61,7 +63,7 @@ alert("some code"); В большинстве браузеров, включая Chrome и Firefox, это работает. -В старых браузерах консоль не учитывает такой `use strict`, там можно "оборачивать" код в функцию, вот так: +Если этого не происходит, например, в старом браузере, есть некрасивый, но надежный способ обеспечить `use strict`. Поместите его в следующую обёртку: ```js (function() { @@ -71,15 +73,18 @@ alert("some code"); })() ``` -## Всегда используйте "use strict" +## Всегда ли нужно использовать "use strict"? + +Вопрос кажется риторическим, но это не так. + +Кто-то посоветует начинать каждый скрипт с `"use strict"`... Но есть способ покруче. + +Современный JavaScript поддерживает "классы" и "модули" — продвинутые структуры языка (и мы, конечно, до них доберёмся), которые автоматически включают строгий режим. Поэтому в них нет нужды добавлять директиву `"use strict"`. -Нам ещё предстоит рассмотреть различия между строгим режимом и режимом "по умолчанию". +**Подытожим: пока очень желательно добавлять `"use strict";` в начале ваших скриптов. Позже, когда весь ваш код будет состоять из классов и модулей, директиву можно будет опускать.** -В следующих главах, изучая особенности языка, мы будем отмечать различия между строгим и стандартным режимами. К счастью, их не так много, и они действительно делают нашу жизнь лучше. +Пока мы узнали о `use strict` только в общих чертах. -На данный момент достаточно иметь общее понимание об этом режиме: +В следующих главах, по мере расширения знаний о возможностях языка, мы яснее увидим отличия между строгим и стандартным режимом. К счастью, их не так много, и все они делают жизнь разработчика лучше. -1. Директива `"use strict"` переключает движок в "современный" режим, изменяя поведение некоторых встроенных функций. Позже в учебнике мы увидим подробности. -2. Строгий режим включается путём размещения `"use strict"` в начале скрипта или функции. Некоторые функции языка, такие как "классы" и "модули", автоматически включают строгий режим. -3. Строгий режим поддерживается всеми современными браузерами. -4. Мы рекомендуем всегда начинать скрипты с `"use strict"`. Все примеры в этом руководстве предполагают строгий режим, если (очень редко) не указано иное. +Все примеры в этом учебнике подразумевают исполнение в строгом режиме, за исключением случаев (очень редких), когда оговорено иное. diff --git a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md index bfb027d285..d9da22ea3b 100644 --- a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md +++ b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md @@ -1,4 +1,4 @@ -## Переменная для названия нашей планеты. +## Переменная для названия нашей планеты Например: diff --git a/1-js/02-first-steps/04-variables/article.md b/1-js/02-first-steps/04-variables/article.md index 930ea33403..b1bdebc933 100644 --- a/1-js/02-first-steps/04-variables/article.md +++ b/1-js/02-first-steps/04-variables/article.md @@ -1,7 +1,7 @@ # Переменные JavaScript-приложению обычно нужно работать с информацией. Например: -1. Интернет магазин -- информация может включать продаваемые товары и корзину покупок. +1. Интернет-магазин -- информация может включать продаваемые товары и корзину покупок. 2. Чат -- информация может включать пользователей, сообщения и многое другое. Переменные используются для хранения этой информации. @@ -10,21 +10,21 @@ JavaScript-приложению обычно нужно работать с ин [Переменная](https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)) -- это "именованное хранилище" для данных. Мы можем использовать переменные для хранения товаров, посетителей и других данных. -Для создания переменной в JavaScript, используйте ключевое слово `let`. +Для создания переменной в JavaScript используйте ключевое слово `let`. -Приведённая ниже инструкция создаёт (другими словами: *объявляет* или *определяет*) переменную с именем "message": +Приведённая ниже инструкция создаёт (другими словами, *объявляет*) переменную с именем "message": ```js let message; ``` -Теперь можно поместить в неё данные, используя оператор присваивания `=`: +Теперь можно поместить в неё данные (другими словами, *определить переменную*), используя оператор присваивания `=`: ```js let message; *!* -message = 'Hello'; // сохранить строку +message = 'Hello'; // сохранить строку 'Hello' в переменной с именем message */!* ``` @@ -53,8 +53,7 @@ alert(message); // Hello! let user = 'John', age = 25, message = 'Hello'; ``` -Такой способ может показаться короче, но мы не рекомендуем его. -Для лучшей читаемости объявляйте каждую переменную на новой строке. +Такой способ может показаться короче, но мы не рекомендуем его. Для лучшей читаемости объявляйте каждую переменную на новой строке. Многострочный вариант немного длиннее, но легче для чтения: @@ -65,6 +64,7 @@ let message = 'Hello'; ``` Некоторые люди также определяют несколько переменных в таком вот многострочном стиле: + ```js no-beautify let user = 'John', age = 25, @@ -81,7 +81,6 @@ let user = 'John' В принципе, все эти варианты работают одинаково. Так что это вопрос личного вкуса и эстетики. - ````smart header="`var` вместо `let`" В старых скриптах вы также можете найти другое ключевое слово: `var` вместо `let`: @@ -91,21 +90,21 @@ let user = 'John' Ключевое слово `var` - *почти* то же самое, что и `let`. Оно объявляет переменную, но немного по-другому, "устаревшим" способом. -Есть тонкие различия между `let` и `var`, но они пока не имеют для нас значения. -Мы подробно рассмотрим их в этой главе . +Есть тонкие различия между `let` и `var`, но они пока не имеют для нас значения. Мы подробно рассмотрим их в главе . ```` ## Аналогия из жизни Мы легко поймём концепцию "переменной", если представим её в виде "коробки" для данных с уникальным названием на ней. -Например, переменную `message` можно представить, как коробку с названием `"message"` и значением `"Hello!"` внутри: +Например, переменную `message` можно представить как коробку с названием `"message"` и значением `"Hello!"` внутри: ![](variable.svg) Мы можем положить любое значение в коробку. Мы также можем изменить его столько раз, сколько захотим: + ```js run let message; @@ -137,14 +136,26 @@ alert(hello); // Hello world! alert(message); // Hello world! ``` +````warn header="Повторное объявление вызывает ошибку" +Переменная может быть объявлена только один раз. + +Повторное объявление той же переменной является ошибкой: + +```js run +let message = "Это"; + +// повторение ключевого слова 'let' приводит к ошибке +let message = "Другое"; // SyntaxError: 'message' has already been declared +``` +Поэтому следует объявлять переменную только один раз и затем использовать её уже без `let`. +```` + ```smart header="Функциональные языки программирования" -Примечательно, что существуют [функциональные](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) языки программирования, такие как [Scala](http://www.scala-lang.org/) или [Erlang](http://www.erlang.org/), которые запрещают изменять значение переменной. +Примечательно, что существуют [функциональные](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) языки программирования, такие как [Scala](https://www.scala-lang.org/) или [Erlang](https://www.erlang.org/), которые запрещают изменять значение переменной. В таких языках однажды сохранённое "в коробку" значение остаётся там навсегда. Если нам нужно сохранить что-то другое, язык заставляет нас создать новую коробку (объявить новую переменную). Мы не можем использовать старую переменную. -Хотя на первый взгляд это может показаться немного странным, эти языки вполне подходят для серьёзной разработки. -Более того, есть такая область, как параллельные вычисления, где это ограничение даёт определённые преимущества. -Изучение такого языка (даже если вы не планируете использовать его в ближайшее время) рекомендуется для расширения кругозора. +Хотя на первый взгляд это может показаться немного странным, эти языки вполне подходят для серьёзной разработки. Более того, есть такая область, как параллельные вычисления, где это ограничение даёт определённые преимущества. Изучение такого языка (даже если вы не планируете использовать его в ближайшее время) рекомендуется для расширения кругозора. ``` ## Имена переменных [#variable-naming] @@ -161,8 +172,7 @@ let userName; let test123; ``` -Если имя содержит несколько слов, обычно используется [верблюжья нотация](https://ru.wikipedia.org/wiki/CamelCase), -то есть, слова следуют одно за другим, где каждое следующее слово начинается с заглавной буквы: `myVeryLongName`. +Если имя содержит несколько слов, обычно используется [верблюжья нотация](https://ru.wikipedia.org/wiki/CamelCase), то есть, слова следуют одно за другим, где каждое следующее слово начинается с заглавной буквы: `myVeryLongName`. Самое интересное -- знак доллара `'$'` и подчёркивание `'_'` также можно использовать в названиях. Это обычные символы, как и буквы, без какого-либо особого значения. @@ -184,10 +194,10 @@ let my-name; // дефис '-' не разрешён в имени ``` ```smart header="Регистр имеет значение" -Переменные с именами `apple` and `AppLE` -- это две разные переменные. +Переменные с именами `apple` и `APPLE` -- это две разные переменные. ``` -````smart header="Не-латинские буквы разрешены, но не рекомендуются" +````smart header="Нелатинские буквы разрешены, но не рекомендуются" Можно использовать любой язык, включая кириллицу или даже иероглифы, например: ```js @@ -199,7 +209,7 @@ let 我 = '...'; ```` ````warn header="Зарезервированные имена" -Существует [список зарезервированных слов](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Lexical_grammar#%D0%9A%D0%BB%D1%8E%D1%87%D0%B5%D0%B2%D1%8B%D0%B5_%D1%81%D0%BB%D0%BE%D0%B2%D0%B0), которые нельзя использовать в качестве имён переменных, потому что они используются самим языком. +Существует [список зарезервированных слов](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Lexical_grammar#%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B2%D1%8B%D0%B5_%D1%81%D0%BB%D0%BE%D0%B2%D0%B0), которые нельзя использовать в качестве имён переменных, потому что они используются самим языком. Например: `let`, `class`, `return` и `function` зарезервированы. @@ -223,13 +233,13 @@ num = 5; // если переменная "num" раньше не существ alert(num); // 5 ``` -Это плохая практика и приведёт к ошибке в строгом режиме: +Это плохая практика, которая приводит к ошибке в строгом режиме: ```js "use strict"; *!* -num = 5; // error: num is not defined +num = 5; // ошибка: num is not defined */!* ``` ```` @@ -242,7 +252,7 @@ num = 5; // error: num is not defined const myBirthday = '18.04.1982'; ``` -Переменные объявленные с помощью `const`, называются "константами". Их нельзя изменить. Попытка сделать это приведёт к ошибке: +Переменные, объявленные с помощью `const`, называются "константами". Их нельзя изменить. Попытка сделать это приведёт к ошибке: ```js run const myBirthday = '18.04.1982'; @@ -258,7 +268,7 @@ myBirthday = '01.01.2001'; // ошибка, константу нельзя пе Названия таких констант пишутся с использованием заглавных букв и подчёркивания. -Для примера, сделаем константы для различных цветов в "шестнадцатеричном формате": +Например, сделаем константы для различных цветов в "шестнадцатеричном формате": ```js run const COLOR_RED = "#F00"; @@ -275,20 +285,21 @@ alert(color); // #FF7F00 - `COLOR_ORANGE` гораздо легче запомнить, чем `"#FF7F00"`. - Гораздо легче допустить ошибку при вводе `"#FF7F00"`, чем при вводе `COLOR_ORANGE`. -- При чтении кода, `COLOR_ORANGE` намного понятнее, чем `#FF7F00`. +- При чтении кода `COLOR_ORANGE` намного понятнее, чем `#FF7F00`. Когда мы должны использовать для констант заглавные буквы, а когда называть их нормально? Давайте разберёмся и с этим. Название "константа" просто означает, что значение переменной никогда не меняется. Но есть константы, которые известны до выполнения (например, шестнадцатеричное значение для красного цвета), а есть константы, которые *вычисляются* во время выполнения сценария, но не изменяются после их первоначального назначения. Например: + ```js const pageLoadTime = /* время, потраченное на загрузку веб-страницы */; ``` -Значение `pageLoadTime` неизвестно до загрузки страницы, поэтому её имя записано обычными, а не прописными буквами. Но это все ещё константа, потому что она не изменяется после назначения. +Значение `pageLoadTime` неизвестно до загрузки страницы, поэтому её имя записано обычными, а не прописными буквами. Но это всё ещё константа, потому что она не изменяется после назначения. -Другими словами, константы с именами, записанными заглавными буквами, используются только, как псевдонимы для "жёстко закодированных" значений. +Другими словами, константы с именами, записанными заглавными буквами, используются только как псевдонимы для "жёстко закодированных" значений. ## Придумывайте правильные имена @@ -307,7 +318,7 @@ const pageLoadTime = /* время, потраченное на загрузку - Используйте легко читаемые имена, такие как `userName` или `shoppingCart`. - Избегайте использования аббревиатур или коротких имён, таких как `a`, `b`, `c`, за исключением тех случаев, когда вы точно знаете, что так нужно. - Делайте имена максимально описательными и лаконичными. Примеры плохих имён: `data` и `value`. Такие имена ничего не говорят. Их можно использовать только в том случае, если из контекста кода очевидно, какие данные хранит переменная. -- Договоритесь с вашей командой о используемых терминах. Если посетитель сайта называется "user" тогда мы должны назвать связанные с ним переменные `currentUser` или `newUser` вместо того, чтобы называть их `currentVisitor` или `newManInTown`. +- Договоритесь с вашей командой об используемых терминах. Если посетитель сайта называется "user", тогда мы должны называть связанные с ним переменные `currentUser` или `newUser`, а не, к примеру, `currentVisitor` или `newManInTown`. Звучит просто? Действительно, это так, но на практике для создания описательных и кратких имён переменных зачастую требуется подумать. Действуйте. @@ -328,7 +339,7 @@ const pageLoadTime = /* время, потраченное на загрузку Мы можем объявить переменные для хранения данных с помощью ключевых слов `var`, `let` или `const`. - `let` -- это современный способ объявления. -- `var` -- это устаревший способ объявления. Обычно мы вообще не используем его, но мы рассмотрим тонкие отличия от `let` в главе , на случай, если это всё-таки вам понадобится. -- `const` -- похож на `let`, но значение переменной не может изменяться. +- `var` -- это устаревший способ объявления. Обычно мы вообще не используем его, но мы рассмотрим тонкие отличия от `let` в главе на случай, если это всё-таки вам понадобится. +- `const` -- похоже на `let`, но значение переменной не может изменяться. Переменные должны быть названы таким образом, чтобы мы могли легко понять, что у них внутри. diff --git a/1-js/02-first-steps/04-variables/variable-change.svg b/1-js/02-first-steps/04-variables/variable-change.svg index 427a6388c6..1b26792380 100644 --- a/1-js/02-first-steps/04-variables/variable-change.svg +++ b/1-js/02-first-steps/04-variables/variable-change.svg @@ -1 +1 @@ -"World!""Hello!"message \ No newline at end of file +"World!""Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/variable.svg b/1-js/02-first-steps/04-variables/variable.svg index 5d15c9e4e4..1c3d8b0cbe 100644 --- a/1-js/02-first-steps/04-variables/variable.svg +++ b/1-js/02-first-steps/04-variables/variable.svg @@ -1 +1 @@ -"Hello!"message \ No newline at end of file +"Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md index a25484f323..5410236e3c 100644 --- a/1-js/02-first-steps/05-types/article.md +++ b/1-js/02-first-steps/05-types/article.md @@ -1,5 +1,9 @@ # Типы данных +Значение в JavaScript всегда относится к данным определённого типа. Например, это может быть строка или число. + +Есть восемь основных типов данных в JavaScript. В этой главе мы рассмотрим их в общем, а в следующих главах поговорим подробнее о каждом. + Переменная в JavaScript может содержать любые данные. В один момент там может быть строка, а в другой - число: ```js @@ -10,8 +14,6 @@ message = 123456; Языки программирования, в которых такое возможно, называются "динамически типизированными". Это значит, что типы данных есть, но переменные не привязаны ни к одному из них. -Есть семь основных типов данных в JavaScript. В этой главе мы рассмотрим их в общем, а в следующих главах поговорим подробнее о каждом. - ## Число ```js @@ -44,16 +46,18 @@ n = 12.345; alert( "не число" / 2 ); // NaN, такое деление является ошибкой ``` - Значение `NaN` "прилипчиво". Любая операция с `NaN` возвращает `NaN`: + Значение `NaN` "прилипчиво". Любая математическая операция с `NaN` возвращает `NaN`: ```js run - alert( "не число" / 2 + 5 ); // NaN + alert( NaN + 1 ); // NaN + alert( 3 * NaN ); // NaN + alert( "не число" / 2 - 1 ); // NaN ``` - Если где-то в математическом выражении есть `NaN`, то результатом вычислений с его участием будет `NaN`. + Если где-то в математическом выражении есть `NaN`, то оно распространяется на весь результат (есть только одно исключение: `NaN ** 0` равно `1`). ```smart header="Математические операции -- безопасны" -Математические операции в JavaScript "безопасны". Мы можем делать что угодно: делить на ноль, обращаться со строками как с числами и т.д. +Математические операции в JavaScript "безопасны". Мы можем делать что угодно: делить на ноль, обращаться с нечисловыми строками как с числами и т.д. Скрипт никогда не остановится с фатальной ошибкой (не "умрёт"). В худшем случае мы получим `NaN` как результат выполнения. ``` @@ -62,6 +66,38 @@ n = 12.345; Подробнее о работе с числами мы поговорим в главе . +## BigInt + +В JavaScript тип `number` не может безопасно работать с числами, большими, чем (253-1) (т. е. `9007199254740991`) или меньшими, чем -(253-1) для отрицательных чисел. + +Если говорить совсем точно, то, технически, тип `number` *может* хранить большие целые числа (до 1.7976931348623157 * 10308), но за пределами безопасного диапазона целых чисел ±(253-1) будет ошибка точности, так как не все цифры помещаются в фиксированную 64-битную память. Поэтому можно хранить "приблизительное" значение. + +Например, эти два числа (прямо за пределами безопасного диапазона) совпадают: + +```js +console.log(9007199254740991 + 1); // 9007199254740992 +console.log(9007199254740991 + 2); // 9007199254740992 +``` + +То есть все нечетные целые числа, большие чем (253-1), вообще не могут храниться в типе `number`. + +В большинстве случаев безопасного диапазона чисел от -(253-1) до (253-1) вполне достаточно, но иногда нам требуется весь диапазон действительно гигантских целых чисел без каких-либо ограничений или пропущенных значений внутри него. Например, в криптографии или при использовании метки времени («timestamp») с микросекундами. + +Тип `BigInt` был добавлен в JavaScript, чтобы дать возможность работать с целыми числами произвольной длины. + +Чтобы создать значение типа `BigInt`, необходимо добавить `n` в конец числового литерала: + +```js +// символ "n" в конце означает, что это BigInt +const bigInt = 1234567890123456789012345678901234567890n; +``` + +Так как необходимость в использовании `BigInt`--чисел появляется достаточно редко, мы рассмотрим их в отдельной главе . Ознакомьтесь с ней, когда вам понадобятся настолько большие числа. + +```smart header="Поддержка" +В данный момент `BigInt` поддерживается только в браузерах Firefox, Chrome, Edge и Safari, но не поддерживается в IE. +``` + ## Строка Строка (`string`) в JavaScript должна быть заключена в кавычки. @@ -80,7 +116,7 @@ let phrase = `Обратные кавычки позволяют встраив Двойные или одинарные кавычки являются "простыми", между ними нет разницы в JavaScript. -Обратные кавычки же имеют "расширенный функционал". Они позволяют нам встраивать выражения в строку, заключая их в `${…}`. Например: +Обратные же кавычки имеют расширенную функциональность. Они позволяют нам встраивать выражения в строку, заключая их в `${…}`. Например: ```js run let name = "Иван"; @@ -92,19 +128,19 @@ alert( `Привет, *!*${name}*/!*!` ); // Привет, Иван! alert( `результат: *!*${1 + 2}*/!*` ); // результат: 3 ``` -Выражение внутри `${…}` вычисляется, и его результат становится частью строки. Мы можем положить туда всё, что угодно: переменную `name` или выражение `1 + 2`, или что-то более сложное. +Выражение внутри `${…}` вычисляется, и его результат становится частью строки. Мы можем положить туда всё, что угодно: переменную `name`, или выражение `1 + 2`, или что-то более сложное. -Обратите внимание, что это можно делать только в обратных кавычках. Другие кавычки не имеют такого функционала встраивания! +Обратите внимание, что это можно делать только в обратных кавычках. Другие кавычки не имеют такой функциональности встраивания! ```js run alert( "результат: ${1 + 2}" ); // результат: ${1 + 2} (двойные кавычки ничего не делают) ``` Мы рассмотрим строки более подробно в главе . -```smart header="Нет *символьного* типа данных." -В некоторых языках для одного символа существует специальный "символьный" тип. Например, в C и Java это `char`. +```smart header="Нет отдельного типа данных для одного символа." +В некоторых языках, например C и Java, для хранения одного символа, например `"a"` или `"%"`, существует отдельный тип. В языках C и Java это `char`. -В JavaScript подобного типа нет, есть только тип `string`. Строка может содержать один символ или множество. +В JavaScript подобного типа нет, есть только тип `string`. Строка может содержать ноль символов (быть пустой), один символ или множество. ``` ## Булевый (логический) тип @@ -144,7 +180,7 @@ let age = null; Это просто специальное значение, которое представляет собой "ничего", "пусто" или "значение неизвестно". -В приведённом выше коде указано, что переменная `age` неизвестна или не имеет значения по какой-то причине. +В приведённом выше коде указано, что значение переменной `age` неизвестно. ## Значение "undefined" @@ -155,19 +191,20 @@ let age = null; Если переменная объявлена, но ей не присвоено никакого значения, то её значением будет `undefined`: ```js run -let x; +let age; -alert(x); // выведет "undefined" +alert(age); // выведет "undefined" ``` Технически мы можем присвоить значение `undefined` любой переменной: ```js run -let x = 123; +let age = 123; -x = undefined; +// изменяем значение на undefined +age = undefined; -alert(x); // "undefined" +alert(age); // "undefined" ``` ...Но так делать не рекомендуется. Обычно `null` используется для присвоения переменной "пустого" или "неизвестного" значения, а `undefined` -- для проверок, была ли переменная назначена. @@ -176,20 +213,32 @@ alert(x); // "undefined" Тип `object` (объект) -- особенный. -Все остальные типы называются "примитивными", потому что их значениями могут быть только простые значения (будь то строка или число, или что-то ещё). Объекты же используются для хранения коллекций данных или более сложных объектов. Мы разберёмся с ними позднее в главе после того, как узнаем больше о примитивах. +Все остальные типы называются "примитивными", потому что их значениями могут быть только простые значения (будь то строка, или число, или что-то ещё). В объектах же хранят коллекции данных или более сложные структуры. + +Объекты занимают важное место в языке и требуют особого внимания. Мы разберёмся с ними в главе после того, как узнаем больше о примитивах. -Тип `symbol` (символ) используется для создания уникальных идентификаторов объектов. Мы упоминаем здесь о нём для полноты картины, изучим этот тип после объектов. +Тип `symbol` (символ) используется для создания уникальных идентификаторов в объектах. Мы упоминаем здесь о нём для полноты картины, изучим этот тип после объектов. ## Оператор typeof [#type-typeof] Оператор `typeof` возвращает тип аргумента. Это полезно, когда мы хотим обрабатывать значения различных типов по-разному или просто хотим сделать проверку. -У него есть два синтаксиса: +У него есть две синтаксические формы: -1. Синтаксис оператора: `typeof x`. -2. Синтаксис функции: `typeof(x)`. +```js +// Обычный синтаксис +typeof 5 // Выведет "number" +// Синтаксис, напоминающий вызов функции (встречается реже) +typeof(5) // Также выведет "number" +``` -Другими словами, он работает со скобками или без скобок. Результат одинаковый. +Если передается выражение, то нужно заключать его в скобки, т.к. typeof имеет более высокий приоритет, чем бинарные операторы: + +```js +typeof 50 + " Квартир"; // Выведет "number Квартир" +typeof (50 + " Квартир"); // Выведет "string" +``` +Другими словами, скобки необходимы для определения типа значения, которое получилось в результате выполнения выражения в них. Вызов `typeof x` возвращает строку с именем типа: @@ -198,6 +247,8 @@ typeof undefined // "undefined" typeof 0 // "number" +typeof 10n // "bigint" + typeof true // "boolean" typeof "foo" // "string" @@ -219,22 +270,25 @@ typeof alert // "function" (3) Последние три строки нуждаются в пояснении: -1. `Math` - это встроенный объект, который предоставляет математические операции и константы. Мы рассмотрим его подробнее в главе . Здесь он служит лишь примером объекта. -2. Результатом вызова `typeof null` является `"object"`. Это неверно. Это официально признанная ошибка в `typeof`, сохранённая для совместимости. Конечно, `null` не является объектом. Это специальное значение с отдельным типом. Повторимся, это ошибка в языке. -3. Вызов `typeof alert` возвращает `"function"`, потому что `alert` является функцией. Мы изучим функции в следующих главах, где заодно увидим, что в JavaScript нет специального типа "функция". Функции относятся к объектному типу. Но `typeof` обрабатывает их особым образом, возвращая `"function"`. Формально это неверно, но очень удобно на практике. +1. `Math` — это встроенный объект, который предоставляет математические операции и константы. Мы рассмотрим его подробнее в главе . Здесь он служит лишь примером объекта. +2. Результатом вызова `typeof null` является `"object"`. Это официально признанная ошибка в `typeof`, ведущая начало с времён создания JavaScript и сохранённая для совместимости. Конечно, `null` не является объектом. Это специальное значение с отдельным типом. +3. Вызов `typeof alert` возвращает `"function"`, потому что `alert` является функцией. Мы изучим функции в следующих главах, где заодно увидим, что в JavaScript нет специального типа "функция". Функции относятся к объектному типу. Но `typeof` обрабатывает их особым образом, возвращая `"function"`. Так тоже повелось от создания JavaScript. Формально это неверно, но может быть удобным на практике. ## Итого -В JavaScript есть 7 основных типов. - -- `number` для любых чисел: целочисленных или чисел с плавающей точкой. -- `string` для строк. Строка может содержать один или больше символов, нет отдельного символьного типа. -- `boolean` для `true`/`false`. -- `null` для неизвестных значений -- отдельный тип, имеющий одно значение `null`. -- `undefined` для неприсвоенных значений -- отдельный тип, имеющий одно значение `undefined`. -- `object` для более сложных структур данных. -- `symbol` для уникальных идентификаторов. +В JavaScript есть 8 основных типов данных. + +- Семь из них называют «примитивными» типами данных: + - `number` для любых чисел: целочисленных или чисел с плавающей точкой; целочисленные значения ограничены диапазоном ±(253-1). + - `bigint` для целых чисел произвольной длины. + - `string` для строк. Строка может содержать ноль или больше символов, нет отдельного символьного типа. + - `boolean` для `true`/`false`. + - `null` для неизвестных значений -- отдельный тип, имеющий одно значение `null`. + - `undefined` для неприсвоенных значений -- отдельный тип, имеющий одно значение `undefined`. + - `symbol` для уникальных идентификаторов. +- И один не является «примитивным» и стоит особняком: + - `object` для более сложных структур данных. Оператор `typeof` позволяет нам увидеть, какой тип данных сохранён в переменной. diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/article.md b/1-js/02-first-steps/06-alert-prompt-confirm/article.md similarity index 60% rename from 1-js/02-first-steps/09-alert-prompt-confirm/article.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/article.md index 1931b522b4..6a0bce1713 100644 --- a/1-js/02-first-steps/09-alert-prompt-confirm/article.md +++ b/1-js/02-first-steps/06-alert-prompt-confirm/article.md @@ -1,19 +1,10 @@ # Взаимодействие: alert, prompt, confirm -В этой части учебника мы разбираем "собственно JavaScript", без привязки к браузеру или другой среде выполнения. - -Но, так как мы будем использовать браузер как демо-среду, нам нужно познакомиться по крайней мере с несколькими функциями его интерфейса, а именно: `alert`, `prompt` и `confirm`. - +Так как мы будем использовать браузер как демо-среду, нам нужно познакомиться с несколькими функциями его интерфейса, а именно: `alert`, `prompt` и `confirm`. ## alert -Синтаксис: - -```js -alert(message); -``` - -Этот код отобразит окно в браузере и приостановит дальнейшее выполнение скриптов до тех пор, пока пользователь не нажмёт кнопку "OK". +С этой функцией мы уже знакомы. Она показывает сообщение и ждёт, пока пользователь нажмёт кнопку "ОК". Например: @@ -21,7 +12,7 @@ alert(message); alert("Hello"); ``` -Это небольшое окно с сообщением называется *модальным окном*. Понятие *модальный* означает, что пользователь не может взаимодействовать с интерфейсом остальной части страницы, нажимать на другие кнопки и т.д. до тех пор, пока взаимодействует с окном. В данном случае -- пока не будет нажата кнопка "OK". +Это небольшое окно с сообщением называется *модальным окном*. Понятие *модальное* означает, что пользователь не может взаимодействовать с интерфейсом остальной части страницы, нажимать на другие кнопки и т.д. до тех пор, пока взаимодействует с окном. В данном случае -- пока не будет нажата кнопка "OK". ## prompt @@ -39,9 +30,13 @@ result = prompt(title, [default]); `default` : Необязательный второй параметр, который устанавливает начальное значение в поле для текста в окне. -Пользователь может напечатать что-либо в поле ввода и нажать OK. Он также может отменить ввод нажатием на кнопку "Отмена" или нажав на клавишу `key:Esc`. +```smart header="Квадратные скобки в синтаксисе `[...]`" +Квадратные скобки вокруг `default` в описанном выше синтаксисе означают, что параметр факультативный, необязательный. +``` + +Пользователь может напечатать что-либо в поле ввода и нажать OK. Введённый текст будет присвоен переменной `result`. Пользователь также может отменить ввод нажатием на кнопку "Отмена" или нажав на клавишу `key:Esc`. В этом случае значением `result` станет `null`. -Вызов `prompt` вернёт текст, указанный в поле для ввода, или `null`, если ввод отменён пользователем. +Вызов `prompt` возвращает текст, указанный в поле для ввода, или `null`, если ввод отменён пользователем. Например: @@ -52,7 +47,7 @@ alert(`Тебе ${age} лет!`); // Тебе 100 лет! ``` ````warn header="Для IE: всегда устанавливайте значение по умолчанию" -Второй параметр не обязательный, но если не указать его, то Internet Explorer установит значение `"undefined"` в поле для ввода. +Второй параметр является необязательным, но если не указать его, то Internet Explorer вставит строку `"undefined"` в поле для ввода. Запустите код в Internet Explorer и посмотрите на результат: @@ -77,14 +72,14 @@ result = confirm(question); Функция `confirm` отображает модальное окно с текстом вопроса `question` и двумя кнопками: OK и Отмена. -Результат `true`, если нажата кнопка OK. В других случаях `false`. +Результат -- `true`, если нажата кнопка OK. В других случаях -- `false`. Например: ```js run let isBoss = confirm("Ты здесь главный?"); -alert( isBoss ); // true если нажата OK +alert( isBoss ); // true, если нажата OK ``` ## Итого @@ -95,16 +90,16 @@ alert( isBoss ); // true если нажата OK : показывает сообщение. `prompt` -: показывает сообщение и запрашивает ввод текста от пользователя. Возвращает напечатанный текст в поле ввода или `null`, если была нажата кнопка "Отмена" или `key:Esc` с клавиатуры. +: показывает сообщение и запрашивает ввод текста от пользователя. Возвращает напечатанный в поле ввода текст или `null`, если была нажата кнопка "Отмена" или `key:Esc` с клавиатуры. `confirm` -: показывает сообщение и ждёт, пока пользователь нажмёт OK или Отмена. Возвращает `true`, если нажата OK и `false`, если нажата кнопка "Отмена" или `key:Esc` с клавиатуры. +: показывает сообщение и ждёт, пока пользователь нажмёт OK или Отмена. Возвращает `true`, если нажата OK, и `false`, если нажата кнопка "Отмена" или `key:Esc` с клавиатуры. Все эти методы являются модальными: останавливают выполнение скриптов и не позволяют пользователю взаимодействовать с остальной частью страницы до тех пор, пока окно не будет закрыто. -На все указанные методы распространяется два ограничения: +На все указанные методы распространяются два ограничения: 1. Расположение окон определяется браузером. Обычно окна находятся в центре. 2. Визуальное отображение окон зависит от браузера, и мы не можем изменить их вид. -Такова цена простоты. Есть другие способы показать более приятные глазу окна с богатым функционалом для взаимодействия с пользователем, но если "навороты" не имеют значения, то данные методы работают отлично. +Такова цена простоты. Есть другие способы показать более приятные глазу окна с богатой функциональностью для взаимодействия с пользователем, но если "навороты" не имеют значения, то данные методы работают отлично. diff --git a/1-js/02-first-steps/06-type-conversions/article.md b/1-js/02-first-steps/07-type-conversions/article.md similarity index 64% rename from 1-js/02-first-steps/06-type-conversions/article.md rename to 1-js/02-first-steps/07-type-conversions/article.md index 71f77ed8a9..bd413fa539 100644 --- a/1-js/02-first-steps/06-type-conversions/article.md +++ b/1-js/02-first-steps/07-type-conversions/article.md @@ -1,13 +1,15 @@ # Преобразование типов -Чаще всего, операторы и функции автоматически приводят переданные им значения к нужному типу. +Чаще всего операторы и функции автоматически приводят переданные им значения к нужному типу. -Например, `alert` автоматически преобразует любое значение к строке. Математические операторы преобразуют значения к числам. +Например, `alert` автоматически преобразует любое значение к строке. Математические операторы преобразуют значения к числам. Есть также случаи, когда нам нужно явно преобразовать значение в ожидаемый тип. ```smart header="Пока что мы не говорим об объектах" -В этой главе мы не касаемся объектов. Сначала мы разберём преобразование примитивных значений. Мы разберём преобразование объектов позже, в главе . +В этой главе мы не касаемся объектов. Сначала мы разберём преобразование примитивных значений. + +Мы разберём преобразование объектов позже, в главе . ``` ## Строковое преобразование @@ -28,7 +30,7 @@ alert(typeof value); // string */!* ``` -Преобразование происходит очевидным способом. `false` становится `"false"`, `null` становится `"null"` и т.п. +Преобразование происходит очевидным образом. `false` становится `"false"`, `null` становится `"null"` и т.п. ## Численное преобразование @@ -37,7 +39,7 @@ alert(typeof value); // string Например, когда операция деления `/` применяется не к числу: ```js run -alert( "6" / "2" ); // 3, Строки преобразуются в числа +alert( "6" / "2" ); // 3, строки преобразуются в числа ``` Мы можем использовать функцию `Number(value)`, чтобы явно преобразовать `value` к числу: @@ -51,14 +53,14 @@ let num = Number(str); // становится числом 123 alert(typeof num); // number ``` -Явноe преобразование часто применяется, когда мы ожидаем получить число из строковых источников, вроде форм текстового ввода. +Явное преобразование часто применяется, когда мы ожидаем получить число из строкового контекста, например из текстовых полей форм. Если строка не может быть явно приведена к числу, то результатом преобразования будет `NaN`. Например: ```js run let age = Number("Любая строка вместо числа"); -alert(age); // NaN, преобразование не удалось +alert(age); // NaN, преобразование не удалось ``` Правила численного преобразования: @@ -68,41 +70,30 @@ alert(age); // NaN, преобразование не удалось |`undefined`|`NaN`| |`null`|`0`| |true / false | `1` / `0` | -| `string` | Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то `0`, иначе из непустой строки "считывается" число. При ошибке результат `NaN`.| +| `string` | Пробельные символы (пробелы, знаки табуляции `\t`, знаки новой строки `\n` и т. п.) по краям обрезаются. Далее, если остаётся пустая строка, то получаем `0`, иначе из непустой строки "считывается" число. При ошибке результат `NaN`.| Примеры: ```js run alert( Number(" 123 ") ); // 123 -alert( Number("123z") ); // NaN (ошибка чтения числа в "z") +alert( Number("123z") ); // NaN (ошибка чтения числа на месте символа "z") alert( Number(true) ); // 1 alert( Number(false) ); // 0 ``` Учтите, что `null` и `undefined` ведут себя по-разному. Так, `null` становится нулём, тогда как `undefined` приводится к `NaN`. -````smart header="Сложение '+' объединяет строки" -Почти все математические операторы выполняют численное преобразование. Исключение составляет `+`. Если одно из слагаемых является строкой, тогда и все остальные приводятся к строкам. - -Тогда они конкатенируются (присоединяются) друг к другу: - -```js run -alert( 1 + '2' ); // '12' (строка справа) -alert( '1' + 2 ); // '12' (строка слева) -``` - -Так происходит, только если хотя бы один из аргументов является строкой. Во всех остальных случаях значения складываются как числа. -```` +Большинство математических операторов также производит данное преобразование, как мы увидим в следующей главе. ## Логическое преобразование Логическое преобразование самое простое. -Происходит в логических операторах (позже мы познакомимся с подобными конструкциями), но также может быть выполнено явно с помощью функции `Boolean(value)`. +Происходит в логических операциях (позже мы познакомимся с условными проверками и подобными конструкциями), но также может быть выполнено явно с помощью функции `Boolean(value)`. Правило преобразования: -- Значения, которые интуитивно "пустые", вроде `0`, пустой строки, `null`, `undefined`, и `NaN`, становятся `false`. +- Значения, которые интуитивно "пустые", вроде `0`, пустой строки, `null`, `undefined` и `NaN`, становятся `false`. - Все остальные значения становятся `true`. Например: @@ -115,7 +106,7 @@ alert( Boolean("Привет!") ); // true alert( Boolean("") ); // false ``` -````warn header="Заметим, что строчка с нулём `\"0\"` это `true`" +````warn header="Заметим, что строка с нулём `\"0\"` — это `true`" Некоторые языки (к примеру, PHP) воспринимают строку `"0"` как `false`. Но в JavaScript, если строка не пустая, то она всегда `true`. ```js run @@ -124,12 +115,11 @@ alert( Boolean(" ") ); // пробел это тоже true (любая непу ``` ```` - ## Итого -Существует 3 наиболее широко используемых преобразований: строковое, численное и логическое. +Существует 3 наиболее широко используемых преобразования: строковое, численное и логическое. -**`Строковое`** -- Происходит, когда там нужно что-то вывести. Может быть вызвано с помощью `String(value)`. Для примитивных значений работает очевидным образом. +**`Строковое`** -- Происходит, когда нам нужно что-то вывести. Может быть вызвано с помощью `String(value)`. Для примитивных значений работает очевидным образом. **`Численное`** -- Происходит в математических операциях. Может быть вызвано с помощью `Number(value)`. @@ -140,9 +130,9 @@ alert( Boolean(" ") ); // пробел это тоже true (любая непу |`undefined`|`NaN`| |`null`|`0`| |true / false | `1 / 0` | -| `string` | Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то `0`, иначе из непустой строки "считывается" число. При ошибке результат `NaN`.| +| `string` | Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то получаем `0`, иначе из непустой строки "считывается" число. При ошибке результат `NaN`.| -**`Логическое`** -- Происходит в логических операторах. Может быть вызвано с помощью `Boolean(value)`. +**`Логическое`** -- Происходит в логических операциях. Может быть вызвано с помощью `Boolean(value)`. Подчиняется правилам: @@ -155,6 +145,6 @@ alert( Boolean(" ") ); // пробел это тоже true (любая непу Большую часть из этих правил легко понять и запомнить. Особые случаи, в которых часто допускаются ошибки: - `undefined` при численном преобразовании становится `NaN`, не `0`. -- `"0"` и строки с одними пробелами вроде `" "` при логическом преобразовании всегда `true`. +- `"0"` и строки из одних пробелов типа `" "` при логическом преобразовании всегда `true`. В этой главе мы не говорили об объектах. Мы вернёмся к ним позже, в главе , посвящённой только объектам, сразу после того, как узнаем больше про основы JavaScript. diff --git a/1-js/02-first-steps/07-operators/1-increment-order/solution.md b/1-js/02-first-steps/08-operators/1-increment-order/solution.md similarity index 100% rename from 1-js/02-first-steps/07-operators/1-increment-order/solution.md rename to 1-js/02-first-steps/08-operators/1-increment-order/solution.md diff --git a/1-js/02-first-steps/07-operators/1-increment-order/task.md b/1-js/02-first-steps/08-operators/1-increment-order/task.md similarity index 100% rename from 1-js/02-first-steps/07-operators/1-increment-order/task.md rename to 1-js/02-first-steps/08-operators/1-increment-order/task.md diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/solution.md b/1-js/02-first-steps/08-operators/2-assignment-result/solution.md similarity index 100% rename from 1-js/02-first-steps/07-operators/2-assignment-result/solution.md rename to 1-js/02-first-steps/08-operators/2-assignment-result/solution.md diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/task.md b/1-js/02-first-steps/08-operators/2-assignment-result/task.md similarity index 72% rename from 1-js/02-first-steps/07-operators/2-assignment-result/task.md rename to 1-js/02-first-steps/08-operators/2-assignment-result/task.md index 2bbd9568b6..242266f946 100644 --- a/1-js/02-first-steps/07-operators/2-assignment-result/task.md +++ b/1-js/02-first-steps/08-operators/2-assignment-result/task.md @@ -4,7 +4,7 @@ importance: 3 # Результат присваивания -Чему будут равны переменные `a` и `x` в примере ниже? +Чему будут равны переменные `a` и `x` после исполнения кода в примере ниже? ```js let a = 2; diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md similarity index 73% rename from 1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md rename to 1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md index 2005116c06..24d683549d 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md @@ -9,18 +9,17 @@ true + false = 1 "$" + 4 + 5 = "$45" "4" - 2 = 2 "4px" - 2 = NaN -7 / 0 = Infinity -" -9 " + 5 = " -9 5" // (3) -" -9 " - 5 = -14 // (4) +" -9 " + 5 = " -9 5" // (3) +" -9 " - 5 = -14 // (4) null + 1 = 1 // (5) undefined + 1 = NaN // (6) " \t \n" - 2 = -2 // (7) ``` -1. Сложение со строкой `"" + 1` преобразует `1` к строке: `"" + 1 = "1"`, и в следующем случае `"1" + 0`, работает то же самое правило. +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` после численного преобразования. +7. Пробельные символы, такие как `\t` и `\n`, по краям строки игнорируются при преобразовании в число, так что строка `" \t \n"`, аналогично пустой строке, становится `0` после численного преобразования. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md similarity index 72% rename from 1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md rename to 1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md index 0dc68682f1..ccabd3689b 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md @@ -16,7 +16,6 @@ true + false "$" + 4 + 5 "4" - 2 "4px" - 2 -7 / 0 " -9 " + 5 " -9 " - 5 null + 1 @@ -24,4 +23,4 @@ undefined + 1 " \t \n" - 2 ``` -Подумайте как следует. Когда закончите – сверьтесь с решением. +Подумайте как следует, запишите ответы и сверьтесь с решением. diff --git a/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md b/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md new file mode 100644 index 0000000000..ed5f105448 --- /dev/null +++ b/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md @@ -0,0 +1,32 @@ +Причина в том, что окно запроса возвращает пользовательский ввод как строку. + +Поэтому переменные получают значения `"1"` и `"2"` соответственно. + +```js run +let a = "1"; // prompt("Первое число?", 1); +let b = "2"; // prompt("Второе число?", 2); + +alert(a + b); // 12 +``` + +Нам нужно привести строки к числам перед применением оператора `+`. Например, с помощью `Number()` или вставки `+` перед ними. + +Вставить `+` можно прямо перед `prompt`: + +```js run +let a = +prompt("Первое число?", 1); +let b = +prompt("Второе число?", 2); + +alert(a + b); // 3 +``` + +Или внутри `alert`: + +```js run +let a = prompt("Первое число?", 1); +let b = prompt("Второе число?", 2); + +alert(+a + +b); // 3 +``` + +В последнем варианте унарный и бинарный `+` используются вместе. Выглядит забавно, правда? diff --git a/1-js/02-first-steps/08-operators/4-fix-prompt/task.md b/1-js/02-first-steps/08-operators/4-fix-prompt/task.md new file mode 100644 index 0000000000..e390f8c62e --- /dev/null +++ b/1-js/02-first-steps/08-operators/4-fix-prompt/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Исправьте сложение + +Ниже приведён код, который запрашивает у пользователя два числа и показывает их сумму. + +Он работает неправильно. Код в примере выводит `12` (для значения полей по умолчанию). + +В чём ошибка? Исправьте её. Результат должен быть `3`. + +```js run +let a = prompt("Первое число?", 1); +let b = prompt("Второе число?", 2); + +alert(a + b); // 12 +``` diff --git a/1-js/02-first-steps/07-operators/article.md b/1-js/02-first-steps/08-operators/article.md similarity index 53% rename from 1-js/02-first-steps/07-operators/article.md rename to 1-js/02-first-steps/08-operators/article.md index b2a212d442..95685286e7 100644 --- a/1-js/02-first-steps/07-operators/article.md +++ b/1-js/02-first-steps/08-operators/article.md @@ -1,8 +1,8 @@ -# Операторы +# Базовые операторы, математика Многие операторы знакомы нам ещё со школы: сложение `+`, умножение `*`, вычитание `-` и так далее. -В этой главе мы сконцентрируемся на операторах, которые в курсе математики не проходят. +В этой главе мы начнём с простых операторов, а потом сконцентрируемся на специфических для JavaScript аспектах, которые не проходят в школьном курсе арифметики. ## Термины: "унарный", "бинарный", "операнд" @@ -23,14 +23,61 @@ ```js run no-beautify let x = 1, y = 3; - alert( y - x ); // 2, бинарный минус + alert( y - x ); // 2, бинарный минус вычитает значения ``` - Формально мы говорим о двух разных операторах: унарное отрицание (один операнд: меняет знак) и бинарное вычитание (два операнда: вычитает). + Формально, в последних примерах мы говорим о двух разных операторах, использующих один символ: оператор отрицания (унарный оператор, который обращает знак) и оператор вычитания (бинарный оператор, который вычитает одно число из другого). -## Сложение строк, бинарный + +## Математика -Давайте посмотрим специальные возможности операторов JavaScript, которые выходят за рамки школьной математики. +Поддерживаются следующие математические операторы: + +- Сложение `+`, +- Вычитание `-`, +- Умножение `*`, +- Деление `/`, +- Взятие остатка от деления `%`, +- Возведение в степень `**`. + +Первые четыре оператора очевидны, а про `%` и `**` стоит сказать несколько слов. + +### Взятие остатка % + +Оператор взятия остатка `%`, несмотря на обозначение, никакого отношения к процентам не имеет. + +Результат `a % b` – это [остаток](https://ru.wikipedia.org/wiki/Деление_с_остатком) от целочисленного деления `a` на `b`. + +Например: + +```js run +alert( 5 % 2 ); // 1, остаток от деления 5 на 2 +alert( 8 % 3 ); // 2, остаток от деления 8 на 3 +alert( 8 % 4 ); // 0, остаток от деления 8 на 4 +``` + +### Возведение в степень ** + +Оператор возведения в степень `a ** b` возводит `a` в степень `b`. + +В школьной математике мы записываем это как ab. + +Например: + +```js run +alert( 2 ** 2 ); // 2² = 4 +alert( 2 ** 3 ); // 2³ = 8 +alert( 2 ** 4 ); // 2⁴ = 16 +``` + +Математически, оператор работает и для нецелых чисел. Например, квадратный корень является возведением в степень ½: + +```js run +alert( 4 ** (1/2) ); // 2 (степень 1/2 эквивалентна взятию квадратного корня) +``` + +## Сложение строк при помощи бинарного + + +Давайте рассмотрим специальные возможности операторов JavaScript, которые выходят за рамки школьной арифметики. Обычно при помощи плюса `'+'` складывают числа. @@ -41,7 +88,7 @@ let s = "моя" + "строка"; alert(s); // моястрока ``` -Обратите внимание, если хотя бы один операнд является строкой, то второй будет также преобразован к строке. +Обратите внимание, если хотя бы один операнд является строкой, то второй будет также преобразован в строку. Например: @@ -50,25 +97,26 @@ alert( '1' + 2 ); // "12" alert( 2 + '1' ); // "21" ``` -Причём не важно, справа или слева находится операнд-строка. Правило простое: если хотя бы один из операндов является строкой, то второй будет также преобразован к строке. - -Тем не менее, помните, что операции выполняются слева направо. Если перед строкой идут два числа, то числа будут сложены перед преобразованием в строку: +Как видите, не важно, первый или второй операнд является строкой. +Вот пример посложнее: ```js run alert(2 + 2 + '1' ); // будет "41", а не "221" ``` -Сложение и преобразование строк - это особенность бинарного плюса `+`. Другие арифметические операторы работают только с числами и всегда преобразуют операнды в числа. +Здесь операторы работают один за другим. Первый `+` складывает два числа и возвращает `4`, затем следующий `+` объединяет результат со строкой, производя действие `4 + '1' = '41'`. + +Сложение и преобразование строк — это особенность бинарного плюса `+`. Другие арифметические операторы работают только с числами и всегда преобразуют операнды в числа. Например, вычитание и деление: ```js run -alert( 2 - '1' ); // 1 -alert( '6' / '2' ); // 3 +alert( 6 - '2' ); // 4, '2' приводится к числу +alert( '6' / '2' ); // 3, оба операнда приводятся к числам ``` -## Преобразование к числу, унарный плюс + +## Приведение к числу, унарный + Плюс `+` существует в двух формах: бинарной, которую мы использовали выше, и унарной. @@ -85,7 +133,7 @@ let y = -2; alert( +y ); // -2 *!* -// Преобразует не-числа в число +// Преобразует не числа в числа alert( +true ); // 1 alert( +"" ); // 0 */!* @@ -93,7 +141,7 @@ alert( +"" ); // 0 На самом деле это то же самое, что и `Number(...)`, только короче. -Необходимость преобразовывать строки в числа возникает очень часто. Например, обычно значения полей HTML-формы - это строки. А что, если их нужно, к примеру, сложить? +Необходимость преобразовывать строки в числа возникает очень часто. Например, обычно значения полей HTML-формы — это строки. А что, если их нужно, к примеру, сложить? Бинарный плюс сложит их как строки: @@ -101,7 +149,7 @@ alert( +"" ); // 0 let apples = "2"; let oranges = "3"; -alert( apples + oranges ); // "23", так как бинарный плюс складывает строки +alert( apples + oranges ); // "23", так как бинарный плюс объединяет строки ``` Поэтому используем унарный плюс, чтобы преобразовать к числу: @@ -119,7 +167,7 @@ alert( +apples + +oranges ); // 5 // alert( Number(apples) + Number(oranges) ); // 5 ``` -С точки зрения математика, такое изобилие плюсов выглядит странным. Но с точки зрения программиста – ничего особенного: сначала выполнятся унарные плюсы, приведут строки к числам, а затем – бинарный `'+'` их сложит. +С точки зрения математики, такое изобилие плюсов выглядит странным. Но с точки зрения программиста тут нет ничего особенного: сначала выполнятся унарные плюсы, которые приведут строки к числам, а затем бинарный `'+'` их сложит. Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете. @@ -127,34 +175,35 @@ alert( +apples + +oranges ); // 5 В том случае, если в выражении есть несколько операторов – порядок их выполнения определяется *приоритетом*, или, другими словами, существует определённый порядок выполнения операторов. -Из школы мы знаем, что умножение в выражении `2 * 2 + 1` выполнится раньше сложения. Это как раз и есть "приоритет". Говорят, что умножение имеет более высокий приоритет, чем сложение. +Из школы мы знаем, что умножение в выражении `1 + 2 * 2` выполнится раньше сложения. Это как раз и есть "приоритет". Говорят, что умножение имеет более высокий приоритет, чем сложение. -Скобки важнее, чем приоритет, так что если мы не удовлетворены порядком по умолчанию, мы можем использовать их, чтобы изменить приоритет. Например, написать `(1 + 2) * 2`. +Скобки важнее, чем приоритет, так что, если мы не удовлетворены порядком по умолчанию, мы можем использовать их, чтобы изменить приоритет. Например, написать `(1 + 2) * 2`. -В JavaScript много операторов. Каждый оператор имеет соответствующий номер приоритета. Тот, у кого это число больше – выполнится раньше. Если приоритет одинаковый, то порядок выполнения – слева направо. +В JavaScript много операторов. Каждый оператор имеет соответствующий номер приоритета. Тот, у кого это число больше, – выполнится раньше. Если приоритет одинаковый, то порядок выполнения – слева направо. -Отрывок из [таблицы приоритетов](https://developer.mozilla.org/ru/JavaScript/Reference/operators/operator_precedence) (нет необходимости всё запоминать, обратите внимание, что у унарных операторов приоритет выше, чем у соответствующих бинарных): +Отрывок из [таблицы приоритетов](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Operator_Precedence) (нет необходимости всё запоминать, обратите внимание, что приоритет унарных операторов выше, чем соответствующих бинарных): | Приоритет | Название | Обозначение | |------------|------|------| | ... | ... | ... | -| 16 | унарный плюс | `+` | -| 16 | унарный минус | `-` | -| 14 | умножение | `*` | -| 14 | деление | `/` | -| 13 | сложение | `+` | -| 13 | вычитание | `-` | +| 15 | унарный плюс | `+` | +| 15 | унарный минус | `-` | +| 14 | возведение в степень | `**` | +| 13 | умножение | `*` | +| 13 | деление | `/` | +| 12 | сложение | `+` | +| 12 | вычитание | `-` | | ... | ... | ... | -| 3 | присваивание | `=` | +| 2 | присваивание | `=` | | ... | ... | ... | -Так как "унарный плюс" имеет приоритет `16`, который выше, чем `13` у "сложения" (бинарный плюс), то в выражении `"+apples + +oranges"` сначала выполнятся унарные плюсы, а затем сложение. +Так как "унарный плюс" имеет приоритет `15`, который выше, чем `12` у "сложения" (бинарный плюс), то в выражении `"+apples + +oranges"` сначала выполнятся унарные плюсы, а затем сложение. ## Присваивание -Давайте отметим, что в таблице приоритетов также есть оператор присваивания `=`. У него один из самых низких приоритетов: `3`. +Давайте отметим, что в таблице приоритетов также есть оператор присваивания `=`. У него один из самых низких приоритетов: `2`. -Именно поэтому, когда переменной что-либо присваивают, например, `x = 2 * 2 + 1`, то сначала выполнится арифметика, а уже затем произойдёт присваивание `=`. +Именно поэтому, когда переменной что-либо присваивают, например, `x = 2 * 2 + 1`, то сначала выполнится арифметика, а уже затем произойдёт присваивание `=` с сохранением результата в `x`. ```js let x = 2 * 2 + 1; @@ -162,24 +211,11 @@ let x = 2 * 2 + 1; alert( x ); // 5 ``` -Возможно присваивание по цепочке: +### Присваивание = возвращает значение -```js run -let a, b, c; +Тот факт, что `=` является оператором, а не "магической" конструкцией языка, имеет интересные последствия. -*!* -a = b = c = 2 + 2; -*/!* - -alert( a ); // 4 -alert( b ); // 4 -alert( c ); // 4 -``` - -Такое присваивание работает справа-налево. Сначала вычисляется самое правое выражение `2 + 2`, и затем оно присваивается переменным слева: `c`, `b` и `a`. В конце у всех переменных будет одно значение. - -````smart header="Оператор `\"=\"` возвращает значение" -Все операторы возвращают значение. Для некоторых это очевидно, например сложение `+` или умножение `*`. Но и оператор присваивания не является исключением. +Большинство операторов в JavaScript возвращают значение. Для некоторых это очевидно, например сложение `+` или умножение `*`. Но и оператор присваивания не является исключением. Вызов `x = value` записывает `value` в `x` *и возвращает его*. @@ -197,64 +233,89 @@ alert( a ); // 3 alert( c ); // 0 ``` -В примере выше результатом `(a = b + 1)` будет значение, которое присваивается в `a` (то есть `3`). Потом оно используется для дальнейших вычислений. +В примере выше результатом `(a = b + 1)` будет значение, которое присваивается переменной `a` (то есть `3`). Потом оно используется для дальнейших вычислений. -Забавное применение присваивания, не так ли? Нам нужно понимать, как это работает, потому что иногда это можно увидеть в JavaScript-библиотеках, но писать самим в таком стиле не рекомендуется. Такие трюки не сделают ваш код более понятным или читабельным. -```` +Забавное применение присваивания, не так ли? Нам нужно понимать, как это работает, потому что иногда это можно увидеть в JavaScript-библиотеках. -## Остаток от деления % +Однако писать самим в таком стиле не рекомендуется. Такие трюки не сделают ваш код более понятным или читабельным. -Оператор взятия остатка `%`, несмотря на обозначение, никакого отношения к процентам не имеет. +### Присваивание по цепочке -Его результат `a % b` – это остаток от деления `a` на `b`. - -Например: +Рассмотрим ещё одну интересную возможность: цепочку присваиваний. ```js run -alert( 5 % 2 ); // 1, остаток от деления 5 на 2 -alert( 8 % 3 ); // 2, остаток от деления 8 на 3 -alert( 6 % 3 ); // 0, остаток от деления 6 на 3 +let a, b, c; + +*!* +a = b = c = 2 + 2; +*/!* + +alert( a ); // 4 +alert( b ); // 4 +alert( c ); // 4 ``` -## Возведение в степень ** +Такое присваивание работает справа налево. Сначала вычисляется самое правое выражение `2 + 2`, и затем результат присваивается переменным слева: `c`, `b` и `a`. В конце у всех переменных будет одно значение. -Оператор возведения в степень `**` недавно добавили в язык. +Опять-таки, чтобы код читался легче, лучше разделять подобные конструкции на несколько строчек: -Для натурального числа `b` результат `a ** b` равен `a`, умноженному на само себя `b` раз. +```js +c = 2 + 2; +b = c; +a = c; +``` +Польза от такого стиля особенно ощущается при быстром просмотре кода. + +## Сокращённая арифметика с присваиванием + +Часто нужно применить оператор к переменной и сохранить результат в ней же. Например: +```js +let n = 2; +n = n + 5; +n = n * 2; +``` + +Эту запись можно укоротить при помощи совмещённых операторов `+=` и `*=`: + ```js run -alert( 2 ** 2 ); // 4 (2 * 2) -alert( 2 ** 3 ); // 8 (2 * 2 * 2) -alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2) +let n = 2; +n += 5; // теперь n = 7 (работает как n = n + 5) +n *= 2; // теперь n = 14 (работает как n = n * 2) + +alert( n ); // 14 ``` -Оператор работает и для нецелых чисел. +Подобные краткие формы записи существуют для **всех** арифметических и побитовых операторов: `/=`, `-=`, `**=` и так далее. -Например: +Вызов с присваиванием имеет в точности такой же приоритет, как обычное присваивание, то есть выполнится после большинства других операций: ```js run -alert( 4 ** (1/2) ); // 2 (степень 1/2 эквивалентна взятию квадратного корня) -alert( 8 ** (1/3) ); // 2 (степень 1/3 эквивалентна взятию кубического корня) +let n = 2; + +n *= 3 + 5; + +alert( n ); // 16 (сначала выполнится правая часть, выражение идентично n *= 8) ``` ## Инкремент/декремент - + -Одной из наиболее частых операций в JavaScript, как и во многих других языках программирования, является увеличение или уменьшение переменной на единицу. +Одной из наиболее частых числовых операций является увеличение или уменьшение на единицу. Для этого существуют даже специальные операторы: -- **Инкремент** `++` увеличивает на 1: +- **Инкремент** `++` увеличивает переменную на 1: ```js run no-beautify let counter = 2; counter++; // работает как counter = counter + 1, просто запись короче alert( counter ); // 3 ``` -- **Декремент** `--` уменьшает на 1: +- **Декремент** `--` уменьшает переменную на 1: ```js run no-beautify let counter = 2; @@ -263,19 +324,19 @@ alert( 8 ** (1/3) ); // 2 (степень 1/3 эквивалентна взят ``` ```warn -Инкремент/декремент можно применить только к переменной. Попытка использовать его на значении, типа 5++, вернёт ошибку. +Инкремент/декремент можно применить только к переменной. Попытка использовать его на значении, типа 5++, приведёт к ошибке. ``` Операторы `++` и `--` могут быть расположены не только после, но и до переменной. -- Когда оператор идёт после переменной - это "постфиксная форма": `counter++`. -- "Префиксная форма" - это когда оператор идёт перед переменной: `++counter`. +- Когда оператор идёт после переменной — это "постфиксная форма": `counter++`. +- "Префиксная форма" — это когда оператор идёт перед переменной: `++counter`. -Обе эти формы записи делают одно и то же: увеличивают `counter` на `1`. +Обе эти инструкции делают одно и то же: увеличивают `counter` на `1`. Есть ли разница между ними? Да, но увидеть её мы сможем, только если будем использовать значение, которое возвращают `++/--`. -Давайте проясним этот момент. Как мы знаем, все операторы возвращают значение. Операторы инкремент/декремент не исключение. Префиксная форма возвращает новое значение, в то время как постфиксная форма возвращает старое (до увеличения/уменьшения числа). +Давайте проясним этот момент. Как мы знаем, все операторы возвращают значение. Операторы инкремента/декремента не исключение. Префиксная форма возвращает новое значение, в то время как постфиксная форма возвращает старое (до увеличения/уменьшения числа). Чтобы увидеть разницу, вот небольшой пример: @@ -286,7 +347,7 @@ let a = ++counter; // (*) alert(a); // *!*2*/!* ``` -В строке `(*)` *префиксная* форма увеличения `counter`, она возвращает новое значение `2`. Так что `alert` покажет `2`. +В строке `(*)` *префиксная* форма `++counter` увеличивает `counter` и возвращает новое значение `2`. Так что `alert` покажет `2`. Теперь посмотрим на постфиксную форму: @@ -301,7 +362,7 @@ alert(a); // *!*1*/!* Подведём итоги: -- Если результат оператора не используется, а нужно только увеличить/уменьшить переменную – без разницы, какую форму использовать: +- Если результат оператора не используется, а нужно только увеличить/уменьшить переменную, тогда без разницы, какую форму использовать: ```js run let counter = 0; @@ -315,7 +376,7 @@ alert(a); // *!*1*/!* let counter = 0; alert( ++counter ); // 1 ``` -- Если нужно увеличить, и при этом получить значение переменной *до увеличения* – постфиксная форма: +- Если нужно увеличить и при этом получить значение переменной *до увеличения* – нужна постфиксная форма: ```js run let counter = 0; @@ -323,7 +384,7 @@ alert(a); // *!*1*/!* ``` ````smart header="Инкремент/декремент можно использовать в любых выражениях" -Операторы `++/--` могут также использоваться внутри выражений. Их приоритет выше, чем у арифметических операций. +Операторы `++/--` могут также использоваться внутри выражений. Их приоритет выше, чем у большинства других арифметических операций. Например: @@ -339,7 +400,7 @@ let counter = 1; alert( 2 * counter++ ); // 2, потому что counter++ возвращает "старое" значение ``` -Хотя технически всё в порядке, такая запись обычно делает код менее читабельным. Одна строка выполняет множество действий -- нехорошо. +Хотя технически здесь всё в порядке, такая запись обычно делает код менее читабельным. Одна строка выполняет множество действий -- нехорошо. При беглом чтении кода можно с лёгкостью пропустить такой `counter++`, и будет неочевидно, что переменная увеличивается. @@ -368,47 +429,15 @@ counter++; - RIGHT SHIFT(правый сдвиг) ( `>>` ) - ZERO-FILL RIGHT SHIFT(правый сдвиг с заполнением нулями) ( `>>>` ) -Они используются редко. Чтобы понять их, нам нужно углубиться в низкоуровневое представление чисел, и было бы неоптимально делать это прямо сейчас, тем более что они нам не понадобятся в ближайшее время. Если вам интересно, вы можете прочитать статью [Побитовые операторы](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) на MDN. Практично будет сделать это, когда возникнет реальная необходимость. +Они используются редко, когда возникает необходимость оперировать с числами на очень низком (побитовом) уровне. В ближайшем времени они нам не понадобятся, так как веб-разработчики редко к ним прибегают, хотя в некоторых сферах (например, в криптографии) они полезны. -## Сокращённая арифметика с присваиванием +Вы можете прочитать о них в главе , когда возникнет реальная необходимость. -Часто нужно применить оператор к переменной и сохранить результат в ней же. +## Оператор "запятая" -Например: +Оператор "запятая" (`,`) редко применяется и является одним из самых необычных. Иногда он используется для написания более короткого кода, поэтому нам нужно знать его, чтобы понимать, что при этом происходит. -```js -let n = 2; -n = n + 5; -n = n * 2; -``` - -Эту запись можно укоротить при помощи совмещённых операторов `+=` и `*=`: - -```js run -let n = 2; -n += 5; // теперь n=7 (работает как n = n + 5) -n *= 2; // теперь n=14 (работает как n = n * 2) - -alert( n ); // 14 -``` - -Подобные краткие формы записи существуют для всех арифметических и побитовых операторов: `/=`, `-=` и так далее. - -Вызов с присваиванием имеет в точности такой же приоритет, как обычное присваивание, то есть выполнится после большинства других операций: - -```js run -let n = 2; - -n *= 3 + 5; - -alert( n ); // 16 (сначала выполнится правая часть, превратив выражение в n *= 8) -``` - -## Оператор запятая - -Оператор "запятая" `,` редко используется и является одним из самых необычных. Иногда он используется для написания более короткого кода, поэтому нам нужно знать его, чтобы понимать, что при этом происходит. - -Оператор запятая предоставляет нам возможность вычислять несколько выражений, разделяя их запятой `,`. Каждое выражение выполняется, но возвращается результат только последнего. +Оператор "запятая" предоставляет нам возможность вычислять несколько выражений, разделяя их запятой `,`. Каждое выражение выполняется, но возвращается результат только последнего. Например: @@ -417,17 +446,27 @@ alert( n ); // 16 (сначала выполнится правая часть, let a = (1 + 2, 3 + 4); */!* -alert( a ); // 7 (результат 3 + 4) +alert( a ); // 7 (результат вычисления 3 + 4) ``` Первое выражение `1 + 2` выполняется, а результат отбрасывается. Затем идёт `3 + 4`, выражение выполняется и возвращается результат. -```smart header="Запятая имеет очень низкий приоритет" -Пожалуйста, обратите внимание, что оператор запятая имеет очень низкий приоритет, приоритет которого ниже `=`, поэтому скобки важны в приведённом примере выше. +````smart header="Запятая имеет очень низкий приоритет" +Пожалуйста, обратите внимание, что оператор `,` имеет очень низкий приоритет, ниже `=`, поэтому скобки важны в приведённом выше примере. + +Попробуйте запустить следующий код (**строгий режим `"use strict"` в примере ниже не используется, иначе мы бы получили ошибку**): + +```js run no-strict +a = 1 + 2, 3 + 4; -Без них в `a = 1 + 2, 3 + 4` сначала выполнится `+`, суммируя числа в `a = 3, 7`, затем оператор присваивания `=` присвоит `a = 3`, а то что идёт дальше, будет игнорировано. Всё так же, как в `(a = 1 + 2), 3+4`. +alert(a); // 3 ``` +Необычный результат, правда? Особенно учитывая то, что оператор `,` должен «выполнять каждое выражение, но возвращать результат только последнего». + +Без скобок в `a = 1 + 2, 3 + 4` сначала выполнится `+`, суммируя числа в `a = 3, 7`, затем оператор присваивания `=` присвоит `a = 3`, а то, что идёт дальше, будет проигнорировано. Всё так же, как в `(a = 1 + 2), 3 + 4`. +```` + Зачем нам оператор, который отбрасывает всё, кроме последнего выражения? Иногда его используют в составе более сложных конструкций, чтобы сделать несколько действий в одной строке. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md similarity index 82% rename from 1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md rename to 1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md index b62c964f55..16e343a8aa 100644 --- a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md +++ b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md @@ -13,9 +13,9 @@ null === +"\n0\n" → false Разъяснения: 1. Очевидно, `true`. -2. Используется посимвольное сравнение, поэтому `false`. +2. Используется посимвольное сравнение, поэтому `false`. `"а"` меньше, чем `"я"`. 3. Снова посимвольное сравнение. Первый символ первой строки `"2"` больше, чем первый символ второй `"1"`. -4. Специальный случай. Значения `null` и `undefinded` равны друг другу при нестрогом сравнении. +4. Специальный случай. Значения `null` и `undefined` равны только друг другу при нестрогом сравнении. 5. Строгое сравнение разных типов, поэтому `false`. 6. Аналогично `(4)`, `null` равен только `undefined`. 7. Строгое сравнение разных типов. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/task.md similarity index 100% rename from 1-js/02-first-steps/08-comparison/1-comparison-questions/task.md rename to 1-js/02-first-steps/09-comparison/1-comparison-questions/task.md diff --git a/1-js/02-first-steps/08-comparison/article.md b/1-js/02-first-steps/09-comparison/article.md similarity index 79% rename from 1-js/02-first-steps/08-comparison/article.md rename to 1-js/02-first-steps/09-comparison/article.md index f4ef601cf4..ced30d93d8 100644 --- a/1-js/02-first-steps/08-comparison/article.md +++ b/1-js/02-first-steps/09-comparison/article.md @@ -1,15 +1,21 @@ # Операторы сравнения -Многие операторы сравнения известны нам из математики: +Многие операторы сравнения известны нам из математики. + +В JavaScript они записываются так: - Больше/меньше: a > b, a < b. - Больше/меньше или равно: a >= b, a <= b. -- Равно: `a == b`. Обратите внимание, для сравнения используется двойной знак равенства `=`. Один знак равенства `a = b` означал бы присваивание. -- Не равно. В математике обозначается символом . В JavaScript записывается как знак равенства с предшествующим ему восклицательным знаком: a != b. +- Равно: `a == b`. Обратите внимание, для сравнения используется двойной знак равенства `==`. Один знак равенства `a = b` означал бы присваивание. +- Не равно. В математике обозначается символом , но в JavaScript записывается как a != b. + +В этом разделе мы больше узнаем про то, какие бывают сравнения, как язык с ними работает и к каким неожиданностям мы должны быть готовы. + +В конце вы найдёте хороший рецепт того, как избегать "причуд" сравнения в JavaScript. ## Результат сравнения имеет логический тип -Операторы сравнения, как и другие операторы, возвращают значение. Это значение имеет логический тип. +Все операторы сравнения возвращают значение логического типа: - `true` -- означает "да", "верно", "истина". - `false` -- означает "нет", "неверно", "ложь". @@ -39,23 +45,23 @@ alert( result ); // true ```js run alert( 'Я' > 'А' ); // true -alert( 'Кот' > 'Код' ); // true +alert( 'Коты' > 'Кода' ); // true alert( 'Сонный' > 'Сон' ); // true ``` Алгоритм сравнения двух строк довольно прост: 1. Сначала сравниваются первые символы строк. -2. Если первый символ первой строки больше (меньше), чем первый символ второй, то первая строка больше (меньше) второй. +2. Если первый символ первой строки больше (меньше), чем первый символ второй, то первая строка больше (меньше) второй. Сравнение завершено. 3. Если первые символы равны, то таким же образом сравниваются уже вторые символы строк. 4. Сравнение продолжается, пока не закончится одна из строк. 5. Если обе строки заканчиваются одновременно, то они равны. Иначе, большей считается более длинная строка. -В примерах выше сравнение `'Я' > 'А'` завершится на первом шаге, тогда как строки `"Кот"` и `"Код"` будут сравниваться посимвольно: +В примерах выше сравнение `'Я' > 'А'` завершится на первом шаге, тогда как строки `'Коты'` и `'Кода'` будут сравниваться посимвольно: 1. `К` равна `К`. 2. `о` равна `о`. -3. `т` больше чем `д`. На этом сравнение заканчивается. Первая строка больше. +3. `т` больше, чем `д`. На этом сравнение заканчивается. Первая строка больше. ```smart header="Используется кодировка Unicode, а не настоящий алфавит" Приведённый выше алгоритм сравнения похож на алгоритм, используемый в словарях и телефонных книгах, но между ними есть и различия. @@ -65,7 +71,7 @@ alert( 'Сонный' > 'Сон' ); // true ## Сравнение разных типов -При сравнении значений разных типов, JavaScript приводит каждое из них к числу. +При сравнении значений разных типов JavaScript приводит каждое из них к числу. Например: @@ -134,11 +140,11 @@ alert( 0 === false ); // false, так как сравниваются разн Ещё есть оператор строгого неравенства `!==`, аналогичный `!=`. -Оператор строгого равенства дольше писать, но он делает код более очевидным и оставляет меньше мест для ошибок. +Оператор строгого равенства дольше писать, но он делает код более очевидным и оставляет меньше места для ошибок. ## Сравнение с null и undefined -Поведение `null` и `undefined` при сравнении с другими значениями - особое: +Поведение `null` и `undefined` при сравнении с другими значениями — особое: При строгом равенстве `===` : Эти значения различны, так как различны их типы. @@ -175,7 +181,7 @@ alert( null >= 0 ); // (3) *!*true*/!* С другой стороны, для нестрогого равенства `==` значений `undefined` и `null` действует особое правило: эти значения ни к чему не приводятся, они равны друг другу и не равны ничему другому. Поэтому (2) `null == 0` ложно. -### Несравнимое значение undefined +### Несравненное значение undefined Значение `undefined` несравнимо с другими значениями: @@ -190,20 +196,19 @@ alert( undefined == 0 ); // false (3) На это есть следующие причины: - Сравнения `(1)` и `(2)` возвращают `false`, потому что `undefined` преобразуется в `NaN`, а `NaN` – это специальное числовое значение, которое возвращает `false` при любых сравнениях. -- Нестрогое равенство `(3)` возвращает `false`, потому что `undefined` равно только `null` и ни чему больше. +- Нестрогое равенство `(3)` возвращает `false`, потому что `undefined` равно только `null`, `undefined` и ничему больше. ### Как избежать проблем -Зачем мы рассмотрели все эти примеры? Должны ли мы постоянно помнить обо всех этих особенностях? Не обязательно. Со временем, все они станут вам знакомы, но можно избежать проблем, если следовать простому правилу. - -Просто относитесь к любому сравнению с `undefined/null`, кроме строгого равенства `===`, с осторожностью. +Зачем мы рассмотрели все эти примеры? Должны ли мы постоянно помнить обо всех этих особенностях? Необязательно. Со временем все они станут вам знакомы, но можно избежать проблем, если следовать надёжным правилам: -Не используйте сравнения `>= > < <=` с переменными, которые могут принимать значения `null/undefined`, если вы не уверены в том, что делаете. Если переменная может принимать эти значения, то добавьте для них отдельные проверки. +- Относитесь очень осторожно к любому сравнению с `undefined/null`, кроме случаев строгого равенства `===`. +- Не используйте сравнения `>= > < <=` с переменными, которые могут принимать значения `null/undefined`, разве что вы полностью уверены в том, что делаете. Если переменная может принимать эти значения, то добавьте для них отдельные проверки. ## Итого - Операторы сравнения возвращают значения логического типа. - Строки сравниваются посимвольно в лексикографическом порядке. - Значения разных типов при сравнении приводятся к числу. Исключением является сравнение с помощью операторов строгого равенства/неравенства. -- Значения `null` и `undefined` равны `==` друг другу и не равны любому другому. -- Будьте осторожны при использовании операторов сравнений `>` и `<` с переменными, которые могут принимать значения `null/undefined`. Хорошей идеей будет сделать отдельную проверку на `null/undefined`. +- Значения `null` и `undefined` равны `==` друг другу и не равны любому другому значению. +- Будьте осторожны при использовании операторов сравнений вроде `>` и `<` с переменными, которые могут принимать значения `null/undefined`. Хорошей идеей будет сделать отдельную проверку на `null/undefined`. diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg index 6e44c9b4e9..2ef4442c7b 100644 --- a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg @@ -1 +1 @@ -НачалоYou don't know? “ECMAScript”!Правильно!What's the "официальное название" JavaScript?ДругоеECMAScript \ No newline at end of file +НачалоНе знаете? “ECMAScript”!Верно!Какое "официальное" название JavaScript?ДругоеECMAScript \ No newline at end of file diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md b/1-js/02-first-steps/10-ifelse/4-rewrite-if-question/solution.md similarity index 100% rename from 1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md rename to 1-js/02-first-steps/10-ifelse/4-rewrite-if-question/solution.md diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md b/1-js/02-first-steps/10-ifelse/4-rewrite-if-question/task.md similarity index 55% rename from 1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md rename to 1-js/02-first-steps/10-ifelse/4-rewrite-if-question/task.md index 78302e5ce0..eadd6aa5a1 100644 --- a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md +++ b/1-js/02-first-steps/10-ifelse/4-rewrite-if-question/task.md @@ -4,7 +4,7 @@ importance: 5 # Перепишите 'if' в '?' -Перепишите `if` с использованием условного оператора `'?'`: +Перепишите конструкцию `if` с использованием условного оператора `'?'`: ```js let result; diff --git a/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/solution.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-else-question/solution.md similarity index 100% rename from 1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/solution.md rename to 1-js/02-first-steps/10-ifelse/5-rewrite-if-else-question/solution.md diff --git a/1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/task.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-else-question/task.md similarity index 100% rename from 1-js/02-first-steps/10-ifelse/6-rewrite-if-else-question/task.md rename to 1-js/02-first-steps/10-ifelse/5-rewrite-if-else-question/task.md diff --git a/1-js/02-first-steps/10-ifelse/article.md b/1-js/02-first-steps/10-ifelse/article.md index 6b0b2e48b5..598a1d25b5 100644 --- a/1-js/02-first-steps/10-ifelse/article.md +++ b/1-js/02-first-steps/10-ifelse/article.md @@ -1,17 +1,17 @@ -# Условные операторы: if, '?' +# Условное ветвление: if, '?' Иногда нам нужно выполнить различные действия в зависимости от условий. -Для этого мы можем использовать оператор `if` и условный оператор `?`, который также называют "оператор вопросительный знак". +Для этого мы можем использовать инструкцию `if` и условный оператор `?`, который также называют оператором "вопросительный знак". -## Оператор "if" +## Инструкция "if" -Оператор `if(...)` вычисляет условие в скобках и, если результат `true`, то выполняет блок кода. +Инструкция `if(...)` вычисляет условие в скобках и, если результат `true`, то выполняет блок кода. Например: ```js run -let year = prompt('В каком году появилась спецификация ECMAScript-2015?', ''); +let year = prompt('В каком году была опубликована спецификация ECMAScript-2015?', ''); *!* if (year == 2015) alert( 'Вы правы!' ); @@ -29,11 +29,11 @@ if (year == 2015) { } ``` -Мы рекомендуем использовать фигурные скобки `{}` всегда, когда вы используете оператор `if`, даже если выполняется только одна команда. Это улучшает читабельность кода. +Мы рекомендуем использовать фигурные скобки `{}` всегда, когда вы используете инструкцию `if`, даже если выполняется только одна команда. Это улучшает читаемость кода. ## Преобразование к логическому типу -Оператор `if (…)` вычисляет выражение в скобках и преобразует результат к логическому типу. +Инструкция `if (…)` вычисляет выражение в скобках и преобразует результат к логическому типу. Давайте вспомним правила преобразования типов из главы : @@ -59,21 +59,21 @@ if (1) { // 1 is truthy Мы также можем передать заранее вычисленное в переменной логическое значение в `if`, например так: ```js -let cond = (year == 2015); // преобразуется к true или false +let condition = (year == 2015); // преобразуется к true или false -if (cond) { +if (condition) { ... } ``` ## Блок "else" -Оператор `if` может содержать необязательный блок "else" ("иначе"). Выполняется, когда условие ложно. +Инструкция `if` может содержать необязательный блок "else" ("иначе"). Он выполняется, когда условие ложно. Например: ```js run -let year = prompt('В каком году появилась спецификация ECMAScript-2015?', ''); +let year = prompt('В каком году была опубликована спецификация ECMAScript-2015?', ''); if (year == 2015) { alert( 'Да вы знаток!' ); @@ -84,12 +84,12 @@ if (year == 2015) { ## Несколько условий: "else if" -Иногда, нужно проверить несколько вариантов условия. Для этого используется блок `else if`. +Иногда нужно проверить несколько вариантов условия. Для этого используется блок `else if`. Например: ```js run -let year = prompt('В каком году появилась спецификация ECMAScript-2015?', ''); +let year = prompt('В каком году была опубликована спецификация ECMAScript-2015?', ''); if (year < 2015) { alert( 'Это слишком рано...' ); @@ -100,13 +100,13 @@ if (year < 2015) { } ``` -В приведённом выше коде, JavaScript сначала проверит `year < 2015`. Если это неверно, он переходит к следующему условию `year > 2015`. Если оно тоже ложно, тогда сработает последний `alert`. +В приведённом выше коде JavaScript сначала проверит `year < 2015`. Если это неверно, он переходит к следующему условию `year > 2015`. Если оно тоже ложно, тогда сработает последний `alert`. Блоков `else if` может быть и больше. Присутствие блока `else` не является обязательным. ## Условный оператор '?' -Иногда, нам нужно назначить переменную в зависимости от условия. +Иногда нам нужно определить переменную в зависимости от условия. Например: @@ -153,10 +153,10 @@ let accessAllowed = (age > 18) ? true : false; let accessAllowed = age > 18 ? true : false; ``` -Но скобки делают код более читабельным, поэтому мы рекомендуем их использовать. +Но скобки делают код более простым для восприятия, поэтому мы рекомендуем их использовать. ````smart -В примере выше, вы можете избежать использования оператора вопросительного знака `?`, т.к. сравнение само по себе уже возвращает `true/false`: +В примере выше вы можете избежать использования оператора вопросительного знака `?`, т.к. сравнение само по себе уже возвращает `true/false`: ```js // то же самое @@ -184,8 +184,8 @@ alert( message ); Поначалу может быть сложно понять, что происходит. Но при ближайшем рассмотрении мы видим, что это обычная последовательная проверка: 1. Первый знак вопроса проверяет `age < 3`. -2. Если верно -- возвращает `'Здравствуй, малыш!'`. В противном случае, проверяет выражение после двоеточия '":"', вычисляет `age < 18`. -3. Если это верно -- возвращает `'Привет!'`. В противном случае, проверяет выражение после следующего двоеточия '":"', вычисляет `age < 100`. +2. Если верно -- возвращает `'Здравствуй, малыш!'`. В противном случае, проверяет выражение после двоеточия ':', вычисляет `age < 18`. +3. Если это верно -- возвращает `'Привет!'`. В противном случае, проверяет выражение после следующего двоеточия ':', вычисляет `age < 100`. 4. Если это верно -- возвращает `'Здравствуйте!'`. В противном случае, возвращает выражение после последнего двоеточия -- `'Какой необычный возраст!'`. Вот как это выглядит при использовании `if..else`: @@ -204,7 +204,7 @@ if (age < 3) { ## Нетрадиционное использование '?' -Иногда оператор вопросительный знак `?` используется в качестве замены `if`: +Иногда оператор "вопросительный знак" `?` используется в качестве замены `if`: ```js run no-beautify let company = prompt('Какая компания создала JavaScript?', ''); @@ -221,7 +221,7 @@ let company = prompt('Какая компания создала JavaScript?', ' **Не рекомендуется использовать оператор вопросительного знака таким образом.** -Несмотря на то, что такая запись короче, чем эквивалентное выражение `if`, она менее читабельна. +Несмотря на то, что такая запись короче, чем эквивалентная инструкция `if`, она хуже читается. Вот, для сравнения, тот же код, использующий `if`: @@ -239,4 +239,4 @@ if (company == 'Netscape') { При чтении глаза сканируют код по вертикали. Блоки кода, занимающие несколько строк, воспринимаются гораздо легче, чем длинный горизонтальный набор инструкций. -Смысл оператора вопросительный знак `?` -- вернуть то или иное значение, в зависимости от условия. Пожалуйста, используйте его именно для этого. Когда вам нужно выполнить разные ветви кода -- используйте `if`. +Смысл оператора "вопросительный знак" `?` -- вернуть то или иное значение, в зависимости от условия. Пожалуйста, используйте его именно для этого. Когда вам нужно выполнить разные ветви кода -- используйте `if`. diff --git a/1-js/02-first-steps/11-logical-operators/10-check-login/ifelse_task.svg b/1-js/02-first-steps/11-logical-operators/10-check-login/ifelse_task.svg new file mode 100644 index 0000000000..319fa9ea1b --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/10-check-login/ifelse_task.svg @@ -0,0 +1 @@ +НачалоОтмененоОтмененоЗдравствуйте!Я вас не знаюНеверный парольКто там?Пароль?ОтменаОтменаАдминЯ ГлавныйДругоеДругое \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/solution.md b/1-js/02-first-steps/11-logical-operators/10-check-login/solution.md similarity index 76% rename from 1-js/02-first-steps/11-logical-operators/9-check-login/solution.md rename to 1-js/02-first-steps/11-logical-operators/10-check-login/solution.md index 347bbd457d..3627f2368b 100644 --- a/1-js/02-first-steps/11-logical-operators/9-check-login/solution.md +++ b/1-js/02-first-steps/11-logical-operators/10-check-login/solution.md @@ -3,19 +3,19 @@ ```js run demo let userName = prompt("Кто там?", ''); -if (userName == 'Админ') { +if (userName === 'Админ') { let pass = prompt('Пароль?', ''); - if (pass == 'Я главный') { + if (pass === 'Я главный') { alert( 'Здравствуйте!' ); - } else if (pass == '' || pass == null) { + } else if (pass === '' || pass === null) { alert( 'Отменено' ); } else { alert( 'Неверный пароль' ); } -} else if (userName == '' || userName == null) { +} else if (userName === '' || userName === null) { alert( 'Отменено' ); } else { alert( "Я вас не знаю" ); diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/task.md b/1-js/02-first-steps/11-logical-operators/10-check-login/task.md similarity index 86% rename from 1-js/02-first-steps/11-logical-operators/9-check-login/task.md rename to 1-js/02-first-steps/11-logical-operators/10-check-login/task.md index 76f65228ca..817fd1074b 100644 --- a/1-js/02-first-steps/11-logical-operators/9-check-login/task.md +++ b/1-js/02-first-steps/11-logical-operators/10-check-login/task.md @@ -12,7 +12,7 @@ importance: 3 - Если введён пароль "Я главный", то выводить "Здравствуйте!", - Иначе – "Неверный пароль", -- При отмене – "Отменено". +- При отмене или в случае если ничего не введено – "Отменено". Блок-схема: @@ -20,6 +20,6 @@ importance: 3 Для решения используйте вложенные блоки `if`. Обращайте внимание на стиль и читаемость кода. -Подсказка: передача пустого ввода в приглашение `prompt` возвращает пустую строку `''`. Нажатие клавиши `key:Esc` во время запроса возвращает`null`. +Подсказка: передача пустого ввода в приглашение `prompt` возвращает пустую строку `''`. Нажатие клавиши `key:Esc` во время запроса возвращает `null`. [demo] diff --git a/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md b/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md index f7529e616a..b563f9a893 100644 --- a/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md +++ b/1-js/02-first-steps/11-logical-operators/4-alert-and/solution.md @@ -6,5 +6,5 @@ alert( alert(1) && alert(2) ); Вызов `alert` не возвращает значения, или, иначе говоря, возвращает `undefined`. -Поэтому до правого `alert `дело не дойдёт, вычисления закончатся на левом. +Поэтому до правого `alert` дело не дойдёт, вычисления закончатся на левом. diff --git a/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md b/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md index 141e2e9ab5..0d84377a43 100644 --- a/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md +++ b/1-js/02-first-steps/11-logical-operators/4-alert-and/task.md @@ -2,7 +2,7 @@ importance: 3 --- -# Что выведет alert (И)?? +# Что выведет alert (И)? Что выведет код ниже? diff --git a/1-js/02-first-steps/11-logical-operators/6-alert-or-assignment-and-assignment/solution.md b/1-js/02-first-steps/11-logical-operators/6-alert-or-assignment-and-assignment/solution.md new file mode 100644 index 0000000000..f7b6e00039 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/6-alert-or-assignment-and-assignment/solution.md @@ -0,0 +1,30 @@ +Ответ: `30`. + +```js run +let value = NaN; + +value &&= 10; +value ||= 20; +value &&= 30; +value ||= 40; + +alert(value); +``` + +Порядок выполнения данного кода: +1. `value &&= 10` + - `value=NaN` + - `NaN` конвертируется в логическое значение `false` + - `value` ложно, поэтому присваивание *не* срабатывает +2. `value ||= 20` + - `value=NaN` + - `NaN` конвертируется в логическое значение `false` + - `value` ложно, поэтому присваивание *срабатывает* +3. `value &&= 30` + - `value=20` + - `20` конвертируется в логическое значение `true` + - `value` истинно, поэтому присваивание *срабатывает* +4. `value ||= 40` + - `value=30` + - `30` конвертируется в логическое значение `true` + - `value` истинно, поэтому присваивание *не* срабатывает \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/6-alert-or-assignment-and-assignment/task.md b/1-js/02-first-steps/11-logical-operators/6-alert-or-assignment-and-assignment/task.md new file mode 100644 index 0000000000..eb1e29ee3d --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/6-alert-or-assignment-and-assignment/task.md @@ -0,0 +1,18 @@ +importance: 2 + +--- + +# Что выведет этот код (||=, &&=)? + +Что выведет код ниже? + +```js +let value = NaN; + +value &&= 10; +value ||= 20; +value &&= 30; +value ||= 40; + +alert(value); +``` \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/solution.md b/1-js/02-first-steps/11-logical-operators/7-check-if-in-range/solution.md similarity index 100% rename from 1-js/02-first-steps/11-logical-operators/6-check-if-in-range/solution.md rename to 1-js/02-first-steps/11-logical-operators/7-check-if-in-range/solution.md diff --git a/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/task.md b/1-js/02-first-steps/11-logical-operators/7-check-if-in-range/task.md similarity index 100% rename from 1-js/02-first-steps/11-logical-operators/6-check-if-in-range/task.md rename to 1-js/02-first-steps/11-logical-operators/7-check-if-in-range/task.md diff --git a/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/solution.md b/1-js/02-first-steps/11-logical-operators/8-check-if-out-range/solution.md similarity index 100% rename from 1-js/02-first-steps/11-logical-operators/7-check-if-out-range/solution.md rename to 1-js/02-first-steps/11-logical-operators/8-check-if-out-range/solution.md diff --git a/1-js/02-first-steps/11-logical-operators/7-check-if-out-range/task.md b/1-js/02-first-steps/11-logical-operators/8-check-if-out-range/task.md similarity index 100% rename from 1-js/02-first-steps/11-logical-operators/7-check-if-out-range/task.md rename to 1-js/02-first-steps/11-logical-operators/8-check-if-out-range/task.md diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg deleted file mode 100644 index 18dc2dbce2..0000000000 --- a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg +++ /dev/null @@ -1 +0,0 @@ -НачалоОтмененоОтмененоЗдравствуйте!I don't know youНеверный парольWho's there?Пароль?ОтменаОтменаАдминЯ ГлавныйДругоеДругое \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/8-if-question/solution.md b/1-js/02-first-steps/11-logical-operators/9-if-question/solution.md similarity index 100% rename from 1-js/02-first-steps/11-logical-operators/8-if-question/solution.md rename to 1-js/02-first-steps/11-logical-operators/9-if-question/solution.md diff --git a/1-js/02-first-steps/11-logical-operators/8-if-question/task.md b/1-js/02-first-steps/11-logical-operators/9-if-question/task.md similarity index 93% rename from 1-js/02-first-steps/11-logical-operators/8-if-question/task.md rename to 1-js/02-first-steps/11-logical-operators/9-if-question/task.md index a60760e743..e12918bc43 100644 --- a/1-js/02-first-steps/11-logical-operators/8-if-question/task.md +++ b/1-js/02-first-steps/11-logical-operators/9-if-question/task.md @@ -2,7 +2,7 @@ importance: 5 --- -# Вопрос о "if" +# Вопрос об "if" Какие из перечисленных ниже `alert` выполнятся? diff --git a/1-js/02-first-steps/11-logical-operators/article.md b/1-js/02-first-steps/11-logical-operators/article.md index 8cbb7daed8..9a0465e822 100644 --- a/1-js/02-first-steps/11-logical-operators/article.md +++ b/1-js/02-first-steps/11-logical-operators/article.md @@ -1,6 +1,16 @@ # Логические операторы -В JavaScript есть три логических оператора: `||` (ИЛИ), `&&` (И) и `!` (НЕ). +В JavaScript есть семь логических операторов: + +- `||` (ИЛИ) + - `||=` (Оператор логического присваивания ИЛИ) +- `&&` (И) + - `&&=` (Оператор логического присваивания И) +- `!` (НЕ) +- `??` (Оператор нулевого слияния) + - `??=` (Оператор нулевого присваивания) + +Здесь мы рассмотрим первые пять, операторы `??` и `??=` будут в следующей статье. Несмотря на своё название, данные операторы могут применяться к значениям любых типов. Полученные результаты также могут иметь различный тип. @@ -35,7 +45,7 @@ alert( false || false ); // false ```js run if (1 || 0) { // работает как if( true || false ) - alert( 'truthy!' ); + alert( 'истинно!' ); } ``` @@ -64,13 +74,13 @@ if (hour < 10 || hour > 18 || isWeekend) { } ``` -## ИЛИ "||" находит первое истинное значение +### ИЛИ "||" находит первое истинное значение [#or-finds-the-first-truthy-value] Описанная выше логика соответствует традиционной. Теперь давайте поработаем с "дополнительными" возможностями JavaScript. Расширенный алгоритм работает следующим образом. -При выполнении ИЛИ || с несколькими значениями: +При выполнении ИЛИ `||` с несколькими значениями: ```js result = value1 || value2 || value3; @@ -84,13 +94,13 @@ result = value1 || value2 || value3; Значение возвращается в исходном виде, без преобразования. -Другими словами, цепочка ИЛИ `"||"` возвращает первое истинное значение или последнее, если такое значение не найдено. +Другими словами, цепочка ИЛИ `||` возвращает первое истинное значение или последнее, если такое значение не найдено. Например: ```js run -alert( 1 || 0 ); // 1 -alert( true || 'no matter what' ); // true +alert( 1 || 0 ); // 1 (1 - истинное значение) +alert( true || 'какая-то строка' ); // true alert( null || 1 ); // 1 (первое истинное значение) alert( null || 0 || 1 ); // 1 (первое истинное значение) @@ -101,53 +111,107 @@ alert( undefined || null || 0 ); // 0 (поскольку все ложно, в 1. **Получение первого истинного значения из списка переменных или выражений.** - Представим, что у нас имеется ряд переменных, которые могут содержать данные или быть `null/undefined`. Как мы можем найти первую переменную с данными? + Например, у нас есть переменные `firstName`, `lastName` и `nickName`, все они необязательные (т.е. они могут быть неопределенными или иметь ложные значения). - С помощью `||`: + Давайте воспользуемся оператором ИЛИ `||`, чтобы выбрать ту переменную, в которой есть данные, и показать её (или "Аноним", если ни в одной переменной данных нет): ```js run - let currentUser = null; - let defaultUser = "John"; + let firstName = ""; + let lastName = ""; + let nickName = "Суперкодер"; + + *!*alert( firstName || lastName || nickName || "Аноним"); // Суперкодер*/!* + ``` + + Если бы все переменные были ложными, в качестве результата мы бы наблюдали `"Аноним"`. +2. **Сокращённое вычисление.** + + Ещё одной отличительной особенностью оператора ИЛИ `||` является так называемое «сокращённое вычисление». - *!* - let name = currentUser || defaultUser || "unnamed"; - */!* + Это означает, что ИЛИ `||` обрабатывает свои операнды до тех пор, пока не будет достигнуто первое истинностное значение, и затем это значение сразу же возвращается, даже не затрагивая другие операнды. - alert( name ); // выбирается "John" – первое истинное значение + Важность этой особенности становится очевидной, если операнд -- это не просто значение, а выражение с сопутствующим эффектом, как, например, присваивание переменной или вызов функции. + + В приведенном ниже примере срабатывает только второй `alert`: + + ```js run + *!*true*/!* || alert("никогда не сработает"); + *!*false*/!* || alert("сработает"); ``` - Если бы и `currentUser`, и `defaultUser` были ложными, в качестве результата мы бы наблюдали `"unnamed"`. -2. **Cокращённое вычисление.** + В первой строке оператор ИЛИ `||` останавливает выполнение сразу после того, как сталкивается с истинным значением (`true`), поэтому сообщение не показывается. - Операндами могут быть как отдельные значения, так и произвольные выражения. ИЛИ вычисляет их слева направо. Вычисление останавливается при достижении первого истинного значения. Этот процесс называется "сокращённым вычислением", поскольку второй операнд вычисляется только в том случае, если первого недостаточно для вычисления всего выражения. + Иногда люди используют эту возможность для выполнения инструкций только в том случае, если условие в левой части является ложным. - Это хорошо заметно, когда выражение, указанное в качестве второго аргумента, имеет побочный эффект, например, изменение переменной. +## ||= (Логическое присваивание ИЛИ) - В приведённом ниже примере `x` не изменяется: +[recent browser="new"] - ```js run no-beautify - let x; +Оператор логического присваивания ИЛИ `||=` записывается как обычный ИЛИ `||` с добавлением символа присваивания `=`. Такая запись не случайна, так как результат выполнения данного оператора напрямую зависит от действий уже известного нам `||`. - *!*true*/!* || (x = 1); +Вот его синтаксис: - alert(x); // undefined, потому что (x = 1) не вычисляется - ``` +```js +a ||= b; +``` - Если бы первый аргумент имел значение `false`, то `||` приступил бы к вычислению второго и выполнил операцию присваивания: +Оператор `||=` принимает два операнда и выполняет следующие действия: - ```js run no-beautify - let x; +- Вычисляет операнды слева направо. +- Конвертирует `a` в логическое значение. +- Если `a` ложно, присваивает `a` значение `b`. - *!*false*/!* || (x = 1); +Концепция оператора `||=` заключается в «сокращённом вычислении», принцип работы которого мы разобрали ранее. - alert(x); // 1 - ``` +Теперь давайте перепишем `a ||= b` под вид «сокращённого вычисления»: + +```js +a || (a = b); +``` + +Мы уже знаем, что ИЛИ `||` возвращает *первое истинное значение*, поэтому, если `a` является таковым, вычисление до правой части выражения не дойдёт. + +Вот пример с очевидным использованием оператора `||=`: + +```js run +let johnHasCar = false; + +johnHasCar ||= "У Джона нет машины!"; // то же самое, что false || (johnHasCar = "...") + +alert( johnHasCar ); // "У Джона нет машины!" +``` + +...А здесь происходит преобразование к логическому значению: + +```js run +let manufacturer = ""; // оператор ||= преобразует пустую строку "" к логическому значению false + +manufacturer ||= "Неизвестный производитель"; // то же самое, что false || (manufacturer = "...") + +alert( manufacturer ); // "Неизвестный производитель" +``` + +Оператор логического присваивания ИЛИ `||=` -- это «[синтаксический сахар](https://ru.wikipedia.org/wiki/Синтаксический_сахар)», добавленный в язык в качестве более короткого варианта записи `if`-выражений с присваиванием. + +Мы можем переписать приведённые выше примеры с использованием обычного `if`: + +```js run +let johnHasCar = false; + +if (johnHasCar == false) { + johnHasCar = "У Джона нет машины!"; +} - Присваивание - лишь один пример. Конечно, могут быть и другие побочные эффекты, которые не проявятся, если вычисление до них не дойдёт. +alert(johnHasCar); // "У Джона нет машины!" - Как мы видим, этот вариант использования `||` является "аналогом `if`". Первый операнд преобразуется в логический. Если он оказывается ложным, начинается вычисление второго. +let manufacturer = ""; - В большинстве случаев лучше использовать "обычный" `if`, чтобы облегчить понимание кода, но иногда это может быть удобно. +if (manufacturer == false) { + manufacturer = "Неизвестный производитель"; +} + +alert(manufacturer); // "Неизвестный производитель" +``` ## && (И) @@ -173,7 +237,7 @@ let hour = 12; let minute = 30; if (hour == 12 && minute == 30) { - alert( 'The time is 12:30' ); + alert( 'Время 12:30' ); } ``` @@ -186,7 +250,7 @@ if (1 && 0) { // вычисляется как true && false ``` -## И "&&" находит первое ложное значение +### И "&&" находит первое ложное значение При нескольких подряд операторах И: @@ -215,7 +279,7 @@ alert( 1 && 5 ); // 5 // Если первый операнд ложный, // И возвращает его. Второй операнд игнорируется alert( null && 5 ); // null -alert( 0 && "no matter what" ); // 0 +alert( 0 && "какая-то строка" ); // 0 ``` Можно передать несколько значений подряд. В таком случае возвратится первое "ложное" значение, на котором остановились вычисления. @@ -236,32 +300,73 @@ alert( 1 && 2 && 3 ); // 3 Таким образом, код `a && b || c && d` по существу такой же, как если бы выражения `&&` были в круглых скобках: `(a && b) || (c && d)`. ```` -Как и оператор ИЛИ, И `&&` иногда может заменять `if`. +````warn header="Не заменяйте `if` на `||` или `&&`" +Иногда люди используют оператор И `&&` как «более короткий способ записи `if`-выражения». -К примеру: +Например: ```js run let x = 1; -(x > 0) && alert( 'Greater than zero!' ); +(x > 0) && alert( 'x больше нуля!' ); ``` -Действие в правой части `&&` выполнится только в том случае, если до него дойдут вычисления. То есть, `alert` сработает, если в левой части (`x > 0)` будет `true`. - +Инструкция в правой части `&&` будет выполнена только в том случае, если вычисление дойдет до нее. То есть, только если `(x > 0)` истинно. -Получился аналог: +Таким образом, мы имеем аналог для следующего кода: ```js run let x = 1; -if (x > 0) { - alert( 'Greater than zero!' ); -} +if (x > 0) alert( 'x больше нуля!' ); +``` + +Несмотря на то, что вариант с `&&` кажется более коротким, `if` более нагляден и, как правило, более читабелен. Поэтому мы рекомендуем использовать каждую конструкцию по назначению: использовать `if`, если нам нужно `if`, и использовать `&&`, если нам нужно И. +```` + +## &&= (Логическое присваивание И) + +[recent browser="new"] + +Оператор логического присваивания И `&&=` записывается как два амперсанда `&&` и символ присваивания `=`. + +Вот его синтаксис: + +```js +a &&= b; ``` -Однако, как правило, вариант с `if` лучше читается и воспринимается. +Принцип действия `&&=` практически такой же, как и у оператора логического присваивания ИЛИ `||=`. Единственное отличие заключается в том, что `&&=` присвоит `a` значение `b` только в том случае, если `a` *истинно*. + +Концепция оператора логического присваивания И `&&=` также основывается на «сокращённом вычислении»: + +```js +a && (a = b); +``` + +Пример использования: + +```js run +let greeting = "Привет"; // строка непустая, поэтому будет преобразована к логическому значению true оператором &&= + +greeting &&= greeting + ", пользователь!"; // то же самое, что true && (greeting = greeting + "...") + +alert( greeting ) // "Привет, пользователь!" +``` + +Так как оператор логического присваивания И `&&=` также как и `||=` является «синтаксическим сахаром», мы можем без проблем переписать пример выше с использованием привычного для нас `if`: + +```js run +let greeting = "Привет"; + +if (greeting) { + greeting = greeting + ", пользователь!" +} + +alert( greeting ) // "Привет, пользователь!" +``` -Он более очевиден, поэтому лучше использовать его. +На практике, в отличие от `||=`, оператор `&&=` используется достаточно редко -- обычно, в комбинации с более сложными языковыми конструкциями, о которых мы будем говорить позже. Подобрать контекст для применения данного оператора -- довольно непростая задача. ## ! (НЕ) @@ -285,10 +390,10 @@ alert( !true ); // false alert( !0 ); // true ``` -В частности, двойное НЕ используют для преобразования значений к логическому типу: +В частности, двойное НЕ `!!` используют для преобразования значений к логическому типу: ```js run -alert( !!"non-empty string" ); // true +alert( !!"непустая строка" ); // true alert( !!null ); // false ``` @@ -297,7 +402,7 @@ alert( !!null ); // false Есть немного более подробный способ сделать то же самое - встроенная функция `Boolean`: ```js run -alert( Boolean("non-empty string") ); // true +alert( Boolean("непустая строка") ); // true alert( Boolean(null) ); // false ``` diff --git a/1-js/02-first-steps/12-nullish-operators/1-values/solution.md b/1-js/02-first-steps/12-nullish-operators/1-values/solution.md new file mode 100644 index 0000000000..1843f30bbb --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/1-values/solution.md @@ -0,0 +1,5 @@ +Ответ: `NaN`. Это первое «определённое» значение. + +```js run +alert(undefined ?? NaN ?? null ?? "" ?? " "); +``` diff --git a/1-js/02-first-steps/12-nullish-operators/1-values/task.md b/1-js/02-first-steps/12-nullish-operators/1-values/task.md new file mode 100644 index 0000000000..7fb00dd3d4 --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/1-values/task.md @@ -0,0 +1,11 @@ +importance: 3 + +--- + +# Что выведет этот код? + +Что выведет код ниже? + +```js +alert(undefined ?? NaN ?? null ?? "" ?? " "); +``` \ No newline at end of file diff --git a/1-js/02-first-steps/12-nullish-operators/2-cities/solution.md b/1-js/02-first-steps/12-nullish-operators/2-cities/solution.md new file mode 100644 index 0000000000..c9db57266f --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/2-cities/solution.md @@ -0,0 +1,14 @@ +Ответ: `"Берлин"`. + +Первое присваивание `city ??= "Берлин"` срабатывает, поскольку изначально `city` — это `null`. После присваивания все остальные действия с оператором `??=` становятся бессмысленными, так как теперь `city` содержит «определённое» значение. + +```js run +let city = null; + +city ??= "Берлин"; +city ??= null; +city ??= "Кёльн"; +city ??= "Гамбург"; + +alert(city); +``` \ No newline at end of file diff --git a/1-js/02-first-steps/12-nullish-operators/2-cities/task.md b/1-js/02-first-steps/12-nullish-operators/2-cities/task.md new file mode 100644 index 0000000000..b2017e8a21 --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/2-cities/task.md @@ -0,0 +1,18 @@ +importance: 3 + +--- + +# Какой будет результат выполнения этого кода? + +Что будет выведено в итоге? + +```js +let city = null; + +city ??= "Берлин"; +city ??= null; +city ??= "Кёльн"; +city ??= "Гамбург"; + +alert(city); +``` \ No newline at end of file diff --git a/1-js/02-first-steps/12-nullish-operators/3-rewrite/solution.md b/1-js/02-first-steps/12-nullish-operators/3-rewrite/solution.md new file mode 100644 index 0000000000..efec8bb84e --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/3-rewrite/solution.md @@ -0,0 +1,7 @@ +```js +let num1 = 10, + num2 = 20, + result; + +*!*result ??= num1 ?? num2;*/!* +``` \ No newline at end of file diff --git a/1-js/02-first-steps/12-nullish-operators/3-rewrite/task.md b/1-js/02-first-steps/12-nullish-operators/3-rewrite/task.md new file mode 100644 index 0000000000..63a65dd47b --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/3-rewrite/task.md @@ -0,0 +1,23 @@ +importance: 3 + +--- + +# Перепишите код используя операторы ??, ??= + +Перепишите этот код используя операторы нулевого слияния и присваивания. + +```js +let num1 = 10, + num2 = 20, + result; + +*!* +if (result === null || result === undefined) { + if (num1 !== null && num1 !== undefined) { + result = num1; + } else { + result = num2; + } +} +*/!* +``` diff --git a/1-js/02-first-steps/12-nullish-operators/article.md b/1-js/02-first-steps/12-nullish-operators/article.md new file mode 100644 index 0000000000..273955f793 --- /dev/null +++ b/1-js/02-first-steps/12-nullish-operators/article.md @@ -0,0 +1,215 @@ +# Операторы нулевого слияния и присваивания: '??', '??=' + +[recent browser="new"] + +## Оператор нулевого слияния (??) + +Оператор нулевого слияния представляет собой два вопросительных знака `??`. + +Так как он обрабатывает `null` и `undefined` одинаковым образом, то для этой статьи мы введём специальный термин. Для краткости будем говорить, что значение "определено", если оно не равняется ни `null`, ни `undefined`. + +Результат выражения `a ?? b` будет следующим: +- если `a` определено, то `a`, +- если `a` не определено, то `b`. + +Иначе говоря, оператор `??` возвращает первый аргумент, если он не `null/undefined`, иначе второй. + +Оператор нулевого слияния не является чем-то принципиально новым. Это всего лишь удобный синтаксис, как из двух значений получить одно, которое "определено". + +Вот как можно переписать выражение `result = a ?? b`, используя уже знакомые нам операторы: + +```js +result = (a !== null && a !== undefined) ? a : b; +``` + +Теперь должно быть абсолютно ясно, что делает `??`. Давайте посмотрим, где это может быть полезно. + +Как правило, оператор `??` нужен для того, чтобы задать значение по умолчанию для потенциально неопределённой переменной. + +Например, здесь мы отобразим `user`, если её значение не `null/undefined`, в противном случае `Аноним`: + +```js run +let user; + +alert(user ?? "Аноним"); // Аноним (user не существует) +``` + +А вот пример, когда `user` присвоено значение: + +```js run +let user = "Иван"; + +alert(user ?? "Аноним"); // Иван (user существует) +``` + +Кроме этого, можно записать последовательность из операторов `??`, чтобы получить первое значение из списка, которое не является `null/undefined`. + +Допустим, у нас есть данные пользователя в переменных `firstName`, `lastName` или `nickName`. Все они могут не существовать, если пользователь решил не вводить соответствующие значение. + +Мы хотели бы отобразить имя пользователя, используя одну из этих переменных, или показать "Аноним", если все они `null/undefined`. + +Для этого воспользуемся оператором `??`: + +```js run +let firstName = null; +let lastName = null; +let nickName = "Суперкодер"; + +// показывает первое значение, которое определено: +*!* +alert(firstName ?? lastName ?? nickName ?? "Аноним"); // Суперкодер +*/!* +``` + +### Сравнение с || + +Оператор ИЛИ `||` можно использовать для того же, что и `??`, как это было показано в [предыдущей главе](info:logical-operators#or-finds-the-first-truthy-value). + +Например, если в приведённом выше коде заменить `??` на `||`, то будет тот же самый результат: + +```js run +let firstName = null; +let lastName = null; +let nickName = "Суперкодер"; + +// показывает первое истинное значение: +*!* +alert(firstName || lastName || nickName || "Аноним"); // Суперкодер +*/!* +``` + +Исторически сложилось так, что оператор ИЛИ `||` появился первым. Он существует с самого начала в JavaScript, поэтому разработчики долгое время использовали его для таких целей. + +С другой стороны, сравнительно недавно в язык был добавлен оператор нулевого слияния `??` - как раз потому, что многие были недовольны оператором `||`. + +Важное различие между ними заключается в том, что: +- `||` возвращает первое *истинное* значение. +- `??` возвращает первое *определённое* значение. + +Проще говоря, оператор `||` не различает `false`, `0`, пустую строку `""` и `null/undefined`. Для него они все одинаковы, т.е. являются ложными значениями. Если первым аргументом для оператора `||` будет любое из перечисленных значений, то в качестве результата мы получим второй аргумент. + +Однако на практике часто требуется использовать значение по умолчанию только тогда, когда переменная является `null/undefined`. Ведь именно тогда значение действительно неизвестно/не определено. + +Рассмотрим следующий пример: + +```js run +let height = 0; + +alert(height || 100); // 100 +alert(height ?? 100); // 0 +``` + +- `height || 100` проверяет `height` на ложное значение, оно равно `0`, да, ложное. + - поэтому результатом `||` является второй аргумент, т.е. `100`. +- `height ?? 100` проверяет, что переменная `height` содержит `null/undefined`, а поскольку это не так, + - то результатом является сама переменная `height`, т.е. `0`. + +На практике нулевая высота часто является вполне нормальным значением, которое не следует заменять значением по умолчанию. Таким образом, `??` здесь как раз работает так, как нужно. + +### Приоритет + +Приоритет оператора `??` такой же, как и у `||`. Они оба равны `3` в [таблице на MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table). + +Это означает, что, как и `||`, оператор нулевого слияния `??` вычисляется до `=` и `?`, но после большинства других операций, таких как `+`, `*`. + +Так что, в выражениях такого вида понадобятся скобки: + +```js run +let height = null; +let width = null; + +// важно: используйте круглые скобки +let area = (height ?? 100) * (width ?? 50); + +alert(area); // 5000 +``` + +Иначе, если опустить скобки, оператор `*` выполнится первым, так как у него приоритет выше, чем у `??`, и это приведёт к неправильным результатам. + +```js +// без скобок +let area = height ?? 100 * width ?? 50; + +// ...сработает вот так (совсем не как нам нужно): +let area = height ?? (100 * width) ?? 50; +``` + +### Использование ?? вместе с && или || + +По соображениям безопасности JavaScript запрещает использование оператора `??` вместе с `&&` и `||`, если приоритет явно не указан при помощи круглых скобок. + +Выполнение следующего кода приведёт к синтаксической ошибке: + +```js run +let x = 1 && 2 ?? 3; // Синтаксическая ошибка +``` + +Это, безусловно, спорное ограничение было добавлено в спецификацию языка с целью избежать программные ошибки, когда люди начнут переходить с `||` на `??`. + +Используйте скобки, чтобы обойти это ограничение: + +```js run +*!* +let x = (1 && 2) ?? 3; // Работает без ошибок +*/!* + +alert(x); // 2 +``` + +## Оператор нулевого присваивания (??=) + +Предположим, нам необходимо проверить, равна ли переменная `null` или `undefined`, и если это так — присвоить этой переменной какое-либо другое значение. + +Вот как мы сделали бы это сейчас: + +```js +let userAge = null; + +if (userAge === null || userAge === undefined) { + userAge = 18; +} +``` + +Выглядит громоздко, правда? Существует оператор, более подходящий для подобных задач. Вот его синтаксис: + +```js +x ??= y +``` + +Оператор `??=` присвоит `x` значение `y` только в том случае, если `x` *не определено* (`null`/`undefined`). + +Теперь попробуем переписать уже знакомый нам фрагмент кода используя новый оператор: + +```js run +let userAge = null; + +userAge ??= 18; + +alert(userAge) // 18 +``` + +Обратите внимание: если бы `userAge` не был равен `null`/`undefined`, то выражение справа от `??=` никогда бы не выполнилось: + +```js run +let userAge = 18; + +userAge ??= alert("не сработает"); +userAge ??= 21; +userAge ??= null; + +alert(userAge) // по-прежнему 18 +``` + +## Итого + +- Оператор нулевого слияния `??` — это быстрый способ выбрать первое "определённое" значение из списка. + + Используется для присвоения переменным значений по умолчанию: + + ```js + // будет height=100, если переменная height равна null или undefined + height = height ?? 100; + ``` +- Оператор `??` имеет очень низкий приоритет, лишь немного выше, чем у `?` и `=`, поэтому при использовании его в выражении, скорее всего, потребуются скобки. +- Запрещено использовать вместе с `||` или `&&` без явно указанного приоритета, то есть без скобок. +- Для присвоения переменной значения в зависимости от того, "определена" она или нет, используется оператор нулевого присваивания `??=`. diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/task.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/task.md diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md similarity index 68% rename from 1-js/02-first-steps/12-while-for/2-which-value-while/solution.md rename to 1-js/02-first-steps/13-while-for/2-which-value-while/solution.md index 050b1ad13e..2f053d259d 100644 --- a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md +++ b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md @@ -19,12 +19,12 @@ while (i++ < 5) alert( i ); ``` - Первое значение: `i = 1`. Остановимся на нём подробнее. Оператор `i++` увеличивает `i`, возвращая старое значение, так что в сравнении `i++ < 5` будет участвовать старое `i = 0`. + Первое значение: `i = 1`. Остановимся на нём подробнее. Оператор `i++` увеличивает `i`, возвращая старое значение, так что в сравнении `i++ < 5` будет участвовать `i = 0` (в отличие от `++i < 5`). Но последующий вызов `alert` уже не относится к этому выражению, так что получит новый `i = 1`. - Далее `2, 3, 4…` Для каждого значения сначала происходит сравнение, а потом – увеличение, и затем срабатывание `alert`. + Далее следуют `2, 3, 4…`. - Окончание цикла: при `i = 4` произойдёт сравнение `while (4 < 5)` – верно, после этого сработает `i++`, увеличив `i` до `5`, так что значение `5` будет выведено. Оно станет последним. + Остановимся на `i = 4`. Префиксная форма `++i` увеличила бы `i` и использовала бы в сравнении `5`. Но здесь мы имеем постфиксную форму `i++`, поэтому она увеличивает `i` до `5`, но возвращает старое значение. Таким образом, сравнение фактически равно `while (4 < 5)` -- `true`, поэтому срабатывает `alert`. - Значение `i = 5` последнее, потому что на следующем шаге `while (5 < 5)` ложно. + Значение `i = 5` -- последнее, так как на следующем шаге `while (5 < 5)` -- `false`. diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/task.md b/1-js/02-first-steps/13-while-for/2-which-value-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/2-which-value-while/task.md rename to 1-js/02-first-steps/13-while-for/2-which-value-while/task.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md b/1-js/02-first-steps/13-while-for/3-which-value-for/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/solution.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/solution.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/task.md b/1-js/02-first-steps/13-while-for/3-which-value-for/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/task.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/task.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/solution.md b/1-js/02-first-steps/13-while-for/4-for-even/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/solution.md rename to 1-js/02-first-steps/13-while-for/4-for-even/solution.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/task.md b/1-js/02-first-steps/13-while-for/4-for-even/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/task.md rename to 1-js/02-first-steps/13-while-for/4-for-even/task.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/task.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/task.md diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md similarity index 90% rename from 1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md rename to 1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md index 87c80c60b8..721300cb0a 100644 --- a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md +++ b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md @@ -3,7 +3,7 @@ let num; do { - num = prompt("Введите число, большее 100?", 0); + num = prompt("Введите число больше 100?", 0); } while (num <= 100 && num); ``` diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md rename to 1-js/02-first-steps/13-while-for/6-repeat-until-correct/task.md diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/solution.md b/1-js/02-first-steps/13-while-for/7-list-primes/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/7-list-primes/solution.md rename to 1-js/02-first-steps/13-while-for/7-list-primes/solution.md diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/task.md b/1-js/02-first-steps/13-while-for/7-list-primes/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/7-list-primes/task.md rename to 1-js/02-first-steps/13-while-for/7-list-primes/task.md diff --git a/1-js/02-first-steps/12-while-for/article.md b/1-js/02-first-steps/13-while-for/article.md similarity index 84% rename from 1-js/02-first-steps/12-while-for/article.md rename to 1-js/02-first-steps/13-while-for/article.md index 7ba0c3740d..b7f3d4f67c 100644 --- a/1-js/02-first-steps/12-while-for/article.md +++ b/1-js/02-first-steps/13-while-for/article.md @@ -6,6 +6,19 @@ Для многократного повторения одного участка кода предусмотрены *циклы*. +```smart header="Циклы for..of и for..in" +Небольшое объявление для продвинутых читателей. + +В этой статье рассматриваются только базовые циклы: `while`, `do..while` и `for(..;..;..)`. + +Если вы пришли к этой статье в поисках других типов циклов, вот указатели: + +- См. [for..in](info:object#forin) для перебора свойств объекта. +- См. [for..of](info:array#perebor-elementov) и [Перебираемые объекты](info:iterable) для перебора массивов и перебираемых объектов. + +В противном случае, продолжайте читать. +``` + ## Цикл "while" Цикл `while` имеет следующий синтаксис: @@ -106,22 +119,22 @@ for (let i = 0; i < 3; i++) { // выведет 0, затем 1, затем 2 | часть | | | |-------|----------|----------------------------------------------------------------------------| -| *начало* | `i = 0` | Выполняется один раз при входе в цикл | -| *условие* | `i < 3`| Проверяется *перед* каждой итерацией цикла. Если оно вычислится в `false`, цикл остановится. | -| *шаг* | `i++` | Выполняется *после* тела цикла на каждой итерации *перед* проверкой условия. | -| *тело* | `alert(i)`| Выполняется снова и снова, пока условие вычисляется в `true`. | +| начало | `let i = 0` | Выполняется один раз при входе в цикл | +| условие | `i < 3`| Проверяется *перед* каждой итерацией цикла.
Если оно вычислится в `false`, цикл остановится. | +| тело | `alert(i)` | Выполняется снова и снова, пока условие вычисляется в `true`. | +| шаг | `i++`| Выполняется *после* тела цикла на каждой итерации *перед* проверкой условия. | В целом, алгоритм работы цикла выглядит следующим образом: ``` -Выполнить *начало* -→ (Если *условие* == true → Выполнить *тело*, Выполнить *шаг*) -→ (Если *условие* == true → Выполнить *тело*, Выполнить *шаг*) -→ (Если *условие* == true → Выполнить *тело*, Выполнить *шаг*) +Выполнить начало +→ (Если условие == true → Выполнить тело, Выполнить шаг) +→ (Если условие == true → Выполнить тело, Выполнить шаг) +→ (Если условие == true → Выполнить тело, Выполнить шаг) → ... ``` -То есть, *начало* выполняется один раз, а затем каждая итерация заключается в проверке *условия*, после которой выполняется *тело* и *шаг*. +То есть, `начало` выполняется один раз, а затем каждая итерация заключается в проверке `условия`, после которой выполняется `тело` и `шаг`. Если тема циклов для вас нова, может быть полезным вернуться к примеру выше и воспроизвести его работу на листе бумаги, шаг за шагом. @@ -162,10 +175,8 @@ for (i = 0; i < 3; i++) { // используем существующую пе alert(i); // 3, переменная доступна, т.к. была объявлена снаружи цикла ``` - ```` - ### Пропуск частей "for" Любая часть `for` может быть пропущена. @@ -212,7 +223,7 @@ for (;;) { Например, следующий код подсчитывает сумму вводимых чисел до тех пор, пока посетитель их вводит, а затем – выдаёт: -```js +```js run let sum = 0; while (true) { @@ -231,7 +242,7 @@ alert( 'Сумма: ' + sum ); Директива `break` в строке `(*)` полностью прекращает выполнение цикла и передаёт управление на строку за его телом, то есть на `alert`. -Вообще, сочетание «бесконечный цикл + `break`» – отличная штука для тех ситуаций, когда условие, по которому нужно прерваться, находится не в начале или конце цикла, а посередине. +Вообще, сочетание «бесконечный цикл + `break`» – отличная штука для тех ситуаций, когда условие, по которому нужно прерваться, находится не в начале или конце цикла, а посередине или даже в нескольких местах его тела. ## Переход к следующей итерации: continue [#continue] @@ -256,7 +267,7 @@ for (let i = 0; i < 10; i++) { ````smart header="Директива `continue` позволяет избегать вложенности" Цикл, который обрабатывает только нечётные значения, мог бы выглядеть так: -```js +```js run for (let i = 0; i < 10; i++) { if (i % 2) { @@ -286,7 +297,6 @@ if (i > 5) { ...и перепишем его, используя вопросительный знак: - ```js no-beautify (i > 5) ? alert(i) : *!*continue*/!*; // continue здесь приведёт к ошибке ``` @@ -310,18 +320,18 @@ for (let i = 0; i < 3; i++) { let input = prompt(`Значение на координатах (${i},${j})`, ''); // Что если мы захотим перейти к Готово (ниже) прямо отсюда? - } } alert('Готово!'); ``` -Нам нужен способ остановить выполнение если пользователь отменит ввод. +Нам нужен способ остановить выполнение, если пользователь отменит ввод. Обычный `break` после `input` лишь прервёт внутренний цикл, но этого недостаточно. Достичь желаемого поведения можно с помощью меток. *Метка* имеет вид идентификатора с двоеточием перед циклом: + ```js labelName: for (...) { ... @@ -347,7 +357,9 @@ labelName: for (...) { alert('Готово!'); ``` -В примере выше это означает, что вызовом `break outer` будет разорван внешний цикл до метки с именем `outer`, и управление перейдёт со строки, помеченной `(*)`, к `alert('Готово!')`. +В примере выше это означает, что вызовом `break outer` будет разорван внешний цикл до метки с именем `outer`. + +Таким образом управление перейдёт со строки, помеченной `(*)`, к `alert('Готово!')`. Можно размещать метку на отдельной строке: @@ -362,13 +374,26 @@ for (let i = 0; i < 3; i++) { ... } Метки не дают возможности передавать управление в произвольное место кода. Например, нет возможности сделать следующее: + ```js break label; // не прыгает к метке ниже label: for (...) ``` -Вызов `break/continue` возможен только внутри цикла, и метка должна находиться где-то выше этой директивы. +Директива `break` должна находиться внутри блока кода. Технически, подойдет любой маркированный блок кода, например: + +```js +label: { + // ... + break label; // работает + // ... +} +``` + +...Хотя в 99.9% случаев `break` используется внутри циклов, как мы видели в примерах выше. + +К слову, `continue` возможно только внутри цикла. ```` ## Итого diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md diff --git a/1-js/02-first-steps/13-switch/article.md b/1-js/02-first-steps/14-switch/article.md similarity index 99% rename from 1-js/02-first-steps/13-switch/article.md rename to 1-js/02-first-steps/14-switch/article.md index 551d990f8f..7f356dc2da 100644 --- a/1-js/02-first-steps/13-switch/article.md +++ b/1-js/02-first-steps/14-switch/article.md @@ -117,7 +117,7 @@ switch (+a) { Для примера, выполним один и тот же код для `case 3` и `case 5`, сгруппировав их: ```js run no-beautify -let a = 2 + 2; +let a = 3; switch (a) { case 4: diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md b/1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md rename to 1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/task.md b/1-js/02-first-steps/15-function-basics/1-if-else-required/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/1-if-else-required/task.md rename to 1-js/02-first-steps/15-function-basics/1-if-else-required/task.md diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md rename to 1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md rename to 1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/solution.md b/1-js/02-first-steps/15-function-basics/3-min/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/solution.md rename to 1-js/02-first-steps/15-function-basics/3-min/solution.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/task.md b/1-js/02-first-steps/15-function-basics/3-min/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/task.md rename to 1-js/02-first-steps/15-function-basics/3-min/task.md diff --git a/1-js/02-first-steps/14-function-basics/4-pow/solution.md b/1-js/02-first-steps/15-function-basics/4-pow/solution.md similarity index 92% rename from 1-js/02-first-steps/14-function-basics/4-pow/solution.md rename to 1-js/02-first-steps/15-function-basics/4-pow/solution.md index 4375dab037..a374abc83e 100644 --- a/1-js/02-first-steps/14-function-basics/4-pow/solution.md +++ b/1-js/02-first-steps/15-function-basics/4-pow/solution.md @@ -13,9 +13,9 @@ function pow(x, n) { let x = prompt("x?", ''); let n = prompt("n?", ''); -if (n < 1) { - alert(`Степень ${n} не поддерживается, используйте натуральное число`); -} else { +if (n >= 1 && n % 1 == 0) { alert( pow(x, n) ); +} else { + alert(`Степень ${n} не поддерживается, используйте натуральное число`); } ``` diff --git a/1-js/02-first-steps/14-function-basics/4-pow/task.md b/1-js/02-first-steps/15-function-basics/4-pow/task.md similarity index 77% rename from 1-js/02-first-steps/14-function-basics/4-pow/task.md rename to 1-js/02-first-steps/15-function-basics/4-pow/task.md index de3b84475f..e4e513c313 100644 --- a/1-js/02-first-steps/14-function-basics/4-pow/task.md +++ b/1-js/02-first-steps/15-function-basics/4-pow/task.md @@ -4,7 +4,7 @@ importance: 4 # Функция pow(x,n) -Напишите функцию `pow(x,n)`, которая возвращает `x` в степени `n`. Иначе говоря, умножает `x` на себя `n` раз и возвращает результат. +Напишите функцию `pow(x,n)`, которая возводит `x` в степень `n` и возвращает результат. ```js pow(3, 2) = 3 * 3 = 9 diff --git a/1-js/02-first-steps/14-function-basics/article.md b/1-js/02-first-steps/15-function-basics/article.md similarity index 83% rename from 1-js/02-first-steps/14-function-basics/article.md rename to 1-js/02-first-steps/15-function-basics/article.md index 3665281087..4f3bacf687 100644 --- a/1-js/02-first-steps/14-function-basics/article.md +++ b/1-js/02-first-steps/15-function-basics/article.md @@ -28,7 +28,7 @@ function имя(параметры) { } ``` -Наша новая функция может быть вызвана по её имени: `showMessage()`. +Наша новая функция может быть вызвана по своему имени: `showMessage()`. Например: @@ -137,12 +137,12 @@ alert( userName ); // *!*Вася*/!*, не изменилась, функция ## Параметры -Мы можем передать внутрь функции любую информацию, используя параметры (также называемые *аргументы функции*). +Мы можем передать внутрь функции любую информацию, используя параметры. В нижеприведённом примере функции передаются два параметра: `from` и `text`. ```js run -function showMessage(*!*from, text*/!*) { // аргументы: from, text +function showMessage(*!*from, text*/!*) { // параметры: from, text alert(from + ': ' + text); } @@ -175,9 +175,20 @@ showMessage(from, "Привет"); // *Аня*: Привет alert( from ); // Аня ``` -## Параметры по умолчанию +Значение, передаваемое в качестве параметра функции, также называется *аргументом*. -Если параметр не указан, то его значением становится `undefined`. +Другими словами: + +- Параметр - это переменная, указанная в круглых скобках в объявлении функции. +- Аргумент - это значение, которое передаётся функции при её вызове. + +Мы объявляем функции со списком параметров, затем вызываем их, передавая аргументы. + +Рассматривая приведённый выше пример, мы могли бы сказать: "функция `showMessage` объявляется с двумя параметрами, затем вызывается с двумя аргументами: `from` и `"Привет"`". + +## Значения по умолчанию + +Если при вызове функции аргумент не был указан, то его значением становится `undefined`. Например, вышеупомянутая функция `showMessage(from, text)` может быть вызвана с одним аргументом: @@ -185,7 +196,7 @@ alert( from ); // Аня showMessage("Аня"); ``` -Это не приведёт к ошибке. Такой вызов выведет `"Аня: undefined"`. В вызове не указан параметр `text`, поэтому предполагается, что `text === undefined`. +Это не приведёт к ошибке. Такой вызов выведет `"*Аня*: undefined"`. В вызове не указан параметр `text`, поэтому предполагается, что `text === undefined`. Если мы хотим задать параметру `text` значение по умолчанию, мы должны указать его после `=`: @@ -209,9 +220,11 @@ function showMessage(from, text = anotherFunction()) { ``` ```smart header="Вычисление параметров по умолчанию" -В JavaScript параметры по умолчанию вычисляются каждый раз, когда функция вызывается без соответствующего параметра. +В JavaScript параметры по умолчанию вычисляются каждый раз, когда функция вызывается без соответствующего аргумента. + +В приведённом выше примере, функция `anotherFunction()` не будет вызвана вообще, если указан аргумент `text`. -В примере выше `anotherFunction()` будет вызываться каждый раз, когда `showMessage()` вызывается без параметра `text`. +С другой стороны, функция будет независимо вызываться каждый раз, когда аргумент `text` отсутствует. ``` ````smart header="Использование параметров по умолчанию в ранних версиях JavaScript" @@ -236,12 +249,53 @@ function showMessage(from, text) { ```js function showMessage(from, text) { // Если значение text ложно, тогда присвоить параметру text значение по умолчанию + // заметим, что при этом пустая строка text === "" будет также считаться отсутствующим значением text = text || 'текст не добавлен'; ... } ``` ```` +### Альтернативные параметры по умолчанию + +Иногда имеет смысл присваивать значения по умолчанию для параметров не в объявлении функции, а на более позднем этапе. + +Во время выполнения функции мы можем проверить, передан ли параметр, сравнив его с `undefined`: + +```js run +function showMessage(text) { + // ... +*!* + if (text === undefined) { // если параметр отсутствует + text = 'пустое сообщение'; + } +*/!* + alert(text); +} +showMessage(); // пустое сообщение +``` + +...Или мы можем использовать оператор `||`: + +```js +function showMessage(text) { + // если значение text ложно или равняется undefined, тогда присвоить text значение 'пусто' + text = text || 'пусто'; + ... +} +``` + +Современные движки JavaScript поддерживают [оператор нулевого слияния](info:nullish-operators) `??`. Его использование будет лучшей практикой, в случае, если большинство ложных значений, таких как `0`, следует расценивать как "нормальные". + +```js run +function showCount(count) { + // если count равен undefined или null, показать "неизвестно" + alert(count ?? "неизвестно"); +} +showCount(0); // 0 +showCount(null); // неизвестно +showCount(); // неизвестно +``` ## Возврат значения @@ -264,7 +318,7 @@ alert( result ); // 3 ```js run function checkAge(age) { - if (age > 18) { + if (age >= 18) { *!* return true; */!* @@ -354,7 +408,7 @@ return ( ## Выбор имени функции [#function-naming] -Функция - это действие. Поэтому имя функции обычно является глаголом. Оно должно быть простым, точным и описывать действие функции, чтобы программист, который будет читать код, получил верное представление о том, что делает функция. +Функция - это действие. Поэтому имя функции обычно является глаголом. Оно должно быть кратким, точным и описывать действие функции, чтобы программист, который будет читать код, получил верное представление о том, что делает функция. Как правило, используются глагольные префиксы, обозначающие общий характер действия, после которых следует уточнение. Обычно в командах разработчиков действуют соглашения, касающиеся значений этих префиксов. @@ -371,13 +425,13 @@ return ( ```js no-beautify showMessage(..) // показывает сообщение -getAge(..) // возвращает возраст (в каком либо значении) +getAge(..) // возвращает возраст (получая его каким-то образом) calcSum(..) // вычисляет сумму и возвращает результат createForm(..) // создаёт форму (и обычно возвращает её) checkPermission(..) // проверяет доступ, возвращая true/false ``` -Благодаря префиксам, при первом взгляде на имя функции становится понятным что делает её код, и какое значение она может возвращать. +Благодаря префиксам, при первом взгляде на имя функции становится понятным, что делает её код, и какое значение она может возвращать. ```smart header="Одна функция -- одно действие" Функция должна делать только то, что явно подразумевается её названием. И это должно быть одним действием. @@ -390,13 +444,13 @@ checkPermission(..) // проверяет доступ, возвращая true/ - `createForm` -- будет плохим выбором, если функция будет изменять документ, добавляя форму в него (должна только создавать форму и возвращать её). - `checkPermission` -- будет плохим выбором, если функция будет отображать сообщение с текстом `доступ разрешён/запрещён` (должна только выполнять проверку и возвращать её результат). -В этих примерах использовались общепринятые смыслы префиксов. Конечно, вы в команде можете договориться о других значениях, но обычно они мало отличаются от общепринятых. В любом случае вы и ваша команда должны точно понимать, что значит префикс, что функция с ним может делать, а чего не может. +В этих примерах использовались общепринятые смыслы префиксов. Конечно, вы в команде можете договориться о других значениях, но обычно они мало отличаются от общепринятых. В любом случае вы и ваша команда должны чётко понимать, что значит префикс, что функция с ним может делать, а чего не может. ``` ```smart header="Сверхкороткие имена функций" Имена функций, которые используются *очень часто*, иногда делают сверхкороткими. -Например, во фреймворке [jQuery](http://jquery.com) есть функция с именем `$`. В библиотеке [Lodash](http://lodash.com/) основная функция представлена именем `_`. +Например, фреймворк [jQuery](https://jquery.com) определяет функцию с помощью `$`. В библиотеке [Lodash](https://lodash.com/) основная функция представлена именем `_`. Это исключения. В основном имена функций должны быть в меру краткими и описательными. ``` diff --git a/1-js/02-first-steps/15-function-expressions-arrows/article.md b/1-js/02-first-steps/16-function-expressions/article.md similarity index 51% rename from 1-js/02-first-steps/15-function-expressions-arrows/article.md rename to 1-js/02-first-steps/16-function-expressions/article.md index 0dc084d6ef..795217fc39 100644 --- a/1-js/02-first-steps/15-function-expressions-arrows/article.md +++ b/1-js/02-first-steps/16-function-expressions/article.md @@ -1,4 +1,4 @@ -# Function Expression и функции-стрелки +# Function Expression Функция в JavaScript - это не магическая языковая структура, а особого типа значение. @@ -12,7 +12,9 @@ function sayHi() { Существует ещё один синтаксис создания функций, который называется *Function Expression* (Функциональное Выражение). -Оно выглядит вот так: +Данный синтаксис позволяет нам создавать новую функцию в середине любого выражения. + +Это выглядит следующим образом: ```js let sayHi = function() { @@ -20,11 +22,21 @@ let sayHi = function() { }; ``` -В коде выше функция создаётся и явно присваивается переменной, как любое другое значение. По сути без разницы, как мы определили функцию, это просто значение, хранимое в переменной `sayHi`. +Здесь мы можем видеть переменную `sayHi`, получающую значение, новую функцию, созданную как `function() { alert("Привет"); }`. + +Поскольку создание функции происходит в контексте выражения присваивания (с правой стороны от `=`), это *Function Expression*. + +Обратите внимание, что после ключевого слова `function` нет имени. Для Function Expression допускается его отсутствие. + +Здесь мы сразу присваиваем её переменной, так что смысл этих примеров кода один и тот же: "создать функцию и поместить её в переменную `sayHi`". + +В более сложных ситуациях, с которыми мы столкнёмся позже, функция может быть создана и немедленно вызвана, или запланирована для дальнейшего выполнения, нигде не сохраняясь, таким образом, оставаясь анонимной. -Смысл обоих примеров кода одинаков: "создать функцию и поместить её значение в переменную `sayHi`". +## Функция - это значение -Мы можем даже вывести это значение с помощью `alert`: +Давайте повторим: независимо от того, как создаётся функция - она является значением. В обоих приведённых выше примерах функция хранится в переменной `sayHi`. + +Мы даже можем вывести это значение с помощью `alert`: ```js run function sayHi() { @@ -35,14 +47,13 @@ function sayHi() { alert( sayHi ); // выведет код функции */!* ``` +Обратите внимание, что последняя строка не вызывает функцию, потому что после `sayHi` нет круглых скобок. Существуют языки программирования, в которых любое упоминание имени функции приводит к её выполнению, но JavaScript к таким не относится. -Обратите внимание, что последняя строка не вызывает функцию `sayHi`, после её имени нет круглых скобок. Существуют языки программирования, в которых любое упоминание имени функции совершает её вызов. JavaScript - не один из них. - -В JavaScript функции - это значения, поэтому мы и обращаемся с ними, как со значениями. Код выше выведет строковое представление функции, которое является её исходным кодом. +В JavaScript функция - это значение, поэтому мы можем обращаться с ней как со значением. Приведённый выше код показывает её строковое представление, которое является её исходным кодом. -Конечно, функция - не обычное значение, в том смысле, что мы можем вызвать его при помощи скобок: `sayHi()`. +Конечно, функция - это особое значение, в том смысле, что мы можем вызвать её как `sayHi()`. -Но всё же это значение. Поэтому мы можем делать с ним то же самое, что и с любым другим значением. +Но всё же это значение. Поэтому мы можем работать с ней так же, как и с другими видами значений. Мы можем скопировать функцию в другую переменную: @@ -53,20 +64,20 @@ function sayHi() { // (1) создаём let func = sayHi; // (2) копируем -func(); // Привет // (3) вызываем копию (работает)! -sayHi(); // Привет // прежняя тоже работает (почему бы нет) +func(); // Привет // (3) вызываем копию (работает)! +sayHi(); // Привет // эта тоже все ещё работает (почему бы и нет) ``` Давайте подробно разберём всё, что тут произошло: -1. Объявление Function Declaration `(1)` создало функцию и присвоило её значение переменной с именем `sayHi`. +1. Объявление Function Declaration `(1)` создаёт функцию и помещает её в переменную с именем `sayHi`. 2. В строке `(2)` мы скопировали её значение в переменную `func`. Обратите внимание (ещё раз): нет круглых скобок после `sayHi`. Если бы они были, то выражение `func = sayHi()` записало бы *результат вызова* `sayHi()` в переменную `func`, а не саму *функцию* `sayHi`. -3. Теперь функция может быть вызвана с помощью обеих переменных `sayHi()` и `func()`. +3. Теперь функция может вызываться как `sayHi()`, так и `func()`. -Заметим, что мы могли бы использовать и Function Expression для того, чтобы создать `sayHi` в первой строке: +Мы также могли бы использовать Function Expression для объявления `sayHi` в первой строке: ```js -let sayHi = function() { +let sayHi = function() { // (1) создаём alert( "Привет" ); }; @@ -74,7 +85,7 @@ let func = sayHi; // ... ``` -Результат был бы таким же. +Всё будет работать так же. ````smart header="Зачем нужна точка с запятой в конце?" У вас мог возникнуть вопрос: Почему в Function Expression ставится точка с запятой `;` на конце, а в Function Declaration нет: @@ -89,14 +100,14 @@ let sayHi = function() { }*!*;*/!* ``` -Ответ прост: -- Нет необходимости в `;` в конце блоков кода и синтаксических конструкций, которые их используют, таких как `if { ... }`, `for { }`, `function f { }` и т.д. -- Function Expression использует внутри себя инструкции присваивания `let sayHi = ...;` как значение. Это не блок кода, а выражение с присваиванием. Таким образом, точка с запятой не относится непосредственно к Function Expression, она лишь завершает инструкцию. +Ответ прост: Function Expression создаётся здесь как `function(...) {...}` внутри выражения присваивания: `let sayHi = …;`. Точку с запятой `;` рекомендуется ставить в конце выражения, она не является частью синтаксиса функции. + +Точка с запятой нужна там для более простого присваивания, такого как `let sayHi = 5;`, а также для присваивания функции. ```` ## Функции-"колбэки" -Рассмотрим ещё примеры функциональных выражений и передачи функции как значения. +Давайте рассмотрим больше примеров передачи функции в виде значения и использования функциональных выражений. Давайте напишем функцию `ask(question, yes, no)` с тремя параметрами: @@ -104,7 +115,7 @@ let sayHi = function() { : Текст вопроса `yes` -: Функция, которая будет вызваться, если ответ будет "Yes" +: Функция, которая будет вызываться, если ответ будет "Yes" `no` : Функция, которая будет вызываться, если ответ будет "No" @@ -133,9 +144,9 @@ ask("Вы согласны?", showOk, showCancel); На практике подобные функции очень полезны. Основное отличие "реальной" функции `ask` от примера выше будет в том, что она использует более сложные способы взаимодействия с пользователем, чем простой вызов `confirm`. В браузерах такие функции обычно отображают красивые диалоговые окна. Но это уже другая история. -**Аргументы функции `ask` ещё называют *функциями-колбэками* или просто *колбэками*.** +**Аргументы `showOk` и `showCancel` функции `ask` называются *функциями-колбэками* или просто *колбэками*.** -Ключевая идея в том, что мы передаём функцию и ожидаем, что она вызовется обратно (от англ. "call back" - обратный вызов) когда-нибудь позже, если это будет необходимо. В нашем случае, `showOk` становится *колбэком*' для ответа "yes", а `showCancel` -- для ответа "no". +Ключевая идея в том, что мы передаём функцию и ожидаем, что она вызовется обратно (от англ. "call back" - обратный вызов) когда-нибудь позже, если это будет необходимо. В нашем случае, `showOk` становится *колбэком* для ответа "yes", а `showCancel` -- для ответа "no". Мы можем переписать этот пример значительно короче, используя Function Expression: @@ -164,7 +175,7 @@ ask( ```smart header="Функция - это значение, представляющее \"действие\"" Обычные значения, такие как строки или числа представляют собой *данные*. -Функции, с другой стороны, можно воспринимать как "действия". +Функции, с другой стороны, можно воспринимать как *действия*. Мы можем передавать их из переменной в переменную и запускать, когда захотим. ``` @@ -174,9 +185,9 @@ ask( Давайте разберём ключевые отличия Function Declaration от Function Expression. -Во-первых, синтаксис: как определить, что есть что в коде. +Во-первых, синтаксис: как отличить их друг от друга в коде. -- Function Declaration: функция объявляется отдельной конструкцией "function..." в основном потоке кода. +- *Function Declaration*: функция объявляется отдельной конструкцией "function..." в основном потоке кода. ```js // Function Declaration @@ -184,7 +195,7 @@ ask( return a + b; } ``` -- Function Expression: функция, созданная внутри другого выражения или синтаксической конструкции. В данном случае функция создаётся в правой части "выражения присваивания" `=`: +- *Function Expression*: функция, созданная внутри другого выражения или синтаксической конструкции. В данном случае функция создаётся в правой части "выражения присваивания" `=`: ```js // Function Expression @@ -193,7 +204,7 @@ ask( }; ``` -Более тонкое отличие состоит, в том, *когда* создаётся функция движком JavaScript. +Более тонкое отличие состоит в том, *когда* создаётся функция движком JavaScript. **Function Expression создаётся, когда выполнение доходит до него, и затем уже может использоваться.** @@ -201,13 +212,13 @@ ask( С Function Declaration всё иначе. -**Function Declaration можно использовать во всем скрипте (или блоке кода, если функция объявлена в блоке).** +**Function Declaration может быть вызвана раньше, чем она объявлена.** Другими словами, когда движок JavaScript *готовится* выполнять скрипт или блок кода, прежде всего он ищет в нём Function Declaration и создаёт все такие функции. Можно считать этот процесс "стадией инициализации". И только после того, как все объявления Function Declaration будут обработаны, продолжится выполнение. -В результате, функции, созданные, как Function Declaration могут быть вызваны раньше своих определений. +В результате функции, созданные как Function Declaration, могут быть вызваны раньше своих определений. Например, так будет работать: @@ -223,7 +234,7 @@ function sayHi(name) { Функция `sayHi` была создана, когда движок JavaScript подготавливал скрипт к выполнению, и такая функция видна повсюду в этом скрипте. -...Если бы это было Function Expression, то такой код вызовет ошибку: +...Если бы это было Function Expression, то такой код вызвал бы ошибку: ```js run refresh untrusted *!* @@ -241,9 +252,9 @@ let sayHi = function(name) { // (*) магии больше нет **В строгом режиме, когда Function Declaration находится в блоке `{...}`, функция доступна везде внутри блока. Но не снаружи него.** -Для примера давайте представим, что нам нужно создать функцию `welcome()` в зависимости от значения переменной `age`, которое мы получим во время выполнения кода. И затем запланируем использовать её когда-нибудь в будущем. +Для примера давайте представим, что нам нужно объявить функцию `welcome()` в зависимости от значения переменной `age`, которое мы получим во время выполнения кода. И затем запланируем использовать её когда-нибудь в будущем. -Такой код, использующий Function Declaration, работать не будет: +Если мы попробуем использовать Function Declaration, это не заработает так, как задумывалось: ```js run let age = prompt("Сколько Вам лет?", 18); @@ -274,7 +285,7 @@ welcome(); // Error: welcome is not defined Вот ещё один пример: ```js run -let age = 16; // присвоим для примера 16 +let age = 16; // возьмём для примера 16 if (age < 18) { *!* @@ -308,7 +319,7 @@ welcome(); // Ошибка: welcome is not defined Верным подходом будет воспользоваться функцией, объявленной при помощи Function Expression, и присвоить значение `welcome` переменной, объявленной снаружи `if`, что обеспечит нам нужную видимость. -Такой код работает, как ожидалось: +Такой код заработает, как ожидалось: ```js run let age = prompt("Сколько Вам лет?", 18); @@ -334,7 +345,7 @@ welcome(); // теперь всё в порядке */!* ``` -Можно упростить этот код ещё сильнее, используя условный оператор `?`: +Или мы могли бы упростить это ещё сильнее, используя условный оператор `?`: ```js run let age = prompt("Сколько Вам лет?", 18); @@ -354,122 +365,17 @@ welcome(); // теперь всё в порядке Также функции вида `function f(…) {…}` чуть более заметны в коде, чем `let f = function(…) {…}`. Function Declaration легче "ловятся глазами". -...Но если Function Declaration нам не подходит по какой-то причине (мы рассмотрели это в примере выше), то можно использовать объявление при помощи Function Expression. -``` - - -## Функции-стрелки [#arrow-functions] - -Существует ещё более простой и краткий синтаксис для создания функций, который часто лучше, чем синтаксис Function Expression. Он называется "функции-стрелки" или "стрелочные функции" (arrow functions), т.к. выглядит следующим образом: - - -```js -let func = (arg1, arg2, ...argN) => expression -``` - -...Такой код создаёт функцию `func` с аргументами `arg1..argN` и вычисляет `expression` с правой стороны с их использованием, возвращая результат. - -Другими словами, это почти то же самое, что и: - -```js -let func = function(arg1, arg2, ...argN) { - return expression; -}; -``` - -...но гораздо короче. - -Давайте взглянем на пример: - -```js run -let sum = (a, b) => a + b; - -/* Функция-стрелка более краткая форма для: - -let sum = function(a, b) { - return a + b; -}; -*/ - -alert( sum(1, 2) ); // 3 - -``` - -Если у нас только один аргумент, то круглые скобки вокруг параметров можно опустить, сделав запись ещё короче: - -```js run -// тоже что и -// let double = function(n) { return n * 2 } -*!* -let double = n => n * 2; -*/!* - -alert( double(3) ); // 6 -``` - -Если нет аргументов, используются пустые круглые скобки (их указывать обязательно): - -```js run -let sayHi = () => alert("Hello!"); - -sayHi(); -``` - -Функции-стрелки могут быть использованы так же, как и Function Expression. - -Например, вот переписанный пример уже знакомой нам функции `welcome()`: - -```js run -let age = prompt("Сколько Вам лет?", 18); - -let welcome = (age < 18) ? - () => alert('Привет') : - () => alert("Здравствуйте!"); - -welcome(); // теперь всё в порядке -``` - -Поначалу функции-стрелки могут показаться необычными и трудночитаемыми, но это быстро пройдёт, как только глаза привыкнут к этим конструкциям. - -Они очень удобны для простых однострочных действий, когда лень писать много букв. - -```smart header="Многострочные стрелочные функции" -В примерах выше аргументы использовались слева от `=>`, а справа вычислялось выражение с их значениями. - -Порой нам нужно кое-что более сложное, возможно, несколько выражений или инструкций. Это также возможно, но мы должны заключать такие выражения в фигурные скобки с использованием директивы `return` внутри них, как в обычной функции. - -Например: - -```js run -let sum = (a, b) => { // фигурная скобка, открывающая тело многострочной функции - let result = a + b; -*!* - return result; // при фигурных скобках нужен return, чтобы вернуть результат -*/!* -}; - -alert( sum(1, 2) ); // 3 -``` - -```smart header="Кое-что ещё" -Здесь мы рассмотрели функции-стрелки как способ писать меньше букв. Но это далеко не всё! Стрелочные функции обладают другими интересными особенностями. Мы вернёмся к ним позже в главе . - -А пока мы можем использовать их для простых однострочных действий и колбэков. +...Но если Function Declaration нам не подходит по какой-то причине, или нам нужно условное объявление (мы рассмотрели это в примере выше), то следует использовать Function Expression. ``` ## Итого -- Функции - это значения. Они могут быть присвоены, скопированы или объявлены в другом месте кода. -- Если функция объявлена как отдельная инструкция в основном потоке кода, то это Function Declaration. -- Если функция была создана как часть выражения, то считается, что эта функция объявлена при помощи Function Expression. +- Функции - это значения. Они могут быть присвоены, скопированы или объявлены в любом месте кода. +- Если функция объявлена как отдельная инструкция в основном потоке кода, то это “Function Declaration”. +- Если функция была создана как часть выражения, то это “Function Expression”. - Function Declaration обрабатываются перед выполнением блока кода. Они видны во всём блоке. -- Функции, объявленные при помощи Function Expression, создаются, только когда поток выполнения достигает их. - -В большинстве случаев, когда нам нужно создать функцию, предпочтительно использовать Function Declaration, т.к. функция будет видима до своего объявления в коде. Это позволяет более гибко организовывать код, и улучшает его читаемость. - -Таким образом, мы должны прибегать к объявлению функций при помощи Function Expression в случае, когда синтаксис Function Declaration не подходит для нашей задачи. Мы рассмотрели несколько таких примеров в этой главе, и рассмотрим их ещё больше в будущем. +- Функции, объявленные при помощи Function Expression, создаются только когда поток выполнения достигает их. -Функции-стрелки очень удобны для однострочных действий. Они бывают двух типов: +В большинстве случаев, когда нам нужно объявить функцию, Function Declaration предпочтительнее, т.к функция будет видна до своего объявления в коде. Это даёт нам больше гибкости в организации кода, и, как правило, делает его более читабельным. -1. Без фигурных скобок: `(...args) => expression` -- правая сторона выражение: функция выполняет его и возвращает результат. -2. С фигурными скобками: `(...args) => { body }` -- скобки позволяют нам писать многострочные инструкции внутри функции, но при этом необходимо указывать директиву `return`, чтобы вернуть какое-либо значение. +Исходя из этого, мы должны использовать Function Expression только тогда, когда Function Declaration не подходит для нашей задачи. Мы рассмотрели несколько таких примеров в этой главе, и увидим ещё больше в будущем. diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md similarity index 100% rename from 1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md rename to 1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md similarity index 100% rename from 1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md rename to 1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md diff --git a/1-js/02-first-steps/17-arrow-functions-basics/article.md b/1-js/02-first-steps/17-arrow-functions-basics/article.md new file mode 100644 index 0000000000..25bc971ce9 --- /dev/null +++ b/1-js/02-first-steps/17-arrow-functions-basics/article.md @@ -0,0 +1,112 @@ +# Стрелочные функции, основы + +Существует ещё один очень простой и лаконичный синтаксис для создания функций, который часто лучше, чем Function Expression. + +Он называется "функции-стрелки" или "стрелочные функции" (arrow functions), т.к. выглядит следующим образом: + +```js +let func = (arg1, arg2, ...argN) => expression; +``` + +Это создаёт функцию `func`, которая принимает аргументы `arg1..argN`, затем вычисляет `expression` в правой части с их использованием и возвращает результат. + +Другими словами, это сокращённая версия: + +```js +let func = function(arg1, arg2, ...argN) { + return expression; +}; +``` + +Давайте рассмотрим конкретный пример: + +```js run +let sum = (a, b) => a + b; + +/* Эта стрелочная функция представляет собой более короткую форму: + +let sum = function(a, b) { + return a + b; +}; +*/ + +alert( sum(1, 2) ); // 3 + +``` + +Как вы можете видеть, `(a, b) => a + b` задаёт функцию, которая принимает два аргумента с именами `a` и `b`. И при выполнении она вычисляет выражение `a + b` и возвращает результат. + +- Если у нас только один аргумент, то круглые скобки вокруг параметров можно опустить, сделав запись ещё короче: + + ```js run + *!* + let double = n => n * 2; + // примерно то же, что и: let double = function(n) { return n * 2 } + */!* + + alert( double(3) ); // 6 + ``` + +- Если аргументов нет, круглые скобки будут пустыми, но они должны присутствовать: + + ```js run + let sayHi = () => alert("Hello!"); + + sayHi(); + ``` + +Стрелочные функции можно использовать так же, как и Function Expression. + +Например, для динамического создания функции: + +```js run +let age = prompt("Сколько Вам лет?", 18); + +let welcome = (age < 18) ? + () => alert('Привет!') : + () => alert("Здравствуйте!"); + +welcome(); +``` + +Поначалу стрелочные функции могут показаться необычными и даже трудночитаемыми, но это быстро пройдёт по мере того, как глаза привыкнут к этим конструкциям. + +Они очень удобны для простых однострочных действий, когда лень писать много слов. + +## Многострочные стрелочные функции + +Стрелочные функции, которые мы видели до этого, были очень простыми. Они брали аргументы слева от `=>` и вычисляли и возвращали выражение справа. + +Иногда нам нужна более сложная функция, с несколькими выражениями и инструкциями. Это также возможно, нужно лишь заключить их в фигурные скобки. При этом важное отличие - в том, что в таких скобках для возврата значения нужно использовать `return` (как в обычных функциях). + +Вроде этого: + +```js run +let sum = (a, b) => { // фигурная скобка, открывающая тело многострочной функции + let result = a + b; +*!* + return result; // если мы используем фигурные скобки, то нам нужно явно указать "return" +*/!* +}; + +alert( sum(1, 2) ); // 3 +``` + +```smart header="Дальше - больше" +Здесь мы представили главной целью стрелочных функций краткость. Но это ещё не всё! + +Стрелочные функции обладают и другими интересными возможностями. + +Чтобы изучить их более подробно, нам сначала нужно познакомиться с некоторыми другими аспектами JavaScript, поэтому мы вернёмся к стрелочным функциям позже, в главе . + +А пока мы можем использовать их для простых однострочных действий и колбэков. +``` + +## Итого + +Стрелочные функции очень удобны для простых действий, особенно для однострочных. + +Они бывают двух типов: + +1. Без фигурных скобок: `(...args) => expression` -- правая сторона выражения: функция вычисляет его и возвращает результат. Скобки можно не ставить, если аргумент только один: `n => n * 2`. +2. С фигурными скобками: `(...args) => { body }` -- скобки позволяют нам писать несколько инструкций внутри функции, но при этом необходимо явно вызывать `return`, чтобы вернуть значение. diff --git a/1-js/02-first-steps/16-javascript-specials/article.md b/1-js/02-first-steps/18-javascript-specials/article.md similarity index 92% rename from 1-js/02-first-steps/16-javascript-specials/article.md rename to 1-js/02-first-steps/18-javascript-specials/article.md index bfe1d5462d..4bd0206e36 100644 --- a/1-js/02-first-steps/16-javascript-specials/article.md +++ b/1-js/02-first-steps/18-javascript-specials/article.md @@ -31,7 +31,7 @@ alert("После этого сообщения ждите ошибку") ```js function f() { - // после объявления функции необязательно ставить точку с запятой + // после объявления функции необязательно ставить точку с запятой } for(;;) { @@ -81,9 +81,10 @@ let x = 5; x = "Вася"; ``` -Всего существует 7 типов данных: +Всего существует 8 типов данных: - `number` для целых и вещественных чисел, +- `bigint` для работы с целыми числами произвольной длины, - `string` для строк, - `boolean` для логических значений истинности или ложности: `true/false`, - `null` – тип с единственным значением `null`, т.е. "пустое значение" или "значение не существует", @@ -143,13 +144,16 @@ JavaScript поддерживает следующие операторы: : Простые `a = b` и составные `a *= 2`. Битовые операции -: Битовые операторы работают с 32-битными целыми числами на самом низком, побитовом уровне. Подробнее об их использовании можно прочитать на ресурсе [MDN](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators). +: Битовые операторы работают с 32-битными целыми числами на самом низком, побитовом уровне. Подробнее об их использовании можно прочитать на ресурсе [MDN](https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Expressions_and_Operators#битовые_(поразрядные)_операторы) и в разделе [Побитовые операторы](https://learn.javascript.ru/bitwise-operators). Условный оператор : Единственный оператор с тремя параметрами: `cond ? resultA : resultB`. Если условие `cond` истинно, возвращается `resultA`, иначе – `resultB`. Логические операторы -: Логические И `&&`, ИЛИ `||` используют так называемое "ленивое вычисление" и возвращают значение, на котором оно остановилось (не обязательно `true` или `false`). Логическое НЕТ `!` конвертирует операнд в логический тип и возвращает инвертированное значение. +: Логические И `&&`, ИЛИ `||` используют так называемое "ленивое вычисление" и возвращают значение, на котором оно остановилось (не обязательно `true` или `false`). Логическое НЕ `!` конвертирует операнд в логический тип и возвращает инвертированное значение. + +Оператор нулевого слияния +: Оператор `??` предоставляет способ выбора определённого значения из списка переменных. Результатом `a ?? b` будет `a`, если только оно не равно `null/undefined`, тогда `b`. Сравнение : Проверка на равенство `==` значений разных типов конвертирует их в число (за исключением `null` и `undefined`, которые могут равняться только друг другу), так что примеры ниже равны: @@ -170,7 +174,7 @@ JavaScript поддерживает следующие операторы: Другие операторы : Существуют и другие операторы, такие как запятая. -Подробности: , , . +Подробности: , , , . ## Циклы @@ -272,7 +276,7 @@ switch (age) { - У параметров могут быть значения по умолчанию: `function sum(a = 1, b = 2) {...}`. - Функции всегда что-нибудь возвращают. Если нет оператора `return`, результатом будет `undefined`. -Подробности: , . +Подробности: , . ## Далее мы изучим больше diff --git a/1-js/03-code-quality/01-debugging-chrome/article.md b/1-js/03-code-quality/01-debugging-chrome/article.md index 16ee7fa48d..7181d98caa 100644 --- a/1-js/03-code-quality/01-debugging-chrome/article.md +++ b/1-js/03-code-quality/01-debugging-chrome/article.md @@ -1,4 +1,4 @@ -# Отладка в браузере Chrome +# Отладка в браузере Давайте отвлечёмся от написания кода и поговорим о его отладке. @@ -12,25 +12,25 @@ - Работая в Chrome, откройте [тестовую страницу](debugging/index.html). - Включите инструменты разработчика, нажав `key:F12` (Mac: `key:Cmd+Opt+I`). -- Щёлкните по панели `sources` ("исходный код"). +- Щёлкните по панели `Sources` ("исходный код"). При первом запуске получаем следующее: ![](chrome-open-sources.svg) -Кнопка-переключатель откроет вкладку со списком файлов. +Кнопка-переключатель откроет вкладку со списком файлов. -Кликните на неё и выберите `hello.js`. Вот что появится: +Кликните на неё и выберите `hello.js` в дереве файлов. Вот что появится: ![](chrome-tabs.svg) Интерфейс состоит из трёх зон: -1. В зоне **Resources** (Ресурсы) показаны файлы HTML, JavaScript, CSS, включая изображения, используемые на странице. Здесь также могут быть файлы различных расширений Chrome. -2. Зона **Source** показывает исходный код. -3. Наконец, зона **Information and control** (Сведения и контроль) отведена для отладки, вскоре мы к ней вернёмся. +1. В зоне **File Navigator** (панель для навигации файлов) показаны файлы HTML, JavaScript, CSS, включая изображения, используемые на странице. Здесь также могут быть файлы различных расширений Chrome. +2. Зона **Code Editor** (редактор кода) показывает исходный код. +3. Наконец, зона **JavaScript Debugging** (панель отладки JavaScript) отведена для отладки, скоро мы к ней вернёмся. -Чтобы скрыть список ресурсов и освободить экранное место для исходного кода, щёлкните по тому же переключателю . +Чтобы скрыть список ресурсов и освободить экранное место для исходного кода, щёлкните по тому же переключателю . ## Консоль @@ -38,38 +38,37 @@ Результат выполнения инструкций сразу же отображается в консоли. -Например, результатом `1+2` будет `3`, а инструкция `hello("debugger")` ничего не возвращает, так что получаем `undefined`: +Например, результатом `1+2` будет `3`, а вызов функции `hello("debugger")` ничего не возвращает, так что результатом будет `undefined`: ![](chrome-sources-console.svg) ## Точки останова (breakpoints) -Давайте разберёмся, как работает код нашей [тестовой страницы](debugging/index.html). В файле `hello.js` щёлкните по строчке номер `4`. Да, щёлкайте именно по самой цифре, не по коду. +Давайте разберёмся, как работает код нашей [тестовой страницы](debugging/index.html). В файле `hello.js` щёлкните на номере строки `4`. Да-да, щёлкайте именно по самой цифре, не по коду. -Ура! Вы поставили точку останова. А теперь щёлкните по цифре `8` на восьмой линии. Номер строки будет окрашен в синий цвет. +Ура! Вы поставили точку останова. А теперь щёлкните по цифре `8` на восьмой линии. -Вот что в итоге должно получиться: +Вот что в итоге должно получиться (синим это те места, по которым вы должны щёлкнуть): ![](chrome-sources-breakpoint.svg) *Точка останова* – это участок кода, где отладчик автоматически приостановит исполнение JavaScript. -Пока исполнение поставлено "на паузу", мы можем просмотреть текущие значения переменных, выполнить команды в консоли, одним словом, мы выполняем отладку кода. +Пока исполнение поставлено "на паузу", мы можем просмотреть текущие значения переменных, выполнить команды в консоли, другими словами, выполнить отладку кода. В правой части графического интерфейса мы видим список точек останова. А когда таких точек выставлено много, да ещё и в разных файлах, этот список поможет эффективно ими управлять: -- Быстро переместиться к любой точке останова в коде – нужно щёлкнуть по точке в правой части экрана. -- Временно деактивировать точку – в общем списке снимите галочку напротив ненужной в данный момент точки. -- Удалить точку – щёлкните по ней правой кнопкой мыши и выберите Remove (Удалить). +- Быстро перейдите к точке останова в коде (нажав на неё на правой панели). +- Временно отключите точку останова, сняв с неё галочку. +- Удалите точку останова, щёлкнув правой кнопкой мыши и выбрав Remove (Удалить). - ...и так далее. ```smart header="Условные точки останова" -Можно задать и так называемую *условную* точку останова – щёлкните правой кнопкой мыши по номеру строки в коде. Если задать выражение, то именно при его истинности выполнение кода будет приостановлено. - -Этот метод используется, когда выполнение кода нужно остановить при присвоении определённого выражения какой-либо переменной или при определённых параметрах функции. +*Щелчок правой кнопкой мыши* по номеру строки позволяет создать *условную* точку останова. Она сработает только в тот момент, когда выражение, которое вы должны указать при создании такой точки, истинно. +Это удобно, когда нам нужно остановиться только при определённом значении переменной или для определённых параметров функции. ``` -## Команда Debugger +## Команда debugger Выполнение кода можно также приостановить с помощью команды `debugger` прямо изнутри самого кода: @@ -78,85 +77,97 @@ function hello(name) { let phrase = `Привет, ${name}!`; *!* - debugger; // <-- здесь выполнение прерывается + debugger; // <-- тут отладчик остановится */!* say(phrase); } ``` -Способ удобен тем, что можно продолжить работать в редакторе кода без необходимости переключения в браузер для выставления точки останова. - +Такая команда сработает только если открыты инструменты разработки, иначе браузер ее проигнорирует. ## Остановимся и оглядимся В нашем примере функция `hello()` вызывается во время загрузки страницы, поэтому для начала отладки (после того, как мы поставили точки останова) проще всего её перезагрузить. Нажмите `key:F5` (Windows, Linux) или `key:Cmd+R` (Mac). -Выполнение прервётся на четвёртой строчке: +Выполнение прервётся на четвёртой строчке (где находится точка останова): ![](chrome-sources-debugger-pause.svg) Чтобы понять, что происходит в коде, щёлкните по стрелочкам справа: -1. **`Watch` показывает текущие значения выражений.** +1. **`Watch`– показывает текущие значения для любых выражений.** - Нажмите на `+` и введите выражение. В процессе выполнения отладчик автоматически пересчитывает и выводит его значение. + Вы можете нажать на `+` и ввести выражение. Отладчик покажет его значение, автоматически пересчитывая его в процессе выполнения. -2. **`Call Stack` показывает последовательность вызовов функций.** +2. **`Call Stack` – показывает цепочку вложенных вызовов.** - В нашем примере отладчик работает с функцией `hello()`, вызванной скриптом из файла `index.html` (там нет функции, поэтому вызов "анонимный"). + В текущий момент отладчик находится внутри вызова `hello()`, вызываемого скриптом в `index.html` (там нет функции, поэтому она называется “анонимной”). - При нажатии на элемент списка (например, на "anonymous") отладчик переходит к соответствующему коду, и нам представляется возможность его проанализировать. + Если вы нажмёте на элемент стека (например, "anonymous"), отладчик перейдёт к соответствующему коду, и нам представляется возможность его проанализировать. 3. **`Scope` показывает текущие переменные.** - В `Local` отображаются локальные переменные функций, а их значения подсвечены в исходном коде. + `Local` показывает локальные переменные функций, а их значения подсвечены прямо в исходном коде. - В `Global` перечисляются глобальные переменные (т.е. объявленные за пределами функций). + В `Global` перечисляются глобальные переменные (то есть вне каких-либо функций). - Не обращайте пока внимание на ключевое слово `this` – его мы изучим чуть позже. + Там также есть ключевое слово `this`, которое мы ещё не изучали, но скоро изучим. ## Пошаговое выполнение скрипта -А теперь давайте *пошагаем* по нашему коду. - -В правой части панели для этого есть несколько кнопок. Рассмотрим их. +А теперь давайте *пошагаем* по нашему скрипту. - – продолжить выполнение. Быстрая клавиша – `key:F8`. -: Возобновляет выполнение кода. Если больше нет точек останова, отладчик прекращает работу и позволяет приложению работать дальше. +Для этого есть кнопки в верхней части правой панели. Давайте рассмотрим их. + + – "Resume": продолжить выполнение, быстрая клавиша `key:F8`. +: Возобновляет выполнение кода. Если больше нет точек останова, то выполнение просто продолжается, без контроля отладчиком. Вот, что мы увидим, кликнув на неё: ![](chrome-sources-debugger-trace-1.svg) - Выполнение кода возобновилось, дошло до другой точки останова внутри `say()`, и отладчик снова приостановил выполнение. Обратите внимание на пункт "Call stack" справа: в списке появился ещё один вызов. Мы теперь внутри функции `say()`. + Выполнение кода возобновилось, дошло до другой точки останова внутри `say()`, и отладчик снова приостановил выполнение. Обратите внимание на пункт "Call stack" справа: в списке появился ещё один вызов. Сейчас мы внутри `say()`. + + – "Step": выполнить следующую команду, быстрая клавиша `key:F9`. +: Выполняет следующую инструкцию. Если мы нажмём на неё сейчас, появится `alert`. + + Нажатие на эту кнопку снова и снова приведёт к пошаговому выполнению всех инструкций скрипта одного за другим. + + – "Step over": выполнить следующую команду, но *не заходя внутрь функции*, быстрая клавиша `key:F10`. +: Работает аналогично предыдущей команде "Step", но ведёт себя по-другому, если следующая инструкция является вызовом функции (имеется ввиду: не встроенная, как `alert`, а объявленная нами функция). + + Если сравнить, то команда "Step" переходит во вложенный вызов функцию и приостанавливает выполнение в первой строке, в то время как "Step over" выполняет вызов вложенной функции незаметно для нас, пропуская её внутренний код. + + Затем выполнение приостанавливается сразу после вызова функции. + + Это хорошо, если нам не интересно видеть, что происходит внутри вызова функции. - – сделать шаг (выполнить следующую команду), *не заходя в функцию*. Быстрая клавиша – `key:F10`. -: Если мы нажмём на неё - будет вызван `alert`. Важно: на месте `alert` может быть любая другая функция, выполнение просто *перешагнёт через неё*, полностью игнорируя её содержимое. + – "Step into", быстрая клавиша `key:F11`. +: Это похоже на "Step", но ведёт себя по-другому в случае асинхронных вызовов функций. Если вы только начинаете изучать JavaScript, то можете не обращать внимания на разницу, так как у нас ещё нет асинхронных вызовов. - – сделать шаг. Быстрая клавиша – `key:F11`. -: В отличие от предыдущего примера, здесь мы "заходим" во вложенные функции и шаг за шагом проходим по скрипту. + На будущее просто помните, что команда "Step" игнорирует асинхронные действия, такие как `setTimeout` (вызов функции по расписанию), которые выполняются позже. "Step into" входит в их код, ожидая их, если это необходимо. См. [DevTools manual](https://developers.google.com/web/updates/2018/01/devtools#async) для получения более подробной информации. - – продолжить выполнение до завершения текущей функции. Быстрая клавиша – `key:Shift+F11`. -: Выполнение кода остановится на самой последней строчке текущей функции. Этот метод применяется, когда мы случайно нажали и зашли в функцию, но нам она неинтересна и мы как можно скорее хотим из неё выбраться. + – "Step out": продолжить выполнение до завершения текущей функции, быстрая клавиша `key:Shift+F11`. +: Продолжает выполнение и останавливает его в самой последней строке текущей функции. Это удобно, когда мы случайно вошли во вложенный вызов, используя , но это нас не интересует, и мы хотим продолжить его до конца как можно скорее. - – активировать/деактивировать все точки останова. + – активировать/деактивировать все точки останова(breakpoints). : Эта кнопка не влияет на выполнение кода, она лишь позволяет массово включить/отключить точки останова. - – разрешить/запретить остановку выполнения в случае возникновения ошибки. -: Если опция включена и инструменты разработчика открыты, любая ошибка в скрипте приостанавливает выполнение кода, что позволяет его проанализировать. Поэтому если скрипт завершается с ошибкой, открываем отладчик, включаем эту опцию, перезагружаем страницу и локализуем проблему. + – включить/отключить автоматическую паузу в случае ошибки. +: При включении, если открыты инструменты разработчика, ошибка при выполнении скрипта автоматически приостанавливает его. Затем мы можем проанализировать переменные в отладчике, чтобы понять, что пошло не так. Поэтому, если наш скрипт умирает с ошибкой, мы можем открыть отладчик, включить эту опцию и перезагрузить страницу, чтобы увидеть, где он умирает и каков контекст в этот момент. ```smart header="Continue to here" -Если щёлкнуть правой кнопкой мыши по строчке кода, в контекстном меню можно выбрать опцию "Continue to here" ("продолжить до этого места"). +Щелчок правой кнопкой мыши по строке кода открывает контекстное меню с отличной опцией под названием "Continue to here" ("продолжить до этого места"). -Этот метод используется, когда нам нужно продвинуться на несколько шагов вперёд до нужной строки, но лень выставлять точки останова. +Это удобно, когда мы хотим перейти на несколько шагов вперёд к строке, но лень устанавливать точку останова (breakpoint). ``` ## Логирование -Если нужно что-то вывести в консоль из кода, применяется функция `console.log`. +Чтобы вывести что-то на консоль из нашего кода, существует функция `console.log`. -К примеру, выведем в консоль значения от нуля до четырёх: +Например, это выводит в консоль значения от `0` до `4`: ```js run // чтобы увидеть результат, сначала откройте консоль @@ -165,7 +176,7 @@ for (let i = 0; i < 5; i++) { } ``` -Обычный пользователь сайта не увидит такой вывод, так как он в консоли. Напомним, что консоль можно открыть через инструменты разработчика – выберите вкладку "Консоль" или нажмите `key:Esc`, находясь в другой вкладке – консоль откроется в нижней части интерфейса. +Обычный пользователь сайта не увидит такой вывод, так как он в консоли. Чтобы увидеть его, либо откройте консольную панель инструментов разработчика, либо нажмите `key:Esc`, находясь в другой панели: это откроет консоль внизу. Если правильно выстроить логирование в приложении, то можно и без отладчика разобраться, что происходит в коде. @@ -173,14 +184,14 @@ for (let i = 0; i < 5; i++) { Приостановить выполнение скрипта можно тремя способами: -1. Точками останова. -2. Использованием в коде команды `debugger`. -3. При ошибке (если инструменты разработчика открыты и опция включена). +1. Точками останова (breakpoints). +2. Использованием в коде команд `debugger`. +3. При ошибке (если инструменты разработчика открыты и кнопка "включена"). -При остановке мы можем отлаживать - проанализировать переменные и пошагово пройти по процессу, что поможет отыскать проблему. +При остановке мы можем отлаживать: анализировать переменные и пошагово пройти по процессу, чтобы отыскать проблему. -Нами описаны далеко не все инструменты разработчика. С полным руководством можно ознакомиться здесь: . +В инструментах разработчика гораздо больше опций, чем описано здесь. С полным руководством можно ознакомиться на . -Для простой отладки вполне достаточно сведений из этой главы, но в дальнейшем рекомендуем вам изучить официальное руководство, если вы собираетесь разрабатывать для браузеров. +Информации из этой главы достаточно, чтобы начать отладку, но позже, особенно если вы много работаете с браузером, пожалуйста, перейдите туда и ознакомьтесь с расширенными возможностями инструментов разработчика. -И, конечно, вы можете просто покликать в разных местах инструментов разработчика. Пожалуй, это наискорейший способ ими овладеть. Не забывайте про правый клик мыши и контекстные меню! +И, конечно, вы можете просто кликать по различным местам инструментов разработки и смотреть, что при этом появляется. Пожалуй, это наискорейший способ ими овладеть. Не забывайте про правый клик мыши и контекстные меню! diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg index bfad75b082..a850a00603 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg @@ -1 +1 @@ -открыть исходные файлы \ No newline at end of file +открыть исходные файлы \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg index 6d15881212..bef78e166d 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg @@ -1 +1 @@ -here's the listточки останова \ No newline at end of file +вот списокточки останова \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg index 39f4f91771..d5d2a0b934 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg index b738684381..c5d92a7564 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg @@ -1 +1 @@ -213выпрыгнуть во внешнюю функциюсмотреть за выражениямитекущие переменные \ No newline at end of file +213посмотреть детали внешнего вызовасмотреть за выражениямитекущие переменные \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg index e92b9c78e9..e9bccf2df7 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg @@ -1 +1 @@ -вложенные вызовы \ No newline at end of file +вложенные вызовы \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg index 4c76292314..41a3d8784b 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg @@ -1 +1 @@ -213 \ No newline at end of file +213 \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/head.html b/1-js/03-code-quality/01-debugging-chrome/head.html index f219b0af18..615326c08e 100644 --- a/1-js/03-code-quality/01-debugging-chrome/head.html +++ b/1-js/03-code-quality/01-debugging-chrome/head.html @@ -1,8 +1,8 @@ diff --git a/1-js/03-code-quality/01-debugging-chrome/largeIcons.svg b/1-js/03-code-quality/01-debugging-chrome/largeIcons.svg new file mode 100644 index 0000000000..83303365bd --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/largeIcons.svg @@ -0,0 +1,1472 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + a + b + c + d + e + f + g + h + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + + + + + + + + + + + diff --git a/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg b/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg deleted file mode 100644 index 5bdf20a83a..0000000000 --- a/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg +++ /dev/null @@ -1,1035 +0,0 @@ - -image/svg+xml \ No newline at end of file diff --git a/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md b/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md index 84126c3a96..26c8058e93 100644 --- a/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md +++ b/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md @@ -12,7 +12,7 @@ function pow(x,n) // <- отсутствует пробел между аргу let x=prompt("x?",''), n=prompt("n?",'') // <-- технически допустимо, // но лучше написать в 2 строки, также нет пробелов и точки с запятой -if (n<0) // <- нет пробелов, стоит добавить отступ в одну строку сверху +if (n<=0) // <- нет пробелов, стоит добавить отступ в одну строку сверху { // <- фигурная скобка на отдельной строке // ниже - слишком длинная строка, лучше разбить для улучшения читаемости alert(`Степень ${n} не поддерживается, введите целую степень, большую 0`); @@ -39,7 +39,7 @@ function pow(x, n) { let x = prompt("x?", ""); let n = prompt("n?", ""); -if (n < 0) { +if (n <= 0) { alert(`Степень ${n} не поддерживается, введите целую степень, большую 0`); } else { diff --git a/1-js/03-code-quality/02-coding-style/article.md b/1-js/03-code-quality/02-coding-style/article.md index c4a52cdf90..e7c3974760 100644 --- a/1-js/03-code-quality/02-coding-style/article.md +++ b/1-js/03-code-quality/02-coding-style/article.md @@ -65,7 +65,7 @@ if (condition) { if (n < 0) alert(`Степень ${n} не поддерживается`); ``` -3. 😏 В одну строку без скобок - приемлемо, если это строка короткая: +3. 😏 В одну строку без скобок - приемлемо, если эта строка короткая: ```js if (n < 0) alert(`Степень ${n} не поддерживается`); ``` @@ -109,7 +109,7 @@ if ( Существует два типа отступов: -- **Горизонтальные отступы: два или четыре пробела.** +- **Горизонтальные отступы: 2 или 4 пробела.** Горизонтальный отступ выполняется с помощью 2 или 4 пробелов, или символа табуляции (клавиша `key:Tab`). Какой из них выбрать - это уже на ваше усмотрение. Пробелы больше распространены. @@ -286,7 +286,7 @@ function pow(x, n) { - [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml) - [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) (есть [перевод](https://leonidlebedev.github.io/javascript-airbnb/)) -- [Idiomatic.JS](https://github.com/rwaldron/idiomatic.js) (есть [перевод](https://github.com/leonidlebedev/javascript-airbnb)) +- [Idiomatic.JS](https://github.com/rwaldron/idiomatic.js) (есть [перевод](https://github.com/rwaldron/idiomatic.js/tree/master/translations/ru_RU)) - [StandardJS](https://standardjs.com/) - (и ещё множество других) @@ -300,17 +300,17 @@ function pow(x, n) { Вот некоторые известные инструменты для проверки: -- [JSLint](http://www.jslint.com/) -- проверяет код на соответствие [стилю JSLint](http://www.jslint.com/lint.html), в онлайн-интерфейсе вверху можно ввести код, а внизу -- различные настройки проверки, чтобы попробовать её в действии. -- [JSHint](http://www.jshint.com/) -- больше проверок, чем в JSLint. -- [ESLint](http://eslint.org/) -- пожалуй, самый современный линтер. +- [JSLint](https://www.jslint.com/) -- проверяет код на соответствие [стилю JSLint](https://www.jslint.com/index.html), в онлайн-интерфейсе вверху можно ввести код, а внизу -- различные настройки проверки, чтобы попробовать её в действии. +- [JSHint](https://jshint.com/) -- больше проверок, чем в JSLint. +- [ESLint](https://eslint.org/) -- пожалуй, самый современный линтер. -Все они, в общем-то, работают. Автор пользуется [ESLint](http://eslint.org/). +Все они, в общем-то, работают. Автор пользуется [ESLint](https://eslint.org/). Большинство линтеров интегрированы со многими популярными редакторами: просто включите плагин в редакторе и настройте стиль. Например, для ESLint вы должны выполнить следующее: -1. Установите [Node.JS](https://nodejs.org/). +1. Установите [Node.js](https://nodejs.org/). 2. Установите ESLint с помощью команды `npm install -g eslint` (npm - установщик пакетов JavaScript). 3. Создайте файл конфигурации с именем `.eslintrc` в корне вашего JavaScript-проекта (в папке, содержащей все ваши файлы). 4. Установите/включите плагин для вашего редактора, который интегрируется с ESLint. У большинства редакторов он есть. diff --git a/1-js/03-code-quality/02-coding-style/code-style.svg b/1-js/03-code-quality/02-coding-style/code-style.svg index 75effb0e4f..d05c2bdf76 100644 --- a/1-js/03-code-quality/02-coding-style/code-style.svg +++ b/1-js/03-code-quality/02-coding-style/code-style.svg @@ -1 +1 @@ -2Без пробелов между именем функции и скобками между скобками и параметрамиОтступ 2 пробелаПробел после for/if/while…} else { без перевода строкиПробелы вокруг вложенного вызоваПустая строка Между логическими блокамиСтроки не очень длинныеТочка с запятой ; обязательнаПробелы вокруг операторовФигурная скобка { на той же строке, после пробелаПробел между параметрамиПробел между параметрами \ No newline at end of file +2Без пробелов между именем функции и скобками между скобками и параметрамиОтступ 2 пробелаПробел после for/if/while…} else { без перевода строкиПробелы вокруг вложенного вызоваПустая строка Между логическими блокамиСтроки не очень длинныеТочка с запятой ; обязательнаПробелы вокруг операторовФигурная скобка { на той же строке, после пробелаПробел между аргументамиПробел между параметрами diff --git a/1-js/03-code-quality/03-comments/article.md b/1-js/03-code-quality/03-comments/article.md index 749d18fae5..0f1e25b0c1 100644 --- a/1-js/03-code-quality/03-comments/article.md +++ b/1-js/03-code-quality/03-comments/article.md @@ -125,25 +125,25 @@ function addJuice(container) { Документируйте параметры и использование функций : Есть специальный синтаксис [JSDoc](https://ru.wikipedia.org/wiki/JSDoc) для документирования функций: использование, параметры, возвращаемое значение. - Например: - ```js - /** - * Возвращает x, возведённое в n-ную степень. - * - * @param {number} x Возводимое в степень число. - * @param {number} n Степень, должна быть натуральным числом. - * @return {number} x, возведённое в n-ную степень. - */ - function pow(x, n) { - ... - } - ``` +Например: +```js +/** + * Возвращает x, возведённое в n-ную степень. + * + * @param {number} x Возводимое в степень число. + * @param {number} n Степень, должна быть натуральным числом. + * @return {number} x, возведённое в n-ную степень. + */ +function pow(x, n) { + ... +} +``` - Подобные комментарии позволяют нам понимать назначение функции и правильно её использовать без необходимости заглядывать в код. +Подобные комментарии позволяют нам понимать назначение функции и правильно её использовать без необходимости заглядывать в код. - Кстати, многие редакторы, такие как [WebStorm](https://www.jetbrains.com/webstorm/), прекрасно их распознают для того, чтобы выполнить автодополнение ввода и различные автоматические проверки кода. +Кстати, многие редакторы, такие как [WebStorm](https://www.jetbrains.com/webstorm/), прекрасно их распознают для того, чтобы выполнить автодополнение ввода и различные автоматические проверки кода. - Также существуют инструменты, например, [JSDoc 3](https://github.com/jsdoc3/jsdoc), которые умеют генерировать HTML-документацию из комментариев. Получить больше информации о JSDoc вы можете здесь: . +Также существуют инструменты, например, [JSDoc 3](https://github.com/jsdoc/jsdoc), которые умеют генерировать HTML-документацию из комментариев. Получить больше информации о JSDoc вы можете здесь: . Почему задача решена именно таким способом? : Важно то, что написано. Но то, что *не* написано, может быть даже более важным, чтобы понимать происходящее. Почему задача решена именно этим способом? Код не даёт ответа. diff --git a/1-js/03-code-quality/04-ninja-code/article.md b/1-js/03-code-quality/04-ninja-code/article.md index b02cc57211..f8c4e5c16a 100644 --- a/1-js/03-code-quality/04-ninja-code/article.md +++ b/1-js/03-code-quality/04-ninja-code/article.md @@ -2,16 +2,16 @@ Предлагаю вашему вниманию советы мастеров древности. -Программисты прошлого использовали их, чтобы заострить разум тех, кто после них будет поддерживать код. +Ниндзя-разработчики прошлого использовали их, чтобы усложнить код и заострить разум тех, кто будет поддерживать его после них. -Гуру разработки при найме старательно ищут их применение в тестовых заданиях. +При найме проверяющие тщательно ищут их применение в коде соискателей. -Новички иногда используют их ещё лучше, чем матёрые ниндзя. +Новички иногда используют их ещё лучше, чем матёрые ниндзя! -Прочитайте их и решите, кто вы: ниндзя, новичок или, может быть, гуру? +Прочитайте их и решите, кто вы: новичок или ниндзя разработки? ```warn header="Осторожно, ирония!" -Многие пытались пройти по пути ниндзя. Мало, кто преуспел. +Многие пытались пройти по пути ниндзя! Мало кто преуспел. ``` ## Краткость – сестра таланта! @@ -37,7 +37,7 @@ i = i ? i < 0 ? Math.max(0, len + i) : i : 0; Ещё один способ писать быстрее - использовать короткие имена переменных. Называйте их `a`, `b` или `c`. -Короткая переменная прячется в коде лучше, чем ниндзя в лесу. Никто не сможет найти её, используя фунцию "Поиск" текстового редактора. Более того, даже найдя – никто не сможет "расшифровать" её и догадаться, что она означает. +Короткая переменная прячется в коде лучше, чем ниндзя в лесу. Никто не сможет найти её, используя функцию "Поиск" текстового редактора. Более того, даже найдя – никто не сможет "расшифровать" её и догадаться, что она означает. ...Но есть одно исключение. В тех местах, где однобуквенные переменные общеприняты, например, в счётчике цикла – ни в коем случае не используйте стандартные названия `i`, `j`, `k`. Где угодно, только не здесь! @@ -102,7 +102,7 @@ i = i ? i < 0 ? Math.max(0, len + i) : i : 0; **Назовите переменные "калькой" с русского языка или как-то "улучшите" английское слово.** -В одном месте напишите `var ssilka`, в другом `var ssylka`, в третьем `var link`, в четвёртом – `var lnk`… Это действительно великолепно работает и очень креативно! +В одном месте напишите `let ssilka`, в другом `let ssylka`, в третьем `let link`, в четвёртом – `let lnk`… Это действительно великолепно работает и очень креативно! Количество ошибок при поддержке такого кода увеличивается во много раз. diff --git a/1-js/03-code-quality/05-testing-mocha/article.md b/1-js/03-code-quality/05-testing-mocha/article.md index c329127e90..9279fcd7b3 100644 --- a/1-js/03-code-quality/05-testing-mocha/article.md +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -51,7 +51,7 @@ describe("pow", function() { Спецификация состоит из трёх основных блоков: `describe("заголовок", function() { ... })` -: Какой функционал мы описываем. В нашем случае мы описываем функцию `pow`. Используется для группировки рабочих лошадок -- блоков `it`. +: Какую функциональность мы описываем. В нашем случае мы описываем функцию `pow`. Используется для группировки рабочих лошадок -- блоков `it`. `it("описание", function() { ... })` : В первом аргументе блока `it` мы *человеческим языком* описываем конкретный способ использования функции, а во втором -- пишем функцию, которая тестирует данный случай. @@ -69,11 +69,11 @@ describe("pow", function() { 1. Пишется начальная спецификация с тестами, проверяющими основную функциональность. 2. Создаётся начальная реализация. -3. Для запуска тестов мы используем фреймворк [Mocha](http://mochajs.org/) (подробнее о нём чуть позже). Пока функция не готова, будут ошибки. Вносим изменения до тех пор, пока всё не начнёт работать так, как нам нужно. +3. Для запуска тестов мы используем фреймворк [Mocha](https://mochajs.org/) (подробнее о нём чуть позже). Пока функция не готова, будут ошибки. Вносим изменения до тех пор, пока всё не начнёт работать так, как нам нужно. 4. Теперь у нас есть правильно работающая начальная реализация и тесты. 5. Мы добавляем новые способы использования в спецификацию, возможно, ещё не реализованные в тестируемом коде. Тесты начинают "падать" (выдавать ошибки). 6. Возвращаемся на шаг 3, дописываем реализацию до тех пор, пока тесты не начнут завершаться без ошибок. -7. Повторяем шаги 3-6, пока требуемый функционал не будет готов. +7. Повторяем шаги 3-6, пока требуемая функциональность не будет готова. Таким образом, разработка проходит *итеративно*. Мы пишем спецификацию, реализуем её, проверяем, что тесты выполняются без ошибок, пишем ещё тесты, снова проверяем, что они проходят и т.д. @@ -85,9 +85,9 @@ describe("pow", function() { В этой главе мы будем пользоваться следующими JavaScript-библиотеками для тестов: -- [Mocha](http://mochajs.org/) -- основной фреймворк. Он предоставляет общие функции тестирования, такие как `describe` и `it`, а также функцию запуска тестов. -- [Chai](http://chaijs.com) -- библиотека, предоставляющая множество функций проверки утверждений. Пока мы будем использовать только `assert.equal`. -- [Sinon](http://sinonjs.org/) -- библиотека, позволяющая наблюдать за функциями, эмулировать встроенные функции и многое другое. Нам она пригодится позднее. +- [Mocha](https://mochajs.org/) -- основной фреймворк. Он предоставляет общие функции тестирования, такие как `describe` и `it`, а также функцию запуска тестов. +- [Chai](https://chaijs.com) -- библиотека, предоставляющая множество функций проверки утверждений. Пока мы будем использовать только `assert.equal`. +- [Sinon](https://sinonjs.org/) -- библиотека, позволяющая наблюдать за функциями, эмулировать встроенные функции и многое другое. Нам она пригодится позднее. Эти библиотеки подходят как для тестирования внутри браузера, так и на стороне сервера. Мы рассмотрим вариант с браузером. @@ -129,11 +129,11 @@ function pow(x, n) { ## Улучшаем спецификацию -Конечно, мы сжульничали. Функция не работает. Попытка посчитать `pow(3,4)` даст некорректный результат, однако тесты проходят. +Конечно, мы сжульничали. Функция не работает. Попытка посчитать `pow(3, 3)` даст некорректный результат, однако тесты проходят. ...Такая ситуация вполне типична, она случается на практике. Тесты проходят, но функция работает неправильно. Наша спецификация не идеальна. Нужно дополнить её тестами. -Давайте добавим ещё один тест, чтобы посмотреть, что `pow(3, 4) = 81`. +Давайте добавим ещё один тест, чтобы посмотреть, что `pow(3, 3) = 27`. У нас есть два пути организации тестов: @@ -145,7 +145,7 @@ function pow(x, n) { it("возводит число в степень n", function() { assert.equal(pow(2, 3), 8); *!* - assert.equal(pow(3, 4), 81); + assert.equal(pow(3, 3), 27); */!* }); @@ -302,7 +302,7 @@ describe("тест", function() { ## Расширение спецификации -Основной функционал `pow` реализован. Первая итерация разработки завершена. Когда мы закончим отмечать и пить шампанское, давайте продолжим работу и улучшим `pow`. +Основная функциональность `pow` реализована. Первая итерация разработки завершена. Когда мы закончим отмечать и пить шампанское, давайте продолжим работу и улучшим `pow`. Как было сказано, функция `pow(x, n)` предназначена для работы с целыми положительными значениями `n`. @@ -339,14 +339,14 @@ describe("pow", function() { ```smart header="Другие функции сравнения" Обратите внимание на `assert.isNaN`. Это проверка того, что переданное значение равно `NaN`. -Библиотека [Chai](http://chaijs.com) содержит множество других подобных функций, например: +Библиотека [Chai](https://chaijs.com) содержит множество других подобных функций, например: - `assert.equal(value1, value2)` -- проверяет равенство `value1 == value2`. - `assert.strictEqual(value1, value2)` -- проверяет строгое равенство `value1 === value2`. - `assert.notEqual`, `assert.notStrictEqual` -- проверяет неравенство и строгое неравенство соответственно. - `assert.isTrue(value)` -- проверяет, что `value === true` - `assert.isFalse(value)` -- проверяет, что `value === false` -- ...с полным списком можно ознакомиться в [документации](http://chaijs.com/api/assert/) +- ...с полным списком можно ознакомиться в [документации](https://chaijs.com/api/assert/) ``` Итак, нам нужно добавить пару строчек в функцию `pow`: @@ -388,7 +388,7 @@ function pow(x, n) { Это особенно важно в больших проектах, когда одна функция может быть использована во множестве мест. Когда мы вносим в такую функцию изменения, у нас нет никакой возможности вручную проверить, что она продолжает работать правильно во всех местах, где её используют. -Не имея тестов, людям приходится выбирать один их двух путей: +Не имея тестов, людям приходится выбирать один из двух путей: 1. Внести изменения, и неважно, что будет. Потом у наших пользователей станут проявляться ошибки, ведь мы наверняка что-то забудем проверить вручную. 2. Или же, если наказание за ошибки в коде серьёзное, то люди просто побоятся вносить изменения в такие функции. Код будет стареть, "зарастать паутиной", и никто не захочет в него лезть. Это нехорошо для разработки. diff --git a/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js index af9f0e6017..acd5a72b6a 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/pow-full.view/test.js @@ -19,7 +19,7 @@ describe("pow", function() { assert.isNaN(pow(2, -1)); }); - it("если n не число, результат будет NaN", function() { + it("если n - дробное число, результат будет NaN", function() { assert.isNaN(pow(2, 1.5)); }); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js index af9f0e6017..fd334d3f75 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js @@ -15,11 +15,11 @@ describe("pow", function() { }); - it("если n - отрицательное число, результат будет NaN", function() { + it("для отрицательных n возвращает NaN", function() { assert.isNaN(pow(2, -1)); }); - it("если n не число, результат будет NaN", function() { + it("для дробных n возвращает NaN", function() { assert.isNaN(pow(2, 1.5)); }); diff --git a/1-js/03-code-quality/06-polyfills/article.md b/1-js/03-code-quality/06-polyfills/article.md index 9be2b3eb6b..870054631c 100644 --- a/1-js/03-code-quality/06-polyfills/article.md +++ b/1-js/03-code-quality/06-polyfills/article.md @@ -1,13 +1,13 @@ # Полифилы -JavaScript - динамично развивающийся язык программирования. Регулярно появляются предложения о добавлении в JS новых возможностей, они анализируются, и, если предложения одобряются, то описания новых возможностей языка переносятся в черновик , а затем публикуются в [спецификации](http://www.ecma-international.org/publications/standards/Ecma-262.htm). +JavaScript - динамично развивающийся язык программирования. Регулярно появляются предложения о добавлении в JS новых возможностей, они анализируются, и, если предложения одобряются, то описания новых возможностей языка переносятся в черновик , а затем публикуются в [спецификации](https://www.ecma-international.org/publications/standards/Ecma-262.htm). Разработчики JavaScript-движков сами решают, какие предложения реализовывать в первую очередь. Они могут заранее добавить в браузеры поддержку функций, которые всё ещё находятся в черновике, и отложить разработку функций, которые уже перенесены в спецификацию, потому что они менее интересны разработчикам или более сложные в реализации. Таким образом, довольно часто реализуется только часть стандарта. -Можно проверить текущее состояние поддержки различных возможностей JavaScript на странице (нам ещё предстоит изучить многое из этого списка). +Можно проверить текущее состояние поддержки различных возможностей JavaScript на странице (нам ещё предстоит изучить многое из этого списка). ## Babel @@ -19,7 +19,7 @@ JavaScript - динамично развивающийся язык програ На самом деле, есть две части Babel: -1. Во-первых, транспилер, который переписывает код. Разработчик запускает Babel на своём компьютере. Он переписывает код в старый стандарт. И после этого код отправляется на сайт. Современные сборщики проектов, такие как [webpack](http://webpack.github.io/) или [brunch](http://brunch.io/), предоставляют возможность запускать транспилер автоматически после каждого изменения кода, что позволяет экономить время. +1. Во-первых, транспилер, который переписывает код. Разработчик запускает Babel на своём компьютере. Он переписывает код в старый стандарт. И после этого код отправляется на сайт. Современные сборщики проектов, такие как [webpack](https://webpack.github.io/) или [brunch](https://brunch.io/), предоставляют возможность запускать транспилер автоматически после каждого изменения кода, что позволяет экономить время. 2. Во-вторых, полифил. @@ -27,9 +27,8 @@ JavaScript - динамично развивающийся язык програ Термин "полифил" означает, что скрипт "заполняет" пробелы и добавляет современные функции. - Два интересных хранилища полифилов: + Интересное хранилище полифилов: - [core js](https://github.com/zloirock/core-js) поддерживает много функций, можно подключать только нужные. - - [polyfill.io](http://polyfill.io) - сервис, который автоматически создаёт скрипт с полифилом в зависимости от необходимых функций и браузера пользователя. Таким образом, чтобы современные функции поддерживались в старых движках, нам надо установить транспилер и добавить полифил. diff --git a/1-js/04-object-basics/01-object/article.md b/1-js/04-object-basics/01-object/article.md index f66f15983c..3c5474b851 100644 --- a/1-js/04-object-basics/01-object/article.md +++ b/1-js/04-object-basics/01-object/article.md @@ -1,7 +1,7 @@ # Объекты -Как мы знаем из главы , в JavaScript существует семь типов данных. Шесть из них называются "примитивными", так как содержат только одно значение (будь то строка, число или что-то другое). +Как мы знаем из главы , в JavaScript существует 8 типов данных. Семь из них называются "примитивными", так как содержат только одно значение (будь то строка, число или что-то другое). Объекты же используются для хранения коллекций различных значений и более сложных сущностей. В JavaScript объекты используются очень часто, это одна из основ языка. Поэтому мы должны понять их, прежде чем углубляться куда-либо ещё. @@ -33,8 +33,6 @@ let user = { // объект }; ``` -Свойства объекта также иногда называют *полями объекта*. - У каждого свойства есть ключ (также называемый "имя" или "идентификатор"). После имени свойства следует двоеточие `":"`, и затем указывается значение свойства. Если в объекте несколько свойств, то они перечисляются через запятую. В объекте `user` сейчас находятся два свойства: @@ -94,6 +92,30 @@ let user = { ``` Это называется "висячая запятая". Такой подход упрощает добавление, удаление и перемещение свойств, так как все строки объекта становятся одинаковыми. +````smart header="Объект, объявленный как константа, может быть изменён" +Объект, объявленный через `const`, *может* быть изменён. + +Например: + +```js run +const user = { + name: "John" +}; + +*!* +user.name = "Pete"; // (*) +*/!* + +alert(user.name); // Pete +``` + +Может показаться, что строка `(*)` должна вызвать ошибку, но нет, здесь всё в порядке. Дело в том, что объявление `const` защищает от изменений только саму переменную `user`, а не её содержимое. + +Определение `const` выдаст ошибку только если мы присвоим переменной другое значение: `user=...`. + +Есть ещё один способ сделать константами свойства объекта, который мы рассмотрим в главе . +```` + ## Квадратные скобки Для свойств, имена которых состоят из нескольких слов, доступ к значению "через точку" не работает: @@ -103,7 +125,9 @@ let user = { user.likes birds = true ``` -Так происходит, потому что точка требует, чтобы ключ был именован по правилам именования переменных. То есть не имел пробелов, не начинался с цифры и не содержал специальные символы, кроме `$` и `_`. +JavaScript видит, что мы обращаемся к свойству `user.likes`, а затем идёт непонятное слово `birds`. В итоге синтаксическая ошибка. + +Точка требует, чтобы ключ был именован по правилам именования переменных. То есть не имел пробелов, не начинался с цифры и не содержал специальные символы, кроме `$` и `_`. Для таких случаев существует альтернативный способ доступа к свойствам через квадратные скобки. Такой способ сработает с любым именем свойства: @@ -120,13 +144,10 @@ alert(user["likes birds"]); // true delete user["likes birds"]; ``` -Сейчас всё в порядке. - -Обратите внимание, что строка в квадратных скобках закавычена (подойдёт любой тип кавычек). +Сейчас всё в порядке. Обратите внимание, что строка в квадратных скобках заключена в кавычки (подойдёт любой тип кавычек). Квадратные скобки также позволяют обратиться к свойству, имя которого может быть результатом выражения. Например, имя свойства может храниться в переменной: - ```js let key = "likes birds"; @@ -185,6 +206,7 @@ alert( bag.apple ); // 5, если fruit="apple" И если посетитель введёт слово `"apple"`, то в объекте `bag` теперь будет лежать свойство `{apple: 5}`. По сути, пример выше работает так же, как и следующий пример: + ```js run let fruit = prompt("Какой фрукт купить?", "apple"); let bag = {}; @@ -208,42 +230,6 @@ let bag = { Подведём итог: в большинстве случаев, когда имена свойств известны и просты, используется запись через точку. Если же нам нужно что-то более сложное, то мы используем квадратные скобки. -````smart header="Зарезервированные слова разрешено использовать как имена свойств" - -Имя переменной не может совпадать с зарезервированными словами, такими как "for", "let", "return" и т.д. - -Но для свойств объекта такого ограничения нет: - -```js run -let obj = { - for: 1, - let: 2, - return: 3 -}; - -alert( obj.for + obj.let + obj.return ); // 6 -``` - -В принципе, разрешены любые имена свойств, но есть специальное свойство `__proto__`, которое по историческим причинам имеет особое поведение. Например, его значение всегда должно быть объектом, и мы не можем установить для него никакое другое значение: - -```js run -let obj = {}; -obj.__proto__ = 5; -alert(obj.__proto__); // [object Object], работает не так, как мы ожидали -``` - -Как мы видим, присвоение примитивного значения `5` игнорируется. - -Это может стать источником ошибок и даже уязвимостей, если мы намереваемся хранить в объекте произвольные пары "ключ:значение" и позволяем посетителям указывать ключи. - -Посетитель может указать `__proto__` в качестве ключа, и логика присваивания будет нарушена (как показано выше). - -Есть способ заставить объекты обрабатывать `__proto__` как обычное свойство. Мы поговорим о нём позже, а пока нам нужно узнать больше об объектах. - -Также существует другая структура данных [Map](info:map-set), которая поддерживает произвольные ключи. Мы изучим её в главе . -```` - - ## Свойство из переменной В реальном коде часто нам необходимо использовать существующие переменные как значения для свойств с тем же именем. @@ -288,9 +274,57 @@ let user = { }; ``` -## Проверка существования свойства -Особенность объектов в том, что можно получить доступ к любому свойству. Даже если свойства не существует - ошибки не будет! При обращении к свойству, которого нет, возвращается `undefined`. Это позволяет просто проверить существование свойства - сравнением его с `undefined`: +## Ограничения на имена свойств + +Как мы уже знаем, имя переменной не может совпадать с зарезервированными словами, такими как "for", "let", "return" и т.д. + +Но для свойств объекта такого ограничения нет: + +```js run +// эти имена свойств допустимы +let obj = { + for: 1, + let: 2, + return: 3 +}; + +alert( obj.for + obj.let + obj.return ); // 6 +``` + +Иными словами, нет никаких ограничений к именам свойств. Они могут быть в виде строк или символов (специальный тип для идентификаторов, который будет рассмотрен позже). + +Все другие типы данных будут автоматически преобразованы к строке. + +Например, если использовать число `0` в качестве ключа, то оно превратится в строку `"0"`: + +```js run +let obj = { + 0: "Тест" // то же самое что и "0": "Тест" +}; + +// обе функции alert выведут одно и то же свойство (число 0 преобразуется в строку "0") +alert( obj["0"] ); // Тест +alert( obj[0] ); // Тест (то же свойство) +``` + +Есть небольшой подводный камень, связанный со специальным свойством `__proto__`. Мы не можем установить его в необъектное значение: + +```js run +let obj = {}; +obj.__proto__ = 5; // присвоим число +alert(obj.__proto__); // [object Object], значение - это объект, т.е. не то, что мы ожидали +``` + +Как мы видим, присвоение примитивного значения `5` игнорируется. + +Мы более подробно исследуем особенности свойства `__proto__` в следующих главах [](info:prototype-inheritance), а также предложим [способы исправления](info:prototype-methods) такого поведения. + +## Проверка существования свойства, оператор "in" + +В отличие от многих других языков, особенность JavaScript-объектов в том, что можно получить доступ к любому свойству. Даже если свойства не существует - ошибки не будет! + +При обращении к свойству, которого нет, возвращается `undefined`. Это позволяет просто проверить существование свойства: ```js run let user = {}; @@ -325,8 +359,9 @@ let key = "age"; alert( *!*key*/!* in user ); // true, имя свойства было взято из переменной key ``` -````smart header="Оператор \"in\" для свойств со значением *'undefined'*" -Обычно строгого сравнения `"=== undefined"` достаточно для проверки наличия свойства. Но есть особый случай, когда оно не подходит, и нужно использовать `"in"`. +Для чего вообще нужен оператор `in`? Разве недостаточно сравнения с `undefined`? + +В большинстве случаев прекрасно сработает сравнение с `undefined`. Но есть особый случай, когда оно не подходит и нужно использовать `"in"`. Это когда свойство существует, но содержит значение `undefined`: @@ -335,20 +370,15 @@ let obj = { test: undefined }; -alert( obj.test ); // выведет undefined, значит свойство не существует? - +alert( obj.test ); // выведет undefined, значит свойство не существует? alert( "test" in obj ); // true, свойство существует! ``` - В примере выше свойство `obj.test` технически существует в объекте. Оператор `in` сработал правильно. -Подобные ситуации случаются очень редко, так как `undefined` обычно явно не присваивается. Для "неизвестных" или "пустых" свойств мы используем значение `null`. Таким образом, оператор `in` является экзотическим гостем в коде. - -```` - +Подобные ситуации случаются очень редко, так как `undefined` обычно явно не присваивается. Для "неизвестных" или "пустых" свойств мы используем значение `null`. -## Цикл "for..in" +## Цикл "for..in" [#forin] Для перебора всех свойств объекта используется цикл `for..in`. Этот цикл отличается от изученного ранее цикла `for(;;)`. @@ -377,12 +407,10 @@ for (let key in user) { } ``` - Обратите внимание, что все конструкции "for" позволяют нам объявлять переменную внутри цикла, как, например, `let key` здесь. Кроме того, мы могли бы использовать другое имя переменной. Например, часто используется вариант `"for (let prop in obj)"`. - ### Упорядочение свойств объекта Упорядочены ли свойства объекта? Другими словами, если мы будем в цикле перебирать все свойства объекта, получим ли мы их в том же порядке, в котором мы их добавляли? Можем ли мы на это рассчитывать? @@ -466,268 +494,6 @@ for (let code in codes) { Теперь код работает так, как мы задумывали. -## Копирование по ссылке - -Одним из фундаментальных отличий объектов от примитивных типов данных является то, что они хранятся и копируются "по ссылке". - -Примитивные типы: строки, числа, логические значения - присваиваются и копируются "по значению". - -Например: - -```js -let message = "Hello!"; -let phrase = message; -``` - -В результате мы имеем две независимые переменные, каждая из которых хранит строку `"Hello!"`. - -![](variable-copy-value.svg) - - -Объекты ведут себя иначе. - -**Переменная хранит не сам объект, а его "адрес в памяти", другими словами "ссылку" на него.** - -Проиллюстрируем это: - - -```js -let user = { - name: "John" -}; -``` - -![](variable-contains-reference.svg) - -Сам объект хранится где-то в памяти. А в переменной `user` лежит "ссылка" на эту область памяти. - -**Когда переменная объекта копируется - копируется ссылка, сам же объект не дублируется.** - -Если мы представляем объект как ящик, то переменная – это ключ к нему. Копирование переменной дублирует ключ, но не сам ящик. - -Например: - - -```js no-beautify -let user = { name: "John" }; - -let admin = user; // копируется ссылка -``` - -Теперь у нас есть две переменные, каждая из которых содержит ссылку на один и тот же объект: - -![](variable-copy-reference.svg) - -Мы можем использовать любую из переменных для доступа к ящику и изменения его содержимого: - -```js run -let user = { name: 'John' }; - -let admin = user; - -*!* -admin.name = 'Pete'; // изменено по ссылке из переменной "admin" -*/!* - -alert(*!*user.name*/!*); // 'Pete', изменения видны по ссылке из переменной "user" -``` - -Приведённый выше пример демонстрирует, что объект только один. Как если бы у нас был один ящик с двумя ключами и мы использовали один из них (`admin`), чтобы войти в него и что-то изменить, а затем, открыв ящик другим ключом (`user`), мы бы увидели эти изменения. - -### Сравнение объектов - -Операторы равенства `==` и строгого равенства `===` для объектов работают одинаково. - -**Два объекта равны только в том случае, если это один и тот же объект.** - -Например, две переменные ссылаются на один и тот же объект, они равны: - -```js run -let a = {}; -let b = a; // копирование по ссылке - -alert( a == b ); // true, обе переменные ссылаются на один и тот же объект -alert( a === b ); // true -``` - -В примере ниже два разных объекта не равны, хотя и оба пусты: - -```js run -let a = {}; -let b = {}; // два независимых объекта - -alert( a == b ); // false -``` - -Для сравнений типа `obj1 > obj2` или для сравнения с примитивом `obj == 5` объекты преобразуются в примитивы. - -Мы скоро изучим, как работают такие преобразования объектов, но, по правде говоря, сравнения такого рода необходимы очень редко и обычно являются результатом ошибки программиста. - -### Объекты-константы - -Объект, объявленный через `const`, *может* быть изменён. - -Пример: - -```js run -const user = { - name: "John" -}; - -*!* -user.age = 25; // (*) -*/!* - -alert(user.age); // 25 -``` - -Может показаться, что строка `(*)` должна вызвать ошибку, но нет, здесь всё в порядке. Дело в том, что объявление `const` защищает от изменений только само значение `user`. А в нашем случае значение `user` - это ссылка на объект, и это значение мы не меняем. В строке `(*)` мы действуем *внутри* объекта, мы не переназначаем `user`. - -Если же мы попытаемся присвоить `user` другое значение, то `const` выдаст ошибку: - -```js run -const user = { - name: "John" -}; - -*!* -// Ошибка (нельзя переопределять константу user) -*/!* -user = { - name: "Pete" -}; -``` - -...Но что делать, если мы хотим сделать константами свойства объекта? Как сделать так, чтобы попытка изменить `user.age = 25` выдавала ошибку? Это тоже возможно. Мы рассмотрим эту тему в главе . - -## Клонирование и объединение объектов, Object.assign - -Как мы узнали ранее, при копировании переменной объекта создаётся ещё одна ссылка на тот же самый объект. - -Но что, если нам всё же нужно дублировать объект? Создать независимую копию, клон? - -Это выполнимо, но немного сложно, так как в JavaScript нет встроенного метода для этого. На самом деле, такая нужда возникает редко. В большинстве случаев нам достаточно копирования по ссылке. - -Но если мы действительно этого хотим, то нам нужно создавать новый объект и повторять структуру дублируемого объекта, перебирая его свойства и копируя их. - -Например так: - -```js run -let user = { - name: "John", - age: 30 -}; - -*!* -let clone = {}; // новый пустой объект - -// скопируем все свойства user в него -for (let key in user) { - clone[key] = user[key]; -} -*/!* - -// теперь в переменной clone находится абсолютно независимый клон объекта. -clone.name = "Pete"; // изменим в нём данные - -alert( user.name ); // в оригинальном объекте значение свойства `name` осталось прежним – John. -``` - -Кроме того, для этих целей мы можем использовать метод [Object.assign](mdn:js/Object/assign). - -Синтаксис: - -```js -Object.assign(dest, [src1, src2, src3...]) -``` - -- Аргументы `dest`, и `src1, ..., srcN` (может быть столько, сколько нужно) являются объектами. -- Метод копирует свойства всех объектов `src1, ..., srcN` в объект `dest`. То есть, свойства всех перечисленных объектов, начиная со второго, копируются в первый объект. После копирования метод возвращает объект `dest`. - -Например, объединим несколько объектов в один: - -```js -let user = { name: "John" }; - -let permissions1 = { canView: true }; -let permissions2 = { canEdit: true }; - -*!* -// копируем все свойства из permissions1 и permissions2 в user -Object.assign(user, permissions1, permissions2); -*/!* - -// now user = { name: "John", canView: true, canEdit: true } -``` - -Если принимающий объект (`user`) уже имеет свойство с таким именем, оно будет перезаписано: - -```js -let user = { name: "John" }; - -// свойство name перезапишется, свойство isAdmin добавится -Object.assign(user, { name: "Pete", isAdmin: true }); - -// now user = { name: "Pete", isAdmin: true } -``` - -Мы также можем использовать `Object.assign` для простого клонирования: - -```js -let user = { - name: "John", - age: 30 -}; - -*!* -let clone = Object.assign({}, user); -*/!* -``` - -Все свойства объекта `user` будут скопированы в пустой объект, и ссылка на этот объект будет в переменной `clone`. На самом деле, такое клонирование работает так же, как и через цикл, но короче. - -До сих пор мы предполагали, что все свойства пользователя примитивны. Но свойства могут быть ссылками на другие объекты. Что с ними делать? - -Например, есть объект: - -```js run -let user = { - name: "John", - sizes: { - height: 182, - width: 50 - } -}; - -alert( user.sizes.height ); // 182 -``` - -Теперь при клонировании недостаточно просто скопировать `clone.sizes = user.sizes`, поскольку `user.sizes` - это объект, он будет скопирован по ссылке. А значит объекты `clone` и `user` в своих свойствах `sizes` будут ссылаться на один и тот же объект: - -```js run -let user = { - name: "John", - sizes: { - height: 182, - width: 50 - } -}; - -let clone = Object.assign({}, user); - -alert( user.sizes === clone.sizes ); // true, один и тот же объект - -// user и clone обращаются к одному sizes -user.sizes.width++; // меняем свойство в одном объекте -alert(clone.sizes.width); // 51, видим результат в другом объекте -``` - -Чтобы исправить это, мы должны в цикле клонирования делать проверку, не является ли значение `user[key]` объектом, и если это так - копируем и его структуру тоже. Это называется "глубокое клонирование". - -Существует стандартный алгоритм глубокого клонирования, [Structured cloning algorithm](http://w3c.github.io/html/infrastructure.html#safe-passing-of-structured-data). Он решает описанную выше задачу, а также более сложные задачи. -Чтобы не изобретать велосипед, мы можем использовать реализацию этого алгоритма из JavaScript-библиотеки [lodash](https://lodash.com), метод [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). - - ## Итого Объекты - это ассоциативные массивы с рядом дополнительных возможностей. @@ -746,10 +512,6 @@ alert(clone.sizes.width); // 51, видим результат в другом - Проверка существования свойства: `"key" in obj`. - Перебор свойств объекта: цикл for `for (let key in obj)`. -Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не "значение объекта", а "ссылку" (адрес в памяти) на это значение. Поэтому копирование такой переменной или передача её в качестве аргумента функции приводит к копированию этой ссылки, а не самого объекта. Все операции с использованием скопированных ссылок (например, добавление или удаление свойств) выполняются с одним и тем же объектом. - -Чтобы сделать "настоящую копию" (клон), мы можем использовать `Object.assign` или [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). - То, что мы изучали в этой главе, называется "простым объектом" ("plain object") или просто `Object`. В JavaScript есть много других типов объектов: diff --git a/1-js/04-object-basics/01-object/object-user-delete.svg b/1-js/04-object-basics/01-object/object-user-delete.svg index 87753bb14d..c5af7e7afc 100644 --- a/1-js/04-object-basics/01-object/object-user-delete.svg +++ b/1-js/04-object-basics/01-object/object-user-delete.svg @@ -1 +1 @@ -nameisAdminuser \ No newline at end of file +nameisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-empty.svg b/1-js/04-object-basics/01-object/object-user-empty.svg index cc2642f4cd..edde844cf5 100644 --- a/1-js/04-object-basics/01-object/object-user-empty.svg +++ b/1-js/04-object-basics/01-object/object-user-empty.svg @@ -1 +1 @@ -пустоuser \ No newline at end of file +пустоuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.svg b/1-js/04-object-basics/01-object/object-user-isadmin.svg index c7a24cc92c..e2cc0eaf15 100644 --- a/1-js/04-object-basics/01-object/object-user-isadmin.svg +++ b/1-js/04-object-basics/01-object/object-user-isadmin.svg @@ -1 +1 @@ -nameageisAdminuser \ No newline at end of file +nameageisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-props.svg b/1-js/04-object-basics/01-object/object-user-props.svg index cb9afc1acb..b3d5c9b71b 100644 --- a/1-js/04-object-basics/01-object/object-user-props.svg +++ b/1-js/04-object-basics/01-object/object-user-props.svg @@ -1 +1 @@ -nameagelikes birdsuser \ No newline at end of file +nameagelikes birdsuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user.svg b/1-js/04-object-basics/01-object/object-user.svg index 69f7efd933..f499fbc10f 100644 --- a/1-js/04-object-basics/01-object/object-user.svg +++ b/1-js/04-object-basics/01-object/object-user.svg @@ -1 +1 @@ -nameageuser \ No newline at end of file +nameageuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object.svg b/1-js/04-object-basics/01-object/object.svg index 5a4a49fb68..47431a6e1f 100644 --- a/1-js/04-object-basics/01-object/object.svg +++ b/1-js/04-object-basics/01-object/object.svg @@ -1 +1 @@ -key1key2key3 \ No newline at end of file +key1key2key3 \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-copy-value.svg b/1-js/04-object-basics/01-object/variable-copy-value.svg deleted file mode 100644 index e09f521fea..0000000000 --- a/1-js/04-object-basics/01-object/variable-copy-value.svg +++ /dev/null @@ -1 +0,0 @@ -"Hello!"message"Hello!"phrase \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/article.md b/1-js/04-object-basics/02-garbage-collection/article.md deleted file mode 100644 index 5dace0d24c..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/article.md +++ /dev/null @@ -1,212 +0,0 @@ -# Сборка мусора - -Управление памятью в JavaScript выполняется автоматически и незаметно. Мы создаём примитивы, объекты, функции... Всё это занимает память. - -Но что происходит, когда что-то больше не нужно? Как JavaScript понимает, что пора очищать память? - -## Достижимость - -Основной концепцией управления памятью в JavaScript является принцип *достижимости*. - -Если упростить, то "достижимые" значения - это те, которые доступны или используются. Они гарантированно находятся в памяти. - -1. Существует базовое множество достижимых значений, которые не могут быть удалены. - - Например: - - - Локальные переменные и параметры текущей функции. - - Переменные и параметры других функций в текущей цепочке вложенных вызовов. - - Глобальные переменные. - - (некоторые другие внутренние значения) - - Эти значения мы будем называть *корнями*. - -2. Любое другое значение считается достижимым, если оно доступно из корня по ссылке или по цепочке ссылок. - - Например, если в локальной переменной есть объект, и он имеет свойство, в котором хранится ссылка на другой объект, то этот объект считается достижимым. И те, на которые он ссылается, тоже достижимы. Далее вы познакомитесь с подробными примерами на эту тему. - -В интерпретаторе JavaScript есть фоновый процесс, который называется [сборщик мусора](https://ru.wikipedia.org/wiki/Сборка_мусора). Он следит за всеми объектами и удаляет те, которые стали недостижимы. - -## Простой пример - -Вот самый простой пример: - -```js -// в user находится ссылка на объект -let user = { - name: "John" -}; -``` - -![](memory-user-john.svg) - -Здесь стрелка обозначает ссылку на объект. Глобальная переменная `user` ссылается на объект `{name: "John"}` (мы будем называть его просто "John"). В свойстве `"name"` объекта John хранится примитив, поэтому оно нарисовано внутри объекта. - -Если перезаписать значение `user`, то ссылка потеряется: - -```js -user = null; -``` - -![](memory-user-john-lost.svg) - -Теперь объект John становится недостижимым. К нему нет доступа, на него нет ссылок. Сборщик мусора удалит эти данные и освободит память. - -## Две ссылки - -Представим, что мы скопировали ссылку из `user` в `admin`: - -```js -// в user находится ссылка на объект -let user = { - name: "John" -}; - -*!* -let admin = user; -*/!* -``` - -![](memory-user-john-admin.svg) - -Теперь, если мы сделаем то же самое: -```js -user = null; -``` - -...то объект John всё ещё достижим через глобальную переменную `admin`, поэтому он находится в памяти. Если бы мы также перезаписали `admin`, то John был бы удалён. - -## Взаимосвязанные объекты - -Теперь более сложный пример. Семья: - -```js -function marry(man, woman) { - woman.husband = man; - man.wife = woman; - - return { - father: man, - mother: woman - } -} - -let family = marry({ - name: "John" -}, { - name: "Ann" -}); -``` - -Функция `marry` "женит" два объекта, давая им ссылки друг на друга, и возвращает новый объект, содержащий ссылки на два предыдущих. - -В результате получаем такую структуру памяти: - -![](family.svg) - -На данный момент все объекты достижимы. - -Теперь удалим две ссылки: - -```js -delete family.father; -delete family.mother.husband; -``` - -![](family-delete-refs.svg) - -Недостаточно удалить только одну из этих ссылок, потому что все объекты останутся достижимыми. - -Но если мы удалим обе, то увидим, что у объекта John больше нет входящих ссылок: - -![](family-no-father.svg) - -Исходящие ссылки не имеют значения. Только входящие ссылки могут сделать объект достижимым. Объект John теперь недостижим и будет удалён из памяти со всеми своими данными, которые также стали недоступны. - -После сборки мусора: - -![](family-no-father-2.svg) - -## Недостижимый "остров" - -Вполне возможна ситуация, при которой целый "остров" связанных объектов может стать недостижимым и удалится из памяти. - -Возьмём объект `family` из примера выше. А затем: - -```js -family = null; -``` - -Структура в памяти теперь станет такой: - -![](family-no-family.svg) - -Этот пример демонстрирует, насколько важна концепция достижимости. - -Объекты John и Ann всё ещё связаны, оба имеют входящие ссылки, но этого недостаточно. - -У объекта `family` больше нет ссылки от корня, поэтому весь "остров" становится недостижимым и будет удалён. - -## Внутренние алгоритмы - -Основной алгоритм сборки мусора - "алгоритм пометок" (англ. "mark-and-sweep"). - -Согласно этому алгоритму, сборщик мусора регулярно выполняет следующие шаги: - -- Сборщик мусора "помечает" (запоминает) все корневые объекты. -- Затем он идёт по их ссылкам и помечает все найденные объекты. -- Затем он идёт по ссылкам помеченных объектов и помечает объекты, на которые есть ссылка от них. Все объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды. -- ...И так далее, пока не будут посещены все ссылки (достижимые от корней). -- Все непомеченные объекты удаляются. - -Например, пусть наша структура объектов выглядит так: - -![](garbage-collection-1.svg) - -Явно виден "недостижимый остров" справа. Теперь посмотрим, как будет работать "алгоритм пометок" сборщика мусора. - -На первом шаге помечаются корни: - -![](garbage-collection-2.svg) - -Затем помечаются объекты по их ссылкам: - -![](garbage-collection-3.svg) - -...а затем объекты по их ссылкам и так далее, пока это вообще возможно: - -![](garbage-collection-4.svg) - -Теперь объекты, до которых не удалось дойти от корней, считаются недостижимыми и будут удалены: - -![](garbage-collection-5.svg) - -Это и есть принцип работы сборки мусора. - -Интерпретаторы JavaScript применяют множество оптимизаций, чтобы сборка мусора работала быстрее и не влияла на производительность. - -Вот некоторые из оптимизаций: - -- **Сборка по поколениям (Generational collection)** - объекты делятся на "новые" и "старые". Многие объекты появляются, выполняют свою задачу и быстро умирают, их можно удалять более агрессивно. Те, которые живут достаточно долго, становятся "старыми" и проверяются реже. -- **Инкрементальная сборка (Incremental collection)** - если объектов много, то обход всех ссылок и пометка достижимых объектов может занять значительное время и привести к видимым задержкам выполнения скрипта. Поэтому интерпретатор пытается организовать сборку мусора поэтапно. Этапы выполняются по отдельности один за другим. Это требует дополнительного учёта для отслеживания изменений между этапами, но зато теперь у нас есть много крошечных задержек вместо одной большой. -- **Сборка в свободное время (Idle-time collection)** - чтобы уменьшить возможное влияние на производительность, сборщик мусора старается работать только во время простоя процессора. - -Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось описать их здесь, я должен воздержаться от этого, потому что разные интерпретаторы JavaScript применяют разные приёмы и хитрости. И, что более важно, всё меняется по мере развития интерпретаторов, поэтому углубляться в эту тему заранее, без реальной необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будут полезны некоторые ссылки ниже. - -## Итого - -Главное из того, что мы узнали: - -- Сборка мусора выполняется автоматически. Мы не можем ускорить или предотвратить её. -- Объекты сохраняются в памяти, пока они достижимы. -- Наличие ссылки не гарантирует, что объект достижим (от корня): несколько взаимосвязанных объектов могут стать недостижимыми как единое целое. - -Современные интерпретаторы реализуют передовые алгоритмы сборки мусора. - -Некоторые из них освещены в книге "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones и др.). - -Если вы знакомы с низкоуровневым программированием, то более подробная информация о сборщике мусора интерпретатора V8 находится в статье [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). - -Также в [блоге интерпретатора V8](https://v8.dev/) время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы изучить сборку мусора, вам необходимо понимать, как устроен внутри интерпретатор V8 в целом. Об этом вы можете почитать в блоге [Вячеслава Егорова](http://mrale.ph), одного из инженеров, разрабатывавших V8. Я говорю про "V8", потому что он лучше всего освещён статьями в интернете. В других интерпретаторах многие подходы схожи, но сборка мусора во многих аспектах отличается. - -Глубокое понимание работы интерпретаторов необходимо, когда вам нужны низкоуровневые оптимизации. Было бы разумно запланировать их изучение как следующий шаг после освоения языка. diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg deleted file mode 100644 index e30bf7e50d..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg +++ /dev/null @@ -1 +0,0 @@ -<global variable>ObjectObjectwifefamilyname: "John"name: "Ann"motherObjectfatherhusband \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg b/1-js/04-object-basics/02-garbage-collection/family-no-family.svg deleted file mode 100644 index cb9458555c..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg +++ /dev/null @@ -1 +0,0 @@ -<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg b/1-js/04-object-basics/02-garbage-collection/family-no-father.svg deleted file mode 100644 index 8e7f3025f7..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg +++ /dev/null @@ -1 +0,0 @@ -ObjectObjectwifefamilyname: "John"name: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family.svg b/1-js/04-object-basics/02-garbage-collection/family.svg deleted file mode 100644 index 376132609b..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/family.svg +++ /dev/null @@ -1 +0,0 @@ -ObjectObjectfatherwifefamilyname: "John"name: "Ann"motherObjecthusband<global variable> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg deleted file mode 100644 index 50697c4bd7..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg deleted file mode 100644 index dc0868323a..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg deleted file mode 100644 index d5faadb315..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg deleted file mode 100644 index 373647767c..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg deleted file mode 100644 index 8b8c3d368a..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg +++ /dev/null @@ -1 +0,0 @@ -<global>недостижимые \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/article.md b/1-js/04-object-basics/02-object-copy/article.md new file mode 100644 index 0000000000..e648d34098 --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/article.md @@ -0,0 +1,265 @@ + +# Копирование объектов и ссылки + +Одно из фундаментальных отличий объектов от примитивов заключается в том, что объекты хранятся и копируются "по ссылке", тогда как примитивные значения: строки, числа, логические значения и т.д. – всегда копируются "как целое значение". + +Это легко понять, если мы немного заглянем под капот того, что происходит, когда мы копируем значение. + +Давайте начнём с примитива, такого как строка. + +Здесь мы помещаем копию `message` во `phrase`: + +```js +let message = "Привет!"; +let phrase = message; +``` + +В результате мы имеем две независимые переменные, каждая из которых хранит строку `"Привет!"`. + +![](variable-copy-value.svg) + +Вполне очевидный результат, не так ли? + +Объекты ведут себя иначе. + +**Переменная, которой присвоен объект, хранит не сам объект, а его "адрес в памяти" – другими словами, "ссылку" на него.** + +Давайте рассмотрим пример такой переменной: + +```js +let user = { + name: "John" +}; +``` + +И вот как это на самом деле хранится в памяти: + +![](variable-contains-reference.svg) + +Объект хранится где-то в памяти (справа от изображения), в то время как переменная `user` (слева) имеет лишь "ссылку" на него. + +Мы можем думать о переменной объекта, такой как `user`, как о листе бумаги с адресом объекта на нем. + +Когда мы выполняем действия с объектом, к примеру, берём свойство `user.name `, движок JavaScript просматривает то, что находится по этому адресу, и выполняет операцию с самим объектом. + +Теперь вот почему это важно. + +**При копировании переменной объекта копируется ссылка, но сам объект не дублируется.** + +Например: + +```js no-beautify +let user = { name: "John" }; + +let admin = user; // копируется ссылка +``` + +Теперь у нас есть две переменные, каждая из которых содержит ссылку на один и тот же объект: + +![](variable-copy-reference.svg) + +Как вы можете видеть, все ещё есть один объект, но теперь с двумя переменными, которые ссылаются на него. + +Мы можем использовать любую переменную для доступа к объекту и изменения его содержимого: + +```js run +let user = { name: 'John' }; + +let admin = user; + +*!* +admin.name = 'Pete'; // изменено по ссылке из переменной "admin" +*/!* + +alert(*!*user.name*/!*); // 'Pete', изменения видны по ссылке из переменной "user" +``` + +Это как если бы у нас был шкафчик с двумя ключами, и мы использовали один из них (`admin`), чтобы войти в него и внести изменения. А затем, если мы позже используем другой ключ (`user`), мы все равно открываем тот же шкафчик и можем получить доступ к изменённому содержимому. + +## Сравнение по ссылке + +Два объекта равны только в том случае, если это один и тот же объект. + +Например, здесь `a` и `b` ссылаются на один и тот же объект, поэтому они равны: + +```js run +let a = {}; +let b = a; // копирование по ссылке + +alert( a == b ); // true, обе переменные ссылаются на один и тот же объект +alert( a === b ); // true +``` + +И здесь два независимых объекта не равны, даже если они выглядят одинаково (оба пусты): + +```js run +let a = {}; +let b = {}; // два независимых объекта + +alert( a == b ); // false +``` + +Для сравнений типа `obj1 > obj2` или для сравнения с примитивом `obj == 5` объекты преобразуются в примитивы. Очень скоро мы изучим, как работают преобразования объектов, но, по правде говоря, такие сравнения требуются очень редко и обычно они появляются в результате ошибок программиста. + +## Клонирование и объединение, Object.assign [#cloning-and-merging-object-assign] + +Итак, копирование объектной переменной создаёт ещё одну ссылку на тот же объект. + +Но что, если нам всё же нужно дублировать объект? Создать независимую копию, клон? + +Это тоже выполнимо, но немного сложнее, потому что в JavaScript для этого нет встроенного метода. Но на самом деле в этом редко возникает необходимость, копирования по ссылке в большинстве случаев вполне хватает. + +Но если мы действительно этого хотим, то нам нужно создать новый объект и воспроизвести структуру существующего, перебрав его свойства и скопировав их на примитивном уровне. + +Например так: + +```js run +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = {}; // новый пустой объект + +// давайте скопируем все свойства user в него +for (let key in user) { + clone[key] = user[key]; +} +*/!* + +// теперь clone это полностью независимый объект с тем же содержимым +clone.name = "Pete"; // изменим в нём данные + +alert( user.name ); // все ещё John в первоначальном объекте +``` + +Также мы можем использовать для этого метод [Object.assign](mdn:js/Object/assign). + +Синтаксис: + +```js +Object.assign(dest, [src1, src2, src3...]) +``` + +- Первый аргумент `dest` — целевой объект. +- Остальные аргументы `src1, ..., srcN` (может быть столько, сколько необходимо) являются исходными объектами +- Метод копирует свойства всех исходных объектов `src1, ..., srcN` в целевой объект `dest`. Другими словами, свойства всех аргументов, начиная со второго, копируются в первый объект. +- Возвращает объект `dest`. + +Например, мы можем использовать его для объединения нескольких объектов в один: +```js +let user = { name: "John" }; + +let permissions1 = { canView: true }; +let permissions2 = { canEdit: true }; + +*!* +// копируем все свойства из permissions1 и permissions2 в user +Object.assign(user, permissions1, permissions2); +*/!* + +// теперь user = { name: "John", canView: true, canEdit: true } +``` + +Если скопированное имя свойства уже существует, оно будет перезаписано: + +```js run +let user = { name: "John" }; + +Object.assign(user, { name: "Pete" }); + +alert(user.name); // теперь user = { name: "Pete" } +``` + +Мы также можем использовать `Object.assign` для замены цикла `for..in ` для простого клонирования: + +```js +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = Object.assign({}, user); +*/!* +``` + +Он копирует все свойства `user` в пустой объект и возвращает его. + +Также существуют и другие методы клонирования объекта. Например, с использованием [оператора расширения](info:rest-parameters-spread-operator) `clone = {...user}`, рассмотренного далее в учебнике. + +## Вложенное клонирование + +До сих пор мы предполагали, что все свойства `user` примитивныe. Но свойства могут быть и ссылками на другие объекты. Что с ними делать? + +Например, есть объект: +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +alert( user.sizes.height ); // 182 +``` + +Теперь недостаточно просто скопировать `clone.sizes = user.sizes`, потому что `user.sizes` - это объект, он будет скопирован по ссылке. Таким образом, `clone` и `user` будут иметь общий объект `sizes`: + +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +let clone = Object.assign({}, user); + +alert( user.sizes === clone.sizes ); // true, тот же объект + +// user и clone обладают общим свойством sizes +user.sizes.width++; // изменяем свойства в первом объекте +alert(clone.sizes.width); // 51, видим результат в другом +``` + +Чтобы исправить это, мы должны использовать цикл клонирования, который проверяет каждое значение `user[key]` и, если это объект, тогда также копирует его структуру. Это называется "глубоким клонированием". + +Мы можем реализовать глубокое клонирование, используя рекурсию. Или, чтобы не изобретать велосипед заново, возьмите готовую реализацию, например [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) из библиотеки JavaScript [lodash](https://lodash.com). + +Также мы можем использовать глобальный метод [structuredClone()](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone), который позволяет сделать полную копию объекта. К сожалению он поддерживается только современными браузерами. [Здесь](https://caniuse.com/?search=structuredClone) можно ознакомиться с поддержкой этого метода. + +````smart header="Объекты, объявленные как константа, могут быть изменены" +Важным побочным эффектом хранения объектов в качестве ссылок является то, что объект, объявленный как `const`, *может* быть изменён. + +Например: + +```js run +const user = { + name: "John" +}; + +*!* +user.name = "Pete"; // (*) +*/!* + +alert(user.name); // Pete +``` + +Может показаться, что строка `(*)` вызовет ошибку, но, это не так. Значение `user` это константа, оно всегда должно ссылаться на один и тот же объект, но свойства этого объекта могут свободно изменяться. + +Другими словами, `const user` выдаст ошибку только в том случае, если мы попытаемся задать `user=...` в целом. + +Тем не менее, если нам действительно нужно создать постоянные свойства объекта, это тоже возможно, но с использованием совершенно других методов. Мы затронем это в главе . +```` + +## Итого + +Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не "значение объекта", а "ссылку" (адрес в памяти) на это значение. Таким образом, копирование такой переменной или передача её в качестве аргумента функции копирует эту ссылку, а не сам объект. + +Все операции с использованием скопированных ссылок (например, добавление/удаление свойств) выполняются с одним и тем же объектом. + +Чтобы создать "реальную копию" (клон), мы можем использовать `Object.assign` для так называемой "поверхностной копии" (вложенные объекты копируются по ссылке) или функцию "глубокого клонирования", такую как [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). diff --git a/1-js/04-object-basics/01-object/variable-contains-reference.svg b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg similarity index 63% rename from 1-js/04-object-basics/01-object/variable-contains-reference.svg rename to 1-js/04-object-basics/02-object-copy/variable-contains-reference.svg index dedb7eaaa9..267f045785 100644 --- a/1-js/04-object-basics/01-object/variable-contains-reference.svg +++ b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg @@ -1 +1 @@ -username \ No newline at end of file +username \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.svg b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg similarity index 73% rename from 1-js/04-object-basics/01-object/variable-copy-reference.svg rename to 1-js/04-object-basics/02-object-copy/variable-copy-reference.svg index f212c0853a..a847fb200e 100644 --- a/1-js/04-object-basics/01-object/variable-copy-reference.svg +++ b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg @@ -1 +1 @@ -useradminname \ No newline at end of file +useradminname \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/variable-copy-value.svg b/1-js/04-object-basics/02-object-copy/variable-copy-value.svg new file mode 100644 index 0000000000..f12e7fc4c8 --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/variable-copy-value.svg @@ -0,0 +1 @@ +"Привет!"message"Привет!"phrase \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/article.md b/1-js/04-object-basics/03-garbage-collection/article.md new file mode 100644 index 0000000000..4c627d2687 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/article.md @@ -0,0 +1,212 @@ +# Сборка мусора + +Управление памятью в JavaScript выполняется автоматически и незаметно. Мы создаём примитивы, объекты, функции... Всё это занимает память. + +Но что происходит, когда что-то больше не нужно? Как движок JavaScript обнаруживает, что пора очищать память? + +## Достижимость + +Основной концепцией управления памятью в JavaScript является принцип *достижимости*. + +Если упростить, то "достижимые" значения - это те, которые доступны или используются. Они гарантированно находятся в памяти. + +1. Существует базовое множество достижимых значений, которые не могут быть удалены. + + Например: + + - Выполняемая в данный момент функция, её локальные переменные и параметры. + - Другие функции в текущей цепочке вложенных вызовов, их локальные переменные и параметры. + - Глобальные переменные. + - (некоторые другие внутренние значения) + + Эти значения мы будем называть *корнями*. + +2. Любое другое значение считается достижимым, если оно доступно из корня по ссылке или по цепочке ссылок. + + Например, если в глобальной переменной есть объект, и он имеет свойство, в котором хранится ссылка на другой объект, то *этот* объект считается достижимым. И те, на которые он ссылается, тоже достижимы. Далее вы познакомитесь с подробными примерами на эту тему. + +В движке JavaScript есть фоновый процесс, который называется [сборщиком мусора](https://ru.wikipedia.org/wiki/Сборка_мусора). Он отслеживает все объекты и удаляет те, которые стали недоступными. + +## Простой пример + +Вот самый простой пример: + +```js +// в user находится ссылка на объект +let user = { + name: "John" +}; +``` + +![](memory-user-john.svg) + +Здесь стрелка обозначает ссылку на объект. Глобальная переменная `user` ссылается на объект `{name: "John"}` (мы будем называть его просто "John" для краткости). В свойстве `"name"` объекта John хранится примитив, поэтому оно нарисовано внутри объекта. + +Если перезаписать значение `user`, то ссылка потеряется: + +```js +user = null; +``` + +![](memory-user-john-lost.svg) + +Теперь объект John становится недостижимым. К нему нет доступа, на него нет ссылок. Сборщик мусора удалит эти данные и освободит память. + +## Две ссылки + +Представим, что мы скопировали ссылку из `user` в `admin`: + +```js +// в user находится ссылка на объект +let user = { + name: "John" +}; + +*!* +let admin = user; +*/!* +``` + +![](memory-user-john-admin.svg) + +Теперь, если мы сделаем то же самое: +```js +user = null; +``` + +...то объект John всё ещё достижим через глобальную переменную `admin`, поэтому он находится в памяти. Если бы мы также перезаписали `admin`, то John был бы удалён. + +## Взаимосвязанные объекты + +Теперь более сложный пример. Семья: + +```js +function marry(man, woman) { + woman.husband = man; + man.wife = woman; + + return { + father: man, + mother: woman + } +} + +let family = marry({ + name: "John" +}, { + name: "Ann" +}); +``` + +Функция `marry` "женит" два объекта, давая им ссылки друг на друга, и возвращает новый объект, содержащий ссылки на два предыдущих. + +В результате получаем такую структуру памяти: + +![](family.svg) + +На данный момент все объекты достижимы. + +Теперь удалим две ссылки: + +```js +delete family.father; +delete family.mother.husband; +``` + +![](family-delete-refs.svg) + +Недостаточно удалить только одну из этих двух ссылок, потому что все объекты останутся достижимыми. + +Но если мы удалим обе, то увидим, что у объекта John больше нет входящих ссылок: + +![](family-no-father.svg) + +Исходящие ссылки не имеют значения. Только входящие ссылки могут сделать объект достижимым. Объект John теперь недостижим и будет удалён из памяти со всеми своими данными, которые также стали недоступны. + +После сборки мусора: + +![](family-no-father-2.svg) + +## Недостижимый "остров" + +Вполне возможна ситуация, при которой целый "остров" взаимосвязанных объектов может стать недостижимым и удалиться из памяти. + +Возьмём объект `family` из примера выше. А затем: + +```js +family = null; +``` + +Структура в памяти теперь станет такой: + +![](family-no-family.svg) + +Этот пример демонстрирует, насколько важна концепция достижимости. + +Объекты John и Ann всё ещё связаны, оба имеют входящие ссылки, но этого недостаточно. + +Бывший объект `family` был отсоединён от корня, на него больше нет ссылки, поэтому весь "остров" становится недостижимым и будет удалён. + +## Внутренние алгоритмы + +Основной алгоритм сборки мусора называется "алгоритм пометок" (от англ. "mark-and-sweep"). + +Согласно этому алгоритму, сборщик мусора регулярно выполняет следующие шаги: + +- Сборщик мусора "помечает" (запоминает) все корневые объекты. +- Затем он идёт по ним и "помечает" все ссылки из них. +- Затем он идёт по отмеченным объектам и отмечает их ссылки. Все посещённые объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды. +- ...И так далее, пока не будут посещены все достижимые (из корней) ссылки. +- Все непомеченные объекты удаляются. + +Например, пусть наша структура объектов выглядит так: + +![](garbage-collection-1.svg) + +Мы ясно видим "недостижимый остров" справа. Теперь давайте посмотрим, как будет работать "алгоритм пометок" сборщика мусора. + +На первом шаге помечаются корни: + +![](garbage-collection-2.svg) + +Затем помечаются объекты по их ссылкам: + +![](garbage-collection-3.svg) + +...А затем объекты по их ссылкам и так далее, пока это возможно: + +![](garbage-collection-4.svg) + +Теперь объекты, которые не удалось посетить в процессе, считаются недостижимыми и будут удалены: + +![](garbage-collection-5.svg) + +Мы также можем представить себе этот процесс как выливание огромного ведра краски из корней, которая течёт по всем ссылкам и отмечает все достижимые объекты. Затем непомеченные удаляются. + +Это концепция того, как работает сборка мусора. Движки JavaScript применяют множество оптимизаций, чтобы она работала быстрее и не задерживала выполнение кода. + +Вот некоторые из оптимизаций: + +- **Сборка по поколениям (Generational collection)** – объекты делятся на два набора: "новые" и "старые". В типичном коде многие объекты имеют короткую жизнь: они появляются, выполняют свою работу и быстро умирают, так что имеет смысл отслеживать новые объекты и, если это так, быстро очищать от них память. Те, которые выживают достаточно долго, становятся "старыми" и проверяются реже. +- **Инкрементальная сборка (Incremental collection)** – если объектов много, и мы пытаемся обойти и пометить весь набор объектов сразу, это может занять некоторое время и привести к видимым задержкам в выполнении скрипта. Так что движок делит всё множество объектов на части, и далее очищает их одну за другой. Получается несколько небольших сборок мусора вместо одной всеобщей. Это требует дополнительного учёта для отслеживания изменений между частями, но зато получается много крошечных задержек вместо одной большой. +- **Сборка в свободное время (Idle-time collection)** - чтобы уменьшить возможное влияние на производительность, сборщик мусора старается работать только во время простоя процессора. + +Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось описать их здесь, я должен воздержаться, потому что разные движки реализуют разные хитрости и методы. И, что ещё более важно, все меняется по мере развития движков, поэтому изучать тему глубоко "заранее", без реальной необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будет несколько ссылок ниже. + +## Итого + +Главное, что нужно знать: + +- Сборка мусора выполняется автоматически. Мы не можем ускорить или предотвратить её. +- Объекты сохраняются в памяти, пока они достижимы. +- Если на объект есть ссылка - вовсе не факт, что он является достижимым (из корня): набор взаимосвязанных объектов может стать недоступен в целом, как мы видели в примере выше. + +Современные движки реализуют разные продвинутые алгоритмы сборки мусора. + +О многих из них рассказано в прекрасной книге о сборке мусора "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones и др.). + +Если вы знакомы с низкоуровневым программированием, то более подробная информация о сборщике мусора V8 находится в статье [A tour of V8: Garbage Collection](https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). + +Также в [блоге V8](https://v8.dev/) время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы изучить сборку мусора, вам лучше подготовиться, узнав о том как устроен движок V8 внутри в целом и почитав блог [Вячеслава Егорова](https://mrale.ph), одного из инженеров, разрабатывавших V8. Я говорю про "V8", потому что он лучше всего освещается в статьях в Интернете. Для других движков многие подходы схожи, но сборка мусора отличается во многих аспектах. + +Глубокое понимание работы движков полезно, когда вам нужна низкоуровневая оптимизация. Было бы разумно запланировать их изучение как следующий шаг после того, как вы познакомитесь с языком. diff --git a/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg b/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg new file mode 100644 index 0000000000..a582ca64b7 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg @@ -0,0 +1 @@ +<global variable>ObjectObjectwifefamilyname: "John"name: "Ann"motherObjectfatherhusband \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family-no-family.svg b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg new file mode 100644 index 0000000000..c73dd6a48f --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg @@ -0,0 +1 @@ +<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg similarity index 57% rename from 1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg rename to 1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg index 37255e5737..6bd13c0e80 100644 --- a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg +++ b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg @@ -1 +1 @@ -Objectfamilyname: "Ann"motherObject<global> \ No newline at end of file +Objectfamilyname: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family-no-father.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg new file mode 100644 index 0000000000..fd1f206073 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg @@ -0,0 +1 @@ +ObjectObjectwifefamilyname: "John"name: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family.svg b/1-js/04-object-basics/03-garbage-collection/family.svg new file mode 100644 index 0000000000..fd05348740 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family.svg @@ -0,0 +1 @@ +ObjectObjectfatherwifefamilyname: "John"name: "Ann"motherObjecthusband<global variable> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg new file mode 100644 index 0000000000..5cac52e9a9 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg new file mode 100644 index 0000000000..7dd3a693a8 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg new file mode 100644 index 0000000000..1060577874 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg new file mode 100644 index 0000000000..bd485adeeb --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg new file mode 100644 index 0000000000..6de756f7cf --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg @@ -0,0 +1 @@ +<global>недостижимые \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg similarity index 60% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg index 9ad1d88bce..1913243548 100644 --- a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg @@ -1 +1 @@ -username: "John"Objectadmin<global> \ No newline at end of file +username: "John"Objectadmin<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg similarity index 75% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg index e75b8d465e..07914a9ca0 100644 --- a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg @@ -1 +1 @@ -name: "John"Objectuser: null<global> \ No newline at end of file +name: "John"Objectuser: null<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg similarity index 66% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john.svg index f051391d8c..15bd51afb1 100644 --- a/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg @@ -1 +1 @@ -username: "John"Object<global> \ No newline at end of file +username: "John"Object<global> \ No newline at end of file diff --git a/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md b/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md index fcdae3f3e5..8856617f58 100644 --- a/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md +++ b/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md @@ -4,41 +4,52 @@ ```js run function makeUser() { return { - name: "Джон", + name: "John", ref: this }; -}; +} let user = makeUser(); alert( user.ref.name ); // Error: Cannot read property 'name' of undefined ``` -Это потому, что правила, которые определяют значение `this`, никак не смотрят на объявление объекта. Важен лишь момент вызова метода. +Это потому, что правила, которые определяют значение `this`, никак не смотрят на объявление объекта. Важен лишь момент вызова. -Здесь значение `this` внутри `makeUser()` является `undefined`, потому что `makeUser()` вызвана как функция, не через "точку" как метод. +Здесь значение `this` внутри `makeUser()` равно `undefined`, потому что оно вызывается как функция, а не через "точечный" синтаксис как метод. -Литерал объекта сам по себе не влияет на `this`. Значение `this` одно для всей функции и блоков кода в ней, литеральные объекты не меняют его. +Значение `this` одно для всей функции, блоки кода и объектные литералы на него не влияют. -Таким образом, при создании объекта `ref: this` берёт текущее значение `this` функции `makeUser()`. +Таким образом, `ref: this` фактически принимает текущее `this` функции `makeUser()`. -А вот противоположный случай: +Мы можем переписать функцию и вернуть то же самое `this` со значением `undefined`: + +```js run +function makeUser(){ + return this; // на этот раз нет литерала объекта +} + +alert( makeUser().name ); // Error: Cannot read property 'name' of undefined +``` +Как вы можете видеть, результат `alert( makeUser().name )` совпадает с результатом `alert( user.ref.name )` из предыдущего примера. + +Вот противоположный случай: ```js run function makeUser() { return { - name: "Джон", + name: "John", *!* ref() { return this; } */!* }; -}; +} let user = makeUser(); -alert( user.ref().name ); // Джон +alert( user.ref().name ); // John ``` -Теперь это работает, поскольку `user.ref()` вызывается как метод. И значением `this` становится объект перед точкой `.`. +Теперь это работает, поскольку `user.ref()` - это метод. И значением `this` становится объект перед точкой `.`. diff --git a/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md b/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md index 9f0e04d0a2..f40a201fd8 100644 --- a/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md +++ b/1-js/04-object-basics/04-object-methods/4-object-property-this/task.md @@ -11,10 +11,10 @@ importance: 5 ```js function makeUser() { return { - name: "Джон", + name: "John", ref: this }; -}; +} let user = makeUser(); diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js b/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js index bc8139ff5e..bde14d6cd4 100644 --- a/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js +++ b/1-js/04-object-basics/04-object-methods/7-calculator/_js.view/test.js @@ -2,7 +2,7 @@ describe("калькулятор", function() { - context("Когда 2 и 3 введены", function() { + context("когда 2 и 3 введены", function() { beforeEach(function() { sinon.stub(window, "prompt"); @@ -15,12 +15,17 @@ describe("калькулятор", function() { afterEach(function() { prompt.restore(); }); + + it('read получает два значения и сохраняет их как свойства объекта', function () { + assert.equal(calculator.a, 2); + assert.equal(calculator.b, 3); + }); - it("Сумма равна 5", function() { + it("сумма равна 5", function() { assert.equal(calculator.sum(), 5); }); - it("Произведение равно 6", function() { + it("произведение равно 6", function() { assert.equal(calculator.mul(), 6); }); }); diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/task.md b/1-js/04-object-basics/04-object-methods/7-calculator/task.md index 5bf85f6e6c..f6c93164fb 100644 --- a/1-js/04-object-basics/04-object-methods/7-calculator/task.md +++ b/1-js/04-object-basics/04-object-methods/7-calculator/task.md @@ -6,7 +6,7 @@ importance: 5 Создайте объект `calculator` (калькулятор) с тремя методами: -- `read()` (читать) запрашивает два значения и сохраняет их как свойства объекта. +- `read()` (читать) запрашивает два значения и сохраняет их как свойства объекта с именами `a` и `b`. - `sum()` (суммировать) возвращает сумму сохранённых значений. - `mul()` (умножить) перемножает сохранённые значения и возвращает результат. diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js index e98fe6410c..a35c009cce 100644 --- a/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/solution.js @@ -11,5 +11,6 @@ let ladder = { }, showStep: function() { alert(this.step); + return this; } }; \ No newline at end of file diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js index 14a8c13de4..c44088c7c0 100644 --- a/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/_js.view/test.js @@ -21,17 +21,25 @@ describe('Лестница', function() { assert(alert.called); }); - it('up() должен увеличивать ступеньки', function() { + it('up() должен увеличивать ступеньку', function() { assert.equal(ladder.up().up().step, 2); }); - it('down() должен уменьшать ступеньки', function() { + it('down() должен уменьшать ступеньку', function() { assert.equal(ladder.down().step, -1); }); it('down().up().up().up() ', function() { assert.equal(ladder.down().up().up().up().step, 2); }); + + it('showStep() должен возвращать this', function() { + assert.equal(ladder.showStep(), ladder); + }); + + it('up().up().down().showStep().down().showStep()', function () { + assert.equal(ladder.up().up().down().showStep().down().showStep().step, 0) + }); after(function() { ladder.step = 0; diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md b/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md index c8b4b87a3c..f42c4e0eb1 100644 --- a/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/solution.md @@ -1,4 +1,4 @@ -Решением является возврат самого объекта в каждом методе. +Решение состоит в том, чтобы возвращать сам объект из каждого вызова. ```js run demo let ladder = { @@ -21,19 +21,19 @@ let ladder = { return this; */!* } -} +}; -ladder.up().up().down().up().down().showStep(); // 1 +ladder.up().up().down().showStep().down().showStep(); // показывает 1 затем 0 ``` -Мы также можем писать один вызов на одной строке. Для длинной цепи вызовов это более читабельно: +Мы также можем записать один вызов на одной строке. Для длинных цепей вызовов это более читабельно: ```js ladder .up() .up() .down() - .up() + .showStep() // 1 .down() - .showStep(); // 1 + .showStep(); // 0 ``` diff --git a/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md b/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md index 2e492c253a..e24f15ce83 100644 --- a/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md +++ b/1-js/04-object-basics/04-object-methods/8-chain-calls/task.md @@ -4,7 +4,7 @@ importance: 2 # Цепь вызовов -Это `ladder` (лестница) - объект, который позволяет подниматься вверх и спускаться: +У нас есть объект `ladder` (лестница), который позволяет подниматься и спускаться: ```js let ladder = { @@ -21,19 +21,21 @@ let ladder = { }; ``` -Теперь, если нам нужно сделать несколько последовательных вызовов, мы можем выполнить это так: +Теперь, если нам нужно выполнить несколько последовательных вызовов, мы можем сделать это так: ```js ladder.up(); ladder.up(); ladder.down(); ladder.showStep(); // 1 +ladder.down(); +ladder.showStep(); // 0 ``` Измените код методов `up`, `down` и `showStep` таким образом, чтобы их вызов можно было сделать по цепочке, например так: ```js -ladder.up().up().down().showStep(); // 1 +ladder.up().up().down().showStep().down().showStep(); // показывает 1 затем 0 ``` Такой подход широко используется в библиотеках JavaScript. diff --git a/1-js/04-object-basics/04-object-methods/article.md b/1-js/04-object-basics/04-object-methods/article.md index b4a3c06e88..2596e90361 100644 --- a/1-js/04-object-basics/04-object-methods/article.md +++ b/1-js/04-object-basics/04-object-methods/article.md @@ -5,14 +5,14 @@ ```js // Объект пользователя let user = { - name: "Джон", + name: "John", age: 30 }; ``` И так же, как и в реальном мире, пользователь может *совершать действия*: выбирать что-то из корзины покупок, авторизовываться, выходить из системы, оплачивать и т.п. -Такие действия в JavaScript представлены свойствами-функциями объекта. +Такие действия в JavaScript представлены функциями в свойствах. ## Примеры методов @@ -20,7 +20,7 @@ let user = { ```js run let user = { - name: "Джон", + name: "John", age: 30 }; @@ -33,15 +33,15 @@ user.sayHi = function() { user.sayHi(); // Привет! ``` -Здесь мы просто использовали Function Expression (функциональное выражение), чтобы создать функцию для приветствия, и присвоили её свойству `user.sayHi` нашего объекта. +Здесь мы просто использовали Function Expression (функциональное выражение), чтобы создать функцию приветствия, и присвоили её свойству `user.sayHi` нашего объекта. -Затем мы вызвали её. Теперь пользователь может говорить! +Затем мы можем вызвать ee как `user.sayHi()`. Теперь пользователь может говорить! Функцию, которая является свойством объекта, называют *методом* этого объекта. Итак, мы получили метод `sayHi` объекта `user`. -Конечно, мы могли бы заранее объявить функцию и использовать её в качестве метода, примерно так: +Конечно, мы могли бы использовать заранее объявленную функцию в качестве метода, вот так: ```js run let user = { @@ -49,10 +49,10 @@ let user = { }; *!* -// сначала объявляем +// сначала, объявляем function sayHi() { alert("Привет!"); -}; +} // затем добавляем в качестве метода user.sayHi = sayHi; @@ -62,16 +62,16 @@ user.sayHi(); // Привет! ``` ```smart header="Объектно-ориентированное программирование" -Когда мы пишем наш код, используя объекты для представления сущностей реального мира, - это называется [объектно-ориентированное программирование](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) или сокращённо: "ООП". +Когда мы пишем наш код, используя объекты для представления сущностей реального мира, - это называется [объектно-ориентированным программированием](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) или сокращённо: "ООП". -ООП является большой предметной областью и интересной наукой само по себе. Как выбрать правильные сущности? Как организовать взаимодействие между ними? Это -- создание архитектуры, и есть хорошие книги по этой теме, такие как "Приёмы объектно-ориентированного проектирования. Паттерны проектирования" авторов Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес или "Объектно-ориентированный анализ и проектирование с примерами приложений" Гради Буча, а также ещё множество других книг. +ООП является большой предметной областью и интересной наукой самой по себе. Как выбрать правильные сущности? Как организовать взаимодействие между ними? Это -- создание архитектуры, и на эту тему есть отличные книги, такие как "Приёмы объектно-ориентированного проектирования. Паттерны проектирования" авторов Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес или "Объектно-ориентированный анализ и проектирование с примерами приложений" Гради Буча, а также ещё множество других книг. ``` ### Сокращённая запись метода Существует более короткий синтаксис для методов в литерале объекта: ```js -// эти объекты делают одно и то же (одинаковые методы) +// эти объекты делают одно и то же user = { sayHi: function() { @@ -82,7 +82,7 @@ user = { // сокращённая запись выглядит лучше, не так ли? user = { *!* - sayHi() { // то же самое, что и "sayHi: function()" + sayHi() { // то же самое, что и "sayHi: function(){...}" */!* alert("Привет"); } @@ -91,62 +91,62 @@ user = { Как было показано, мы можем пропустить ключевое слово `"function"` и просто написать `sayHi()`. -Нужно отметить, что эти две записи не полностью эквивалентны. Есть тонкие различия, связанные с наследованием объектов (что будет рассмотрено позже), но на данном этапе изучения это неважно. В большинстве случаев сокращённый синтаксис предпочтителен. +Нужно отметить, что эти две записи не полностью эквивалентны. Есть тонкие различия, связанные с наследованием объектов (что будет рассмотрено позже), но на данном этапе изучения это неважно. Почти во всех случаях сокращённый синтаксис предпочтителен. ## Ключевое слово "this" в методах -Как правило, методу объекта необходим доступ к информации, которая хранится в объекте, чтобы выполнить с ней какие-либо действия (в соответствии с назначением метода). +Как правило, методу объекта обычно требуется доступ к информации, хранящейся в объекте, для выполнения своей работы. -Например, коду внутри `user.sayHi()` может понадобиться имя пользователя, которое хранится в объекте `user`. +Например, коду внутри `user.sayHi()` может потребоваться имя пользователя, которое хранится в объекте `user`. **Для доступа к информации внутри объекта метод может использовать ключевое слово `this`.** -Значение `this` - это объект "перед точкой", который использовался для вызова метода. +Значение `this` - это объект "перед точкой", который используется для вызова метода. Например: ```js run let user = { - name: "Джон", + name: "John", age: 30, sayHi() { *!* - // this - это "текущий объект" + // "this" - это "текущий объект". alert(this.name); */!* } }; -user.sayHi(); // Джон +user.sayHi(); // John ``` Здесь во время выполнения кода `user.sayHi()` значением `this` будет являться `user` (ссылка на объект `user`). -Технически также возможно получить доступ к объекту без ключевого слова `this`, ссылаясь на него через внешнюю переменную (в которой хранится ссылка на этот объект): +Технически также возможно получить доступ к объекту без ключевого слова `this`, обратившись к нему через внешнюю переменную (в которой хранится ссылка на этот объект): ```js let user = { - name: "Джон", + name: "John", age: 30, sayHi() { *!* - alert(user.name); // используем переменную "user" вместо ключевого слова "this" + alert(user.name); // "user" вместо "this" */!* } }; ``` -...Но такой код будет ненадёжным. Если мы решим скопировать ссылку на объект `user` в другую переменную, например, `admin = user`, и перезапишем переменную `user` чем-то другим, тогда будет осуществлён доступ к неправильному объекту при вызове метода из `admin`. +...Но такой код ненадёжен. Если мы решим скопировать ссылку на объект `user` в другую переменную, например, `admin = user`, и перезапишем переменную `user` чем-то другим, тогда будет осуществлён доступ к неправильному объекту при вызове метода из `admin`. Это показано ниже: ```js run let user = { - name: "Джон", + name: "John", age: 30, sayHi() { @@ -159,18 +159,20 @@ let user = { let admin = user; -user = null; // обнулим переменную для наглядности, теперь она не хранит ссылку на объект. +user = null; // перезапишем переменную для наглядности, теперь она не хранит ссылку на объект. -admin.sayHi(); // Ошибка! Внутри sayHi() используется user, которая больше не ссылается на объект! +*!* +admin.sayHi(); // TypeError: Cannot read property 'name' of null +*/!* ``` -Если мы используем `this.name` вместо `user.name` внутри `alert`, тогда этот код будет работать. +Если бы мы использовали `this.name` вместо `user.name` внутри `alert`, тогда этот код бы сработал. ## "this" не является фиксированным -В JavaScript ключевое слово "this" ведёт себя иначе, чем в большинстве других языков программирования. Оно может использоваться в любой функции. +В JavaScript ключевое слово "this" ведёт себя иначе, чем в большинстве других языков программирования. Его можно использовать в любой функции, даже если это не метод объекта. -В этом коде нет синтаксической ошибки: +В следующем примере нет синтаксической ошибки: ```js function sayHi() { @@ -178,13 +180,13 @@ function sayHi() { } ``` -Значение `this` вычисляется во время выполнения кода и зависит от контекста. +Значение `this` вычисляется во время выполнения кода, в зависимости от контекста. -Например, здесь одна и та же функция назначена двум разным объектам и имеет различное значение "this" при вызовах: +Например, здесь одна и та же функция назначена двум разным объектам и имеет различное значение "this" в вызовах: ```js run -let user = { name: "Джон" }; -let admin = { name: "Админ" }; +let user = { name: "John" }; +let admin = { name: "Admin" }; function sayHi() { alert( this.name ); @@ -196,18 +198,18 @@ user.f = sayHi; admin.f = sayHi; */!* -// вызовы функции, приведённые ниже, имеют разное значение this -// "this" внутри функции является ссылкой на объект, который указан "перед точкой" -user.f(); // Джон (this == user) -admin.f(); // Админ (this == admin) +// эти вызовы имеют разное значение this +// "this" внутри функции - это объект "перед точкой" +user.f(); // John (this == user) +admin.f(); // Admin (this == admin) -admin['f'](); // Админ (неважен способ доступа к методу - через точку или квадратные скобки) +admin['f'](); // Admin (нет разницы между использованием точки или квадратных скобок для доступа к объекту) ``` -Правило простое: при вызове `obj.f()` значение `this` внутри `f` равно `obj`. Так что, в приведённом примере это `user` или `admin`. +Правило простое: если вызывается `obj.f()`, то во время вызова `f`, `this` - это `obj`. Так что, в приведённом выше примере это либо `user`, либо `admin`. ````smart header="Вызов без объекта: `this == undefined`" -Мы даже можем вызвать функцию вовсе без использования объекта: +Мы даже можем вызвать функцию вообще без объекта: ```js run function sayHi() { @@ -217,146 +219,53 @@ function sayHi() { sayHi(); // undefined ``` -В строгом режиме (`"use strict"`) в таком коде значением `this` будет являться `undefined`. Если мы попытаемся получить доступ к `name`, используя `this.name` - это вызовет ошибку. +В строгом режиме (`"use strict"`) в таком коде значением `this` будет являться `undefined`. Если мы попытаемся получить доступ к `this.name` - это вызовет ошибку. -В нестрогом режиме значением `this` в таком случае будет *глобальный объект* (`window` для браузера, мы вернёмся к этому позже в главе [Глобальный объект](info:global-object)). Это -- исторически сложившееся поведение `this`, которое исправляется использованием строгого режима (`"use strict"`). +В нестрогом режиме значением `this` в таком случае будет *глобальный объект* (`window` в браузерe, мы вернёмся к этому позже в главе [Глобальный объект](info:global-object)). Это -- исторически сложившееся поведение `this`, которое исправляется использованием строгого режима (`"use strict"`). -Обычно подобный вызов является ошибкой программирования. Если внутри функции используется `this`, тогда ожидается, что она будет вызываться в контексте какого-либо объекта. +Обычно подобный вызов является ошибкой программирования. Если внутри функции используется `this`, тогда она ожидает, что будет вызвана в контексте какого-либо объекта. ```` ```smart header="Последствия свободного `this`" -Если вы до этого изучали другие языки программирования, тогда вы, скорее всего, привыкли к идее "фиксированного `this`" - когда методы, определённые внутри объекта, всегда сохраняют в качестве значения `this` ссылку на свой объект (в котором был определён метод). - -В JavaScript `this` является "свободным", его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а зависит от того, какой объект вызывает метод (какой объект стоит "перед точкой"). - -Эта идея вычисления `this` в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок. - -Здесь мы не будем судить о том, является ли это решение в языке хорошим или плохим. Мы должны понимать, как с этим работать, чтобы получать выгоды и избегать проблем. -``` - -## Внутренняя реализация: Ссылочный тип +Если вы до этого изучали другие языки программирования, то вы, вероятно, привыкли к идее "фиксированного`this`" - когда методы, определённые в объекте, всегда имеют `this`, ссылающееся на этот объект. -```warn header="Продвинутая возможность языка" -Этот раздел объясняет сложную тему, чтобы лучше понимать некоторые запутанные случаи. - -Если вы хотите продвигаться быстрее, его можно пропустить или отложить. -``` +В JavaScript `this` является "свободным", его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а скорее от того, какой объект вызывает метод (какой объект стоит "перед точкой"). -Некоторые хитрые способы вызова метода приводят к потере значения `this`, например: +Эта концепция вычисления `this` в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок. -```js run -let user = { - name: "Джон", - hi() { alert(this.name); }, - bye() { alert("Пока"); } -}; - -user.hi(); // Джон (простой вызов метода работает хорошо) - -*!* -// теперь давайте попробуем вызывать user.hi или user.bye -// в зависимости от имени пользователя user.name -(user.name == "Джон" ? user.hi : user.bye)(); // Ошибка! -*/!* -``` - -В последней строчке кода используется условный оператор `?`, который определяет, какой будет вызван метод (`user.hi` или `user.bye`) в зависимости от выполнения условия. В данном случае будет выбран `user.hi`. - -Затем метод тут же вызывается с помощью скобок `()`. Но вызов не работает как положено! - -Вы можете видеть, что при вызове будет ошибка, потому что значением `"this"` внутри функции становится `undefined` (полагаем, что у нас строгий режим). - -Так работает (доступ к методу объекта через точку): -```js -user.hi(); -``` - -Так уже не работает (вызываемый метод вычисляется): -```js -(user.name == "Джон" ? user.hi : user.bye)(); // Ошибка! +Здесь наша позиция заключается не в том, чтобы судить, является ли это архитектурное решение в языке хорошим или плохим. Скоро мы поймем, как с этим работать, как получить выгоду и избежать проблем. ``` -Почему? Если мы хотим понять, почему так происходит, давайте разберёмся (заглянем под капот), как работает вызов методов (`obj.method()`). - -Присмотревшись поближе, в выражении `obj.method()` можно заметить две операции: - -1. Сначала оператор точка `'.'` возвращает свойство объекта - его метод (`obj.method`). -2. Затем скобки `()` вызывают этот метод (исполняется код метода). - -Итак, каким же образом информация о `this` передаётся из первой части во вторую? - -Если мы поместим эти операции в отдельные строки, то значение `this`, естественно, будет потеряно: - -```js run -let user = { - name: "Джон", - hi() { alert(this.name); } -} - -*!* -// разделим получение метода объекта и его вызов в разных строках -let hi = user.hi; -hi(); // Ошибка, потому что значением this является undefined -*/!* -``` - -Здесь `hi = user.hi` сохраняет функцию в переменной, и далее в последней строке она вызывается полностью сама по себе, без объекта, так что нет `this`. - -**Для работы вызовов типа `user.hi()`, JavaScript использует трюк - точка `'.'` возвращает не саму функцию, а специальное значение "ссылочного типа", называемого [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).** - -Этот ссылочный тип (Reference Type) является внутренним типом. Мы не можем явно использовать его, но он используется внутри языка. - -Значение ссылочного типа - это "триплет": комбинация из трёх значений `(base, name, strict)`, где: - -- `base` - это объект. -- `name` - это имя свойства объекта. -- `strict` - это режим исполнения. Является true, если действует строгий режим (`use strict`). - -Результатом доступа к свойству `user.hi` является не функция, а значение ссылочного типа. Для `user.hi` в строгом режиме оно будет таким: - -```js -// значение ссылочного типа (Reference Type) -(user, "hi", true) -``` - -Когда скобки `()` применяются к значению ссылочного типа (происходит вызов), то они получают полную информацию об объекте и его методе, и могут поставить правильный `this` (`=user` в данном случае, по `base`). - -Ссылочный тип - исключительно внутренний, промежуточный, используемый, чтобы передать информацию от точки `.` до вызывающих скобок `()`. - -При любой другой операции, например, присваивании `hi = user.hi`, ссылочный тип заменяется на собственно значение `user.hi` (функцию), и дальше работа уже идёт только с ней. Поэтому дальнейший вызов происходит уже без `this`. - -Таким образом, значение `this` передаётся правильно, только если функция вызывается напрямую с использованием синтаксиса точки `obj.method()` или квадратных скобок `obj['method']()` (они делают то же самое). Позднее в этом учебнике мы изучим различные варианты решения проблемы потери значения `this`. Например, такие как [func.bind()](/bind#solution-2-bind). - ## У стрелочных функций нет "this" -Стрелочные функции особенные: у них нет своего "собственного" `this`. Если мы используем `this` внутри стрелочной функции, то его значение берётся из внешней "нормальной" функции. +Стрелочные функции особенные: у них нет своего "собственного" `this`. Если мы ссылаемся на `this` внутри такой функции, то оно берётся из внешней "нормальной" функции. Например, здесь `arrow()` использует значение `this` из внешнего метода `user.sayHi()`: ```js run let user = { - firstName: "Илья", + firstName: "Ilya", sayHi() { let arrow = () => alert(this.firstName); arrow(); } }; -user.sayHi(); // Илья +user.sayHi(); // Ilya ``` -Это является особенностью стрелочных функций. Они полезны, когда мы на самом деле не хотим иметь отдельное значение `this`, а хотим брать его из внешнего контекста. Позднее в главе мы увидим больше примеров на эту тему. +Это особенность стрелочных функций. Она полезна, когда мы на самом деле не хотим иметь отдельное `this`, а скорее хотим взять его из внешнего контекста. Позже в главе мы увидим больше примеров на эту тему. ## Итого -- Функции, которые находятся в объекте в качестве его свойств, называются "методами". +- Функции, которые находятся в свойствах объекта, называются "методами". - Методы позволяют объектам "действовать": `object.doSomething()`. - Методы могут ссылаться на объект через `this`. Значение `this` определяется во время исполнения кода. - При объявлении любой функции в ней можно использовать `this`, но этот `this` не имеет значения до тех пор, пока функция не будет вызвана. -- Эта функция может быть скопирована между объектами (из одного объекта в другой). -- Когда функция вызывается синтаксисом "метода" - `object.method()`, значением `this` во время вызова является объект перед точкой. +- Функция может быть скопирована между объектами (из одного объекта в другой). +- Когда функция вызывается синтаксисом "метода" - `object.method()`, значением `this` во время вызова является `object`. -Также ещё раз заметим, что стрелочные функции являются особенными - у них нет `this`. Когда внутри стрелочной функции обращаются к `this`, то его значение берётся снаружи. +Также ещё раз заметим, что стрелочные функции являются особенными - у них нет `this`. Когда внутри стрелочной функции обращаются к `this`, то его значение берётся извне. diff --git a/1-js/04-object-basics/05-object-toprimitive/article.md b/1-js/04-object-basics/05-object-toprimitive/article.md deleted file mode 100644 index 4ba85fb740..0000000000 --- a/1-js/04-object-basics/05-object-toprimitive/article.md +++ /dev/null @@ -1,223 +0,0 @@ - -# Преобразование объектов в примитивы - -Что произойдёт, если сложить два объекта `obj1 + obj2`, вычесть один из другого `obj1 - obj2` или вывести их на экран, воспользовавшись `alert(obj)`? - -В этом случае объекты сначала автоматически преобразуются в примитивы, а затем выполняется операция. - -В главе мы видели правила для численных, строковых и логических преобразований. Но обделили вниманием объекты. Теперь, поскольку мы уже знаем о методах объектов и символах, можно исправить это упущение. - -1. Все объекты в логическом контексте являются `true`. Существуют лишь их численные и строковые преобразования. -2. Численные преобразования происходят, когда мы вычитаем объекты или выполняем математические операции. Например, объекты `Date` (мы рассмотрим их в статье ) могут вычитаться, и результатом `date1 - date2` будет временной отрезок между двумя датами. -3. Что касается строковых преобразований -- они обычно происходят, когда мы выводим объект `alert(obj)`, а также в других случаях, когда объект используется как строка. - -## Преобразование к примитивам - -Мы можем тонко настраивать строковые и численные преобразования, используя специальные методы объекта. - -Существуют три варианта преобразований ("три хинта"), описанные в [спецификации](https://tc39.github.io/ecma262/#sec-toprimitive): - -`"string"` -: Для преобразования объекта к строке, когда операция ожидает получить строку, например `alert`: - - ```js - // вывод - alert(obj); - - // используем объект в качестве имени свойства - anotherObj[obj] = 123; - ``` - -`"number"` -: Для преобразования объекта к числу, в случае математических операций: - - ```js - // явное преобразование - let num = Number(obj); - - // математическое (исключая бинарный оператор "+") - let n = +obj; // унарный плюс - let delta = date1 - date2; - - // сравнения больше/меньше - let greater = user1 > user2; - ``` - -`"default"` -: Происходит редко, когда оператор "не уверен", какой тип ожидать. - - Например, бинарный плюс `+` может работать с обоими типами: строками (объединять их) и числами (складывать). Таким образом, и те, и другие будут вычисляться. Или когда происходит сравнение объектов с помощью нестрогого равенства `==` со строкой, числом или символом, и неясно, какое преобразование должно быть выполнено. - - ```js - // бинарный плюс - let total = car1 + car2; - - // obj == string/number/symbol - if (user == 1) { ... }; - ``` - - Оператор больше/меньше `<>` также может работать как со строками, так и с числами. Однако, по историческим причинам он использует хинт "number", а не "default". - - На практике все встроенные объекты, исключая `Date` (мы познакомимся с ним чуть позже), реализуют `"default"` преобразования тем же способом, что и `"number"`. И нам следует поступать также. - -Обратите внимание, что существуют лишь три варианта хинтов. Всё настолько просто. Не существует хинта со значением "boolean" (все объекты являются `true` в логическом контексте) или каких-либо ещё. И если мы считаем `"default"` и `"number"` одинаковыми, как большинство встроенных объектов, то остаются всего два варианта преобразований. - -**В процессе преобразования движок JavaScript пытается найти и вызвать три следующих метода объекта:** - -1. Вызывает `obj[Symbol.toPrimitive](hint)` - метод с символьным ключом `Symbol.toPrimitive` (системный символ), если такой метод существует, и передаёт ему хинт. -2. Иначе, если хинт равен `"string"` - - пытается вызвать `obj.toString()`, а если его нет, то `obj.valueOf()`, если он существует. -3. В случае, если хинт равен `"number"` или `"default"` - - пытается вызвать `obj.valueOf()`, а если его нет, то `obj.toString()`, если он существует. - -## Symbol.toPrimitive - -Начнём с универсального подхода - символа `Symbol.toPrimitive`: метод с таким названием (если есть) используется для всех преобразований: - -```js -obj[Symbol.toPrimitive] = function(hint) { - // должен вернуть примитивное значение - // hint равно чему-то одному из: "string", "number" или "default" -}; -``` - -Для примера используем его в реализации объекта `user`: - -```js run -let user = { - name: "John", - money: 1000, - - [Symbol.toPrimitive](hint) { - alert(`hint: ${hint}`); - return hint == "string" ? `{name: "${this.name}"}` : this.money; - } -}; - -// демонстрация результатов преобразований: -alert(user); // hint: string -> {name: "John"} -alert(+user); // hint: number -> 1000 -alert(user + 500); // hint: default -> 1500 -``` - -Как мы видим из кода, `user` преобразовывается либо в информативную читаемую строку, либо в денежный счёт в зависимости от значения хинта. Единственный метод `user[Symbol.toPrimitive]` смог обработать все случаи преобразований. - - -## Методы toString/valueOf - -Методы `toString` и `valueOf` берут своё начало с древних времён. Они не символы, так как в то время символов ещё не существовало, а просто обычные методы объектов со строковыми именами. Они предоставляют "устаревший" способ реализации преобразований объектов. - -Если нет метода `Symbol.toPrimitive`, движок JavaScript пытается найти эти методы и вызвать их следующим образом: - -- `toString -> valueOf` для хинта со значением "string". -- `valueOf -> toString` -- в ином случае. - -Для примера, используем их в реализации всё того же объекта `user`. Воспроизведём его поведение комбинацией методов `toString` и `valueOf`: - -```js run -let user = { - name: "John", - money: 1000, - - // для хинта равного "string" - toString() { - return `{name: "${this.name}"}`; - }, - - // для хинта равного "number" или "default" - valueOf() { - return this.money; - } - -}; - -alert(user); // toString -> {name: "John"} -alert(+user); // valueOf -> 1000 -alert(user + 500); // valueOf -> 1500 -``` - -Как видим, получилось то же поведение, что и в предыдущем примере с `Symbol.toPrimitive`. - -Довольно часто мы хотим описать одно "универсальное" преобразование объекта к примитиву для всех ситуаций. Для этого достаточно создать один `toString`: - -```js run -let user = { - name: "John", - - toString() { - return this.name; - } -}; - -alert(user); // toString -> John -alert(user + 500); // toString -> John500 -``` - -В отсутствие `Symbol.toPrimitive` и `valueOf`, `toString` обработает все случаи преобразований к примитивам. - - -## Возвращаемые типы - -Важно понимать, что все описанные методы для преобразований объектов не обязаны возвращать именно требуемый "хинтом" тип примитива. - -Нет обязательного требования, чтобы `toString()` возвращал именно строку, или чтобы метод `Symbol.toPrimitive` возвращал именно число для хинта "number". - -**Единственное обязательное требование: методы должны возвращать примитив, а не объект.** - -```smart header="Историческая справка" -По историческим причинам, если `toString` или `valueOf` вернёт объект, то ошибки не будет, но такое значение будет проигнорировано (как если бы метода вообще не существовало). - -Метод `Symbol.toPrimitive`, напротив, *обязан* возвращать примитив, иначе будет ошибка. -``` - -## Последующие операции - -Операция, инициировавшая преобразование, получает примитив и затем продолжает работу с ним, производя дальнейшие преобразования, если это необходимо. - -Например: - -- Математические операции, исключая бинарный плюс, преобразуют примитив к числу: - - ```js run - let obj = { - // toString обрабатывает все преобразования в случае отсутствия других методов - toString() { - return "2"; - } - }; - - alert(obj * 2); // 4, объект был преобразован к примитиву "2", затем умножение сделало его числом - ``` - -- Бинарный плюс `+` в аналогичном случае сложит строки: - - ```js run - let obj = { - toString() { - return "2"; - } - }; - - alert(obj + 2); // 22 (преобразование к примитиву вернуло строку => конкатенация) - ``` - -## Итого - -Преобразование объектов в примитивы вызывается автоматически многими встроенными функциями и операторами, которые ожидают примитив в качестве аргумента. - -Существует всего 3 типа преобразований (хинтов): -- `"string"` (для `alert` и других операций, которым нужна строка) -- `"number"` (для математических операций) -- `"default"` (для некоторых операций) - -В спецификации явно указано, какой хинт должен использовать каждый оператор. И существует совсем немного операторов, которые не знают, что ожидать, и используют хинт со значением `"default"`. Обычно для встроенных объектов хинт `"default"` обрабатывается так же, как `"number"`. Таким образом, последние два очень часто объединяют вместе. - -Алгоритм преобразований к примитивам следующий: - -1. Сначала вызывается метод `obj[Symbol.toPrimitive](hint)`, если он существует. -2. Иначе, если хинт равен `"string"` - - происходит попытка вызвать `obj.toString()`, затем `obj.valueOf()`, смотря что есть. -3. Иначе, если хинт равен `"number"` или `"default"` - - происходит попытка вызвать `obj.valueOf()`, затем `obj.toString()`, смотря что есть. - -На практике довольно часто достаточно реализовать только `obj.toString()` как "универсальный" метод для всех типов преобразований, возвращающий "читаемое" представление объекта, достаточное для логирования или отладки. diff --git a/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md index ed865531cd..06167834b4 100644 --- a/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md +++ b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/solution.md @@ -1,8 +1,8 @@ Да, возможно. -Если функция возвращает объект, то вместо `this` будет возвращён этот объект. +Если функция возвращает объект, то `new` вернёт его вместо `this`. -Например, они могут вернуть один и тот же объект `obj`, определённый снаружи: +Таким образом, они могут, к примеру, возвращать один и тот же внешне определённый объект `obj`: ```js run no-beautify let obj = {}; diff --git a/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md index 5ecac1f57b..33b9cf13d6 100644 --- a/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md +++ b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md @@ -4,17 +4,16 @@ importance: 2 # Две функции - один объект -Возможно ли создать функции `A` и `B` в примере ниже, где объекты равны `new A()==new B()`? - +Возможно ли создать функции `A` и `B`, чтобы `new A() == new B()`? ```js no-beautify function A() { ... } function B() { ... } -let a = new A; -let b = new B; +let a = new A(); +let b = new B(); alert( a == b ); // true ``` -Если да - приведите пример вашего кода. +Если да – приведите пример вашего кода. diff --git a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js index 036053927c..bba80e5c2e 100644 --- a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js @@ -10,6 +10,11 @@ describe("calculator", function() { calculator = new Calculator(); calculator.read(); }); + + it("the read method asks for two values using prompt and remembers them in object properties", function() { + assert.equal(calculator.a, 2); + assert.equal(calculator.b, 3); + }); it("when 2 and 3 are entered, the sum is 5", function() { assert.equal(calculator.sum(), 5); diff --git a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md index b0bc1dfa63..cdc406e6f4 100644 --- a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md @@ -2,14 +2,13 @@ importance: 5 --- -# Создание калькулятора при помощи конструктора +# Создайте калькулятор при помощи конструктора, new Calculator -Создайте функцию-конструктор `Calculator`, который создаёт объекты с тремя методами: +Создайте функцию-конструктор `Calculator`, которая создаёт объекты с тремя методами: - `read()` запрашивает два значения при помощи `prompt` и сохраняет их значение в свойствах объекта. -- `sum()` возвращает сумму введённых свойств. -- `mul()` возвращает произведение введённых свойств. - +- `sum()` возвращает сумму этих свойств. +- `mul()` возвращает произведение этих свойств. Например: diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js b/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js index 585287c542..3c9983aeb1 100644 --- a/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/_js.view/solution.js @@ -2,7 +2,7 @@ function Accumulator(startingValue) { this.value = startingValue; this.read = function() { - this.value += +prompt('How much to add?', 0); + this.value += +prompt('Сколько нужно добавить?', 0); }; } diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md b/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md index d821d9d3ad..ddf7f15797 100644 --- a/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md @@ -2,24 +2,24 @@ importance: 5 --- -# Создаём Accumulator +# Создайте new Accumulator -Напишите функцию-конструктор `Accumulator(startingValue)`. +Создайте функцию-конструктор `Accumulator(startingValue)`. Объект, который она создаёт, должен уметь следующее: - Хранить "текущее значение" в свойстве `value`. Начальное значение устанавливается в аргументе конструктора `startingValue`. -- Метод `read()` использует `prompt` для получения числа и прибавляет его к свойству `value`. +- Метод `read()` должен использовать `prompt` для считывания нового числа и прибавления его к `value`. -Таким образом, свойство `value` является текущей суммой всего, что ввёл пользователь при вызовах метода `read()`, с учётом начального значения `startingValue`. +Другими словами, свойство `value` представляет собой сумму всех введённых пользователем значений, с учётом начального значения `startingValue`. Ниже вы можете посмотреть работу кода: ```js let accumulator = new Accumulator(1); // начальное значение 1 -accumulator.read(); // прибавит ввод prompt к текущему значению -accumulator.read(); // прибавит ввод prompt к текущему значению +accumulator.read(); // прибавляет введённое пользователем значение к текущему значению +accumulator.read(); // прибавляет введённое пользователем значение к текущему значению alert(accumulator.value); // выведет сумму этих значений ``` diff --git a/1-js/04-object-basics/06-constructor-new/article.md b/1-js/04-object-basics/06-constructor-new/article.md index 075fd61fcc..a7a785ce90 100644 --- a/1-js/04-object-basics/06-constructor-new/article.md +++ b/1-js/04-object-basics/06-constructor-new/article.md @@ -1,15 +1,15 @@ -# Конструкторы, создание объектов через "new" +# Конструктор, оператор "new" -Обычный синтаксис `{...}` позволяет создать только один объект. Но зачастую нам нужно создать множество однотипных объектов, таких как пользователи, элементы меню и т.д. +Обычный синтаксис `{...}` позволяет создать только один объект. Но зачастую нам нужно создать множество похожих, однотипных объектов, таких как пользователи, элементы меню и так далее. Это можно сделать при помощи функции-конструктора и оператора `"new"`. ## Функция-конструктор -Функции-конструкторы являются обычными функциями. Но есть два соглашения: +Функции-конструкторы технически являются обычными функциями. Но есть два соглашения: 1. Имя функции-конструктора должно начинаться с большой буквы. -2. Функция-конструктор должна вызываться при помощи оператора `"new"`. +2. Функция-конструктор должна выполняться только с помощью оператора `"new"`. Например: @@ -20,20 +20,20 @@ function User(name) { } *!* -let user = new User("Вася"); +let user = new User("Jack"); */!* -alert(user.name); // Вася +alert(user.name); // Jack alert(user.isAdmin); // false ``` Когда функция вызывается как `new User(...)`, происходит следующее: 1. Создаётся новый пустой объект, и он присваивается `this`. -2. Выполняется код функции. Обычно он модифицирует `this`, добавляет туда новые свойства. +2. Выполняется тело функции. Обычно оно модифицирует `this`, добавляя туда новые свойства. 3. Возвращается значение `this`. -Другими словами, вызов `new User(...)` делает примерно вот что: +Другими словами, `new User(...)` делает что-то вроде: ```js function User(name) { @@ -51,45 +51,48 @@ function User(name) { } ``` -То есть, результат вызова `new User("Вася")` - это тот же объект, что и: +Таким образом, `let user = new User("Jack")` возвращает тот же результат, что и: ```js let user = { - name: "Вася", + name: "Jack", isAdmin: false }; ``` -Теперь, когда нам необходимо будет создать других пользователей, мы можем использовать `new User("Маша")`, -`new User("Даша")` и т.д. Данная конструкция гораздо удобнее и читабельнее, чем каждый раз создавать литерал объекта. Это и является основной целью конструкторов - удобное повторное создание однотипных объектов. +Теперь, если нам будет необходимо создать других пользователей, мы можем просто вызвать `new User("Ann")`, `new User("Alice")` и так далее. Данная конструкция гораздо удобнее и читабельнее, чем многократное создание литерала объекта. -Ещё раз заметим: технически любая функция может быть использована как конструктор. То есть, каждая функция может быть вызвана при помощи оператора `new`, и выполнится алгоритм, указанный выше в примере. Заглавная буква в названии функции является всеобщим соглашением по именованию, она как бы подсказывает разработчику, что данная функция является функцией-конструктором, и её нужно вызывать через `new`. +Это и является основной целью конструкторов - реализовать код для многократного создания однотипных объектов. + +Давайте ещё раз отметим - технически любая функция (кроме стрелочных функций, поскольку у них нет `this`) может использоваться в качестве конструктора. Его можно запустить с помощью `new`, и он выполнит выше указанный алгоритм. Подобные функции должны начинаться с заглавной буквы - это общепринятое соглашение, чтобы было ясно, что функция должна вызываться с помощью "new". ````smart header="new function() { ... }" -Если в нашем коде большое количество строк, создающих один сложный объект, мы можем обернуть их в функцию-конструктор следующим образом: +Если в нашем коде присутствует большое количество строк, создающих один сложный объект, то мы можем обернуть их в функцию-конструктор, которая будет немедленно вызвана, вот так: ```js -let user = new function() { - this.name = "Вася"; +// создаём функцию и сразу же вызываем её с помощью new +let user = new function() { + this.name = "John"; this.isAdmin = false; // ...другой код для создания пользователя - // возможна любая сложная логика и выражения - // локальные переменные и т. д. + // возможна любая сложная логика и инструкции + // локальные переменные и так далее }; ``` -Такой конструктор не может быть вызван дважды, так как он нигде не сохраняется, просто создаётся и тут же вызывается. Таким образом, такой метод создания позволяет инкапсулировать код, который создаёт отдельный объект, но без возможности его повторного использования. + +Такой конструктор не может быть вызван снова, так как он нигде не сохраняется, просто создаётся и тут же вызывается. Таким образом, этот трюк направлен на инкапсуляцию кода, который создаёт отдельный объект, без возможности повторного использования в будущем. ```` ## Проверка на вызов в режиме конструктора: new.target ```smart header="Продвинутая возможность" -Данный метод используется очень редко. Вы можете пропустить эту секцию, если не хотите углубляться в детали языка. +Синтаксис из этого раздела используется крайне редко. Вы можете пропустить его, если не хотите углубляться в детали языка. ``` Используя специальное свойство `new.target` внутри функции, мы можем проверить, вызвана ли функция при помощи оператора `new` или без него. -В случае, если функция вызвана при помощи `new`, то в `new.target` будет сама функция, в противном случае `undefined`. +В случае обычного вызова функции `new.target` будет `undefined`. Если же она была вызвана при помощи `new`, `new.target` будет равен самой функции. ```js run function User() { @@ -107,43 +110,44 @@ new User(); // function User { ... } */!* ``` -Это можно использовать, чтобы отличить обычный вызов от вызова "в режиме конструктора". В частности, вот так можно сделать, чтобы функцию можно было вызывать как с, так и без `new`: +Это можно использовать внутри функции, чтобы узнать, была ли она вызвана при помощи `new`, "в режиме конструктора", или без него, "в обычном режиме". + +Также мы можем сделать, чтобы вызовы с `new` и без него делали одно и то же: ```js run function User(name) { - if (!new.target) { // в случае, если вы вызвали без оператора new - return new User(name); // ...добавим оператор new за вас + if (!new.target) { // в случае, если вы вызвали меня без оператора new + return new User(name); // ...я добавлю new за вас } this.name = name; } -let vasya = User("Вася"); // переадресовывает вызовы на new User -alert(vasya.name); // Вася +let john = User("John"); // переадресовывает вызов на new User +alert(john.name); // John ``` -Такой подход иногда используется в библиотеках для создания более гибкого синтаксиса, который позволяет разработчикам вызывать функции при помощи оператора `new` или без него. +Такой подход иногда используется в библиотеках, чтобы сделать синтаксис более гибким. Чтобы люди могли вызывать функцию с `new` и без него, и она все ещё могла работать. -Впрочем, это не очень хорошая практика, так как отсутствие `new` может ввести разработчика в заблуждение. С оператором `new` мы точно знаем, что в итоге будет создан новый объект. +Впрочем, вероятно, это не очень хорошая практика использовать этот трюк везде, так как отсутствие `new` может ввести разработчика в заблуждение. С `new` мы точно знаем, что создаётся новый объект. +## Возврат значения из конструктора, return -## Возврат значения из конструктора return - -Обычно конструкторы ничего не возвращают явно. Их задача - записать все необходимое в `this`, который в итоге станет результатом. +Обычно конструкторы не имеют оператора `return`. Их задача - записать все необходимое в `this`, и это автоматически становится результатом. Но если `return` всё же есть, то применяется простое правило: -- При вызове `return` с объектом, будет возвращён объект, а не `this`. -- При вызове `return` с примитивным значением, примитивное значение будет отброшено. +- При вызове `return` с объектом, вместо `this` вернётся объект. +- При вызове `return` с примитивным значением, оно проигнорируется. -Другими словами, `return` с объектом возвращает объект, в любом другом случае конструктор вернёт `this`. +Другими словами, `return` с объектом возвращает этот объект, во всех остальных случаях возвращается `this`. -В примере ниже `return` возвращает объект вместо `this`: +К примеру, здесь `return` замещает `this`, возвращая объект: ```js run function BigUser() { - this.name = "Вася"; + this.name = "John"; return { name: "Godzilla" }; // <-- возвращает этот объект } @@ -151,24 +155,23 @@ function BigUser() { alert( new BigUser().name ); // Godzilla, получили этот объект ``` -А вот пример с пустым `return` (или мы могли бы поставить примитив после `return`, неважно) +А вот пример с пустым `return` (или мы могли бы поставить примитив после `return`, неважно): ```js run function SmallUser() { - this.name = "Вася"; + this.name = "John"; return; // <-- возвращает this } -alert( new SmallUser().name ); // Вася +alert( new SmallUser().name ); // John ``` -Обычно у конструкторов отсутствует `return`. В данном блоке мы упомянули особое поведение с возвращаемыми объектами, чтобы не оставлять пробелов в изучении языка. - +Обычно у конструкторов отсутствует `return`. Здесь мы упомянули особое поведение с возвращаемыми объектами в основном для полноты картины. -````smart header="Отсутствие скобок" -Кстати, мы можем не ставить скобки после `new`, если вызов конструктора идёт без аргументов. +````smart header="Пропуск скобок" +Кстати, мы можем не ставить круглые скобки после `new`: ```js let user = new User; // <-- без скобок @@ -176,16 +179,16 @@ let user = new User; // <-- без скобок let user = new User(); ``` -Пропуск скобок считается плохой практикой, но синтаксис языка такое позволяет. +Пропуск скобок считается плохой практикой, но просто чтобы вы знали, такой синтаксис разрешён спецификацией. ```` ## Создание методов в конструкторе -Использование конструкторов для создания объектов даёт большую гибкость. Можно передавать конструктору параметры, определяющие, как создавать объект, и что в него записывать. +Использование конструкторов для создания объектов даёт большую гибкость. Функции-конструкторы могут иметь параметры, определяющие, как создавать объект и что в него записывать. -В `this` мы можем добавлять не только свойства, но и методы. +Конечно, мы можем добавить к `this` не только свойства, но и методы. -Например, в примере ниже, `new User(name)` создаёт объект с данным именем `name` и методом `sayHi`: +Например, `new User(name)` ниже создаёт объект с заданным `name` и методом `sayHi`: ```js run function User(name) { @@ -197,31 +200,32 @@ function User(name) { } *!* -let vasya = new User("Вася"); +let john = new User("John"); -vasya.sayHi(); // Меня зовут: Вася +john.sayHi(); // Меня зовут: John */!* /* -vasya = { - name: "Вася", +john = { + name: "John", sayHi: function() { ... } } */ ``` -Для создания сложных объектов есть и более "продвинутый" синтаксис - [классы](info:classes), которые мы разберём позже. +Для создания сложных объектов есть и более продвинутый синтаксис - [классы](info:classes), который мы рассмотрим позже. ## Итого -- Функции-конструкторы или просто конструкторы являются обычными функциями, именовать которые следует с заглавной буквы. -- Конструкторы следует вызывать при помощи оператора `new`. Такой вызов создаёт пустой `this` в начале выполнения и возвращает заполненный в конце. +- Функции-конструкторы или просто конструкторы, являются обычными функциями, но существует общепринятое соглашение именовать их с заглавной буквы. +- Функции-конструкторы следует вызывать только с помощью `new`. Такой вызов подразумевает создание пустого `this` в начале и возврат заполненного в конце. Мы можем использовать конструкторы для создания множества похожих объектов. -JavaScript предоставляет функции-конструкторы для множества встроенных объектов языка: например, `Date`, `Set` и других, которые нам ещё предстоит изучить. +JavaScript предоставляет функции-конструкторы для множества встроенных объектов языка: таких как `Date`, `Set`, и других, которые нам ещё предстоит изучить. + +```smart header="Мы ещё вернёмся к объектам!" +В этой главе мы рассмотрели только основы объектов и конструкторов. Данная информация необходима нам для дальнейшего изучения типов данных и функций в последующих главах. -```smart header="Объекты, мы к ним ещё вернёмся!" -В этой главе мы рассмотрели базовые принципы объектов и конструкторов. Данная информация необходима нам для дальнейшего изучения типов данных и функций. Как только мы с ними разберёмся, мы вернёмся к объектам для более детального изучения в -главах и . +Как только мы с ними разберёмся, мы вернёмся к объектам для более детального изучения в главах и . ``` diff --git a/1-js/04-object-basics/07-optional-chaining/article.md b/1-js/04-object-basics/07-optional-chaining/article.md new file mode 100644 index 0000000000..53005e7356 --- /dev/null +++ b/1-js/04-object-basics/07-optional-chaining/article.md @@ -0,0 +1,235 @@ + +# Опциональная цепочка '?.' + +[recent browser="new"] + +Опциональная цепочка `?.` — это безопасный способ доступа к свойствам вложенных объектов, даже если какое-либо из промежуточных свойств не существует. + +## Проблема "несуществующего свойства" + +Если вы только начали читать учебник и изучать JavaScript, то, возможно, проблема вас ещё не коснулась, но она довольно распространена. + +В качестве примера предположим, что у нас есть объекты `user`, которые содержат информацию о наших пользователях. + +У большинства наших пользователей есть адреса в свойстве `user.address` с улицей `user.address.street`, но некоторые из них их не указали. + +В таком случае, когда мы попытаемся получить `user.address.street`, а пользователь окажется без адреса, мы получим ошибку: + +```js run +let user = {}; // пользователь без свойства "address" + +alert(user.address.street); // Ошибка! +``` + +Это ожидаемый результат. JavaScript работает следующим образом. Поскольку `user.address` имеет значение `undefined`, попытка получить `user.address.street` завершается ошибкой. + +Во многих практических случаях мы бы предпочли получить здесь `undefined` вместо ошибки (что означало бы "улицы нет"). + +...Или ещё один пример. В веб-разработке мы можем получить объект, соответствующий элементу веб-страницы, с помощью специального вызова метода, такого как `document.querySelector('.elem')`, и он возвращает `null`, когда такого элемента нет. + +```js run +// document.querySelector('.elem') равен null, если элемента нет +let html = document.querySelector('.elem').innerHTML; // ошибка, если он равен null +``` + +Ещё раз, если элемент не существует, мы получим сообщение об ошибке доступа к свойству `.innerHTML` у `null`. И в некоторых случаях, когда отсутствие элемента является нормальным, мы хотели бы избежать ошибки и просто принять `html = null` в качестве результата. + +Как мы можем это сделать? + +Очевидным решением было бы проверить значение с помощью `if` или условного оператора `?`, прежде чем обращаться к его свойству, вот так: + +```js +let user = {}; + +alert(user.address ? user.address.street : undefined); +``` + +Это работает, тут нет ошибки... Но это довольно неэлегантно. Как вы можете видеть, `"user.address"` появляется в коде дважды. + +Вот как то же самое выглядело бы для `document.querySelector`: + +```js run +let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null; +``` + +Как видно, поиск элемента `document.querySelector('.elem')` здесь вызывается дважды, что не очень хорошо. + +Для более глубоко вложенных свойств это ещё менее красиво, поскольку потребуется больше повторений. + +К примеру, давайте аналогично вычислим `user.address.street.name`. + +Нам нужно проверить как `user.address`, так и `user.address.street`: + +```js +let user = {}; // у пользователя нет адреса + +alert(user.address ? user.address.street ? user.address.street.name : null : null); +``` + +Это просто ужасно, у кого-то могут даже возникнуть проблемы с пониманием такого кода. + +Есть немного лучший способ написать это, используя оператор `&&`: + +```js run +let user = {}; // пользователь без адреса + +alert( user.address && user.address.street && user.address.street.name ); // undefined (без ошибки) +``` + +Проход при помощи логического оператора И `&&` через весь путь к свойству гарантирует, что все компоненты существуют (если нет, вычисление прекращается), но также не является идеальным. + +Как вы можете видеть, имена свойств по-прежнему дублируются в коде. Например, в приведённом выше коде `user.address` появляется три раза. + +Вот почему в язык была добавлена опциональная цепочка `?.`. Чтобы решить эту проблему - раз и навсегда! + +## Опциональная цепочка + +Опциональная цепочка `?.` останавливает вычисление и возвращает `undefined`, если значение перед `?.` равно `undefined` или `null`. + +**Далее в этой статье, для краткости, мы будем говорить, что что-то "существует", если оно не является `null` и не `undefined`.** + +Другими словами, `value?.prop`: +- работает как `value.prop`, если значение `value` существует, +- в противном случае (когда `value` равно `undefined/null`) он возвращает `undefined`. + +Вот безопасный способ получить доступ к `user.address.street`, используя `?.`: + +```js run +let user = {}; // пользователь без адреса + +alert( user?.address?.street ); // undefined (без ошибки) +``` + +Код лаконичный и понятный, в нем вообще нет дублирования. + +А вот пример с `document.querySelector`: + +```js run +let html = document.querySelector('.elem')?.innerHTML; // будет undefined, если элемента нет +``` + +Считывание адреса с помощью `user?.address` работает, даже если объект `user` не существует: + +```js run +let user = null; + +alert( user?.address ); // undefined +alert( user?.address.street ); // undefined +``` + +Обратите внимание: синтаксис `?.` делает необязательным значение перед ним, но не какое-либо последующее. + +Так например, в записи `user?.address.street.name` `?.` позволяет `user` безопасно быть `null/undefined` (и в этом случае возвращает `undefined`), но это так только для `user`. Доступ к последующим свойствам осуществляется обычным способом. Если мы хотим, чтобы некоторые из них были необязательными, тогда нам нужно будет заменить больше `.` на `?.`. + +```warn header="Не злоупотребляйте опциональной цепочкой" +Нам следует использовать `?.` только там, где нормально, что чего-то не существует. + +К примеру, если, в соответствии с логикой нашего кода, объект `user` должен существовать, но `address` является необязательным, то нам следует писать `user.address?.street`, но не `user?.address?.street`. + +В этом случае, если вдруг `user` окажется `undefined`, мы увидим программную ошибку по этому поводу и исправим её. В противном случае, если слишком часто использовать `?.`, ошибки могут замалчиваться там, где это неуместно, и их будет сложнее отлаживать. +``` + +````warn header="Переменная перед `?.` должна быть объявлена" +Если переменной `user` вообще нет, то `user?.anything` приведёт к ошибке: + +```js run +// ReferenceError: user is not defined +user?.address; +``` +Переменная должна быть объявлена (к примеру, как `let/const/var user` или как параметр функции). Опциональная цепочка работает только с объявленными переменными. +```` + +## Сокращённое вычисление + +Как было сказано ранее, `?.` немедленно останавливает вычисление, если левая часть не существует. + +Так что если после `?.` есть какие-то вызовы функций или операции, то они не произойдут. + +Например: + +```js run +let user = null; +let x = 0; + +user?.sayHi(x++); // нет "user", поэтому выполнение не достигает вызова sayHi и x++ + +alert(x); // 0, значение не увеличилось +``` + +## Другие варианты применения: ?.(), ?.[] + +Опциональная цепочка `?.` — это не оператор, а специальная синтаксическая конструкция, которая также работает с функциями и квадратными скобками. + +Например, `?.()` используется для вызова функции, которая может не существовать. + +В приведённом ниже коде у некоторых наших пользователей есть метод `admin`, а у некоторых его нет: + +```js run +let userAdmin = { + admin() { + alert("Я админ"); + } +}; + +let userGuest = {}; + +*!* +userAdmin.admin?.(); // Я админ +*/!* + +*!* +userGuest.admin?.(); // ничего не произойдет (такого метода нет) +*/!* +``` + +Здесь в обеих строках мы сначала используем точку (`userAdmin.admin`), чтобы получить свойство `admin`, потому что мы предполагаем, что объект `userAdmin` существует, так что читать из него безопасно. + +Затем `?.()` проверяет левую часть: если функция `admin` существует, то она запускается (это так для `userAdmin`). В противном случае (для `userGuest`) вычисление остановится без ошибок. + +Синтаксис `?.[]` также работает, если мы хотим использовать скобки `[]` для доступа к свойствам вместо точки `.`. Как и в предыдущих случаях, он позволяет безопасно считывать свойство из объекта, который может не существовать. + +```js run +let key = "firstName"; + +let user1 = { + firstName: "John" +}; + +let user2 = null; + +alert( user1?.[key] ); // John +alert( user2?.[key] ); // undefined +``` + +Также мы можем использовать `?.` с `delete`: + +```js run +delete user?.name; // удаляет user.name если пользователь существует +``` + +````warn header="Мы можем использовать `?.` для безопасного чтения и удаления, но не для записи" +Опциональная цепочка `?.` не имеет смысла в левой части присваивания. + +Например: +```js run +let user = null; + +user?.name = "John"; // Ошибка, не работает +// то же самое что написать undefined = "John" +``` + +```` + +## Итого + +Синтаксис опциональной цепочки `?.` имеет три формы: + +1. `obj?.prop` -- возвращает `obj.prop` если `obj` существует, в противном случае `undefined`. +2. `obj?.[prop]` -- возвращает `obj[prop]` если `obj` существует, в противном случае `undefined`. +3. `obj.method?.()` -- вызывает `obj.method()`, если `obj.method` существует, в противном случае возвращает `undefined`. + +Как мы видим, все они просты и понятны в использовании. `?.` проверяет левую часть на `null/undefined` и позволяет продолжить вычисление, если это не так. + +Цепочка `?.` позволяет безопасно получать доступ к вложенным свойствам. + +Тем не менее, мы должны использовать `?.` осторожно, только там, где по логике кода допустимо, что левая часть не существует. Чтобы он не скрывал от нас ошибки программирования, если они возникнут. diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/08-symbol/article.md similarity index 93% rename from 1-js/04-object-basics/03-symbol/article.md rename to 1-js/04-object-basics/08-symbol/article.md index 0a24697afb..6edf9869ce 100644 --- a/1-js/04-object-basics/03-symbol/article.md +++ b/1-js/04-object-basics/08-symbol/article.md @@ -16,7 +16,7 @@ let id = Symbol(); ``` -При создании символу можно дать описание (также называемое имя), в основном использующееся для отладки кода: +При создании, символу можно дать описание (также называемое имя), в основном использующееся для отладки кода: ```js run // Создаём символ id с описанием (именем) "id" @@ -53,6 +53,7 @@ alert(id); // TypeError: Cannot convert a Symbol value to a string Это -- языковая "защита" от путаницы, ведь строки и символы -- принципиально разные типы данных и не должны неконтролируемо преобразовываться друг в друга. Если же мы действительно хотим вывести символ с помощью `alert`, то необходимо явно преобразовать его с помощью метода `.toString()`, вот так: + ```js run let id = Symbol("id"); *!* @@ -61,6 +62,7 @@ alert(id.toString()); // Symbol(id), теперь работает ``` Или мы можем обратиться к свойству `symbol.description`, чтобы вывести только описание: + ```js run let id = Symbol("id"); *!* @@ -179,22 +181,6 @@ alert( clone[id] ); // 123 Здесь нет никакого парадокса или противоречия. Так и задумано. Идея заключается в том, что, когда мы клонируем или объединяем объекты, мы обычно хотим скопировать *все* свойства (включая такие свойства с ключами-символами, как, например, `id` в примере выше). -````smart header="Ключи других типов принудительно преобразуются к строке" -Мы можем использовать только строки и символы в качестве ключей свойств. Все другие типы данных будут автоматически преобразованы к строке. - -Например, число `0`, будучи использованным как ключ свойства, превратится в строку `"0"`: - -```js run -let obj = { - 0: "Тест" // то же самое что и "0": "Тест" -}; - -// обе функции alert выведут одно и то же свойство (число 0 преобразовывается в строку "0") -alert( obj["0"] ); // Тест -alert( obj[0] ); // Тест (то же свойство) -``` -```` - ## Глобальные символы Итак, как мы видели, обычно все символы уникальны, даже если их имена совпадают. Но иногда мы наоборот хотим, чтобы символы с одинаковыми именами были одной сущностью. Например, разные части нашего приложения хотят получить доступ к символу `"id"`, подразумевая именно одно и то же свойство. @@ -211,7 +197,7 @@ alert( obj[0] ); // Тест (то же свойство) // читаем символ из глобального реестра и записываем его в переменную let id = Symbol.for("id"); // если символа не существует, он будет создан -// читаем его снова в другую переменную (возможно, из другого места кода) +// читаем его снова и записываем в другую переменную (возможно, из другого места кода) let idAgain = Symbol.for("id"); // проверяем -- это один и тот же символ @@ -285,6 +271,7 @@ alert( localSymbol.description ); // name Символы имеют два основных варианта использования: 1. "Скрытые" свойства объектов. + Если мы хотим добавить свойство в объект, который "принадлежит" другому скрипту или библиотеке, мы можем создать символ и использовать его в качестве ключа. Символьное свойство не появится в `for..in`, так что оно не будет нечаянно обработано вместе с другими. Также оно не будет модифицировано прямым обращением, так как другой скрипт не знает о нашем символе. Таким образом, свойство будет защищено от случайной перезаписи или использования. Так что, используя символьные свойства, мы можем спрятать что-то нужное нам, но что другие видеть не должны. diff --git a/1-js/04-object-basics/09-object-toprimitive/article.md b/1-js/04-object-basics/09-object-toprimitive/article.md new file mode 100644 index 0000000000..ed6a70e668 --- /dev/null +++ b/1-js/04-object-basics/09-object-toprimitive/article.md @@ -0,0 +1,279 @@ + +# Преобразование объектов в примитивы + +Что произойдёт, если сложить два объекта `obj1 + obj2`, вычесть один из другого `obj1 - obj2` или вывести их на экран, воспользовавшись `alert(obj)`? + +JavaScript совершенно не позволяет настраивать, как операторы работают с объектами. В отличие от некоторых других языков программирования, таких как Ruby или C++, мы не можем реализовать специальный объектный метод для обработки сложения (или других операторов). + +В случае таких операций, объекты автоматически преобразуются в примитивы, затем выполняется сама операция над этими примитивами, и на выходе мы получим примитивное значение. + +Это важное ограничение: результатом `obj1 + obj2` (или другой математической операции) не может быть другой объект! + +К примеру, мы не можем создавать объекты, представляющие векторы или матрицы (или достижения или может ещё что-то), складывать их и ожидать в качестве результата "суммированный" объект. Такие архитектурные ходы автоматически оказываются "за бортом". + +Итак, поскольку мы технически здесь мало что можем сделать, в реальных проектах нет математики с объектами. Если она всё же происходит, то за редким исключением, это из-за ошибок в коде. + +В этой главе мы рассмотрим, как объект преобразуется в примитив и как это можно настроить. + +У нас есть две цели: + +1. Это позволит нам понять, что происходит в случае ошибок в коде, когда такая операция произошла случайно. +2. Есть исключения, когда такие операции возможны и вполне уместны. Например, вычитание или сравнение дат (`Date` объекты). Мы встретимся с ними позже. + +## Правила преобразования + +В главе мы рассмотрели правила для числовых, строковых и логических преобразований примитивов. Но мы оставили пробел для объектов. Теперь, когда мы уже знаем о методах и символах, пришло время заполнить этот пробел. + +1. Не существует преобразования к логическому значению. В логическом контексте все объекты являются `true`, всё просто. Существует лишь их числовое и строковое преобразование. +2. Числовое преобразование происходит, когда мы вычитаем объекты или применяем математические функции. Например, объекты `Date` (которые будут рассмотрены в главе ) могут быть вычтены, и результатом `date1 - date2` будет разница во времени между двумя датами. +3. Что касается преобразований к строке -- оно обычно происходит, когда мы выводим на экран объект при помощи `alert(obj)` и в подобных контекстах. + +Мы можем реализовать свои преобразования к строкам и числам, используя специальные объектные методы. + +Теперь давайте углубимся в детали. Это единственный путь для того, чтобы разобраться в нюансах этой темы. + +## Хинты + +Как JavaScript решает, какое преобразование применить? + +Существует три варианта преобразования типов, которые происходят в различных ситуациях. Они называются "хинтами", как описано в [спецификации](https://tc39.github.io/ecma262/#sec-toprimitive): + +`"string"` +: Для преобразования объекта к строке, когда мы выполняем операцию над объектом, которая ожидает строку, например `alert`: + + ```js + // вывод + alert(obj); + + // используем объект в качестве ключа + anotherObj[obj] = 123; + ``` + +`"number"` +: Для преобразования объекта к числу, в случае математических операций: + + ```js + // явное преобразование + let num = Number(obj); + + // математические (не считая бинарного плюса) + let n = +obj; // унарный плюс + let delta = date1 - date2; + + // сравнения больше/меньше + let greater = user1 > user2; + ``` + + Большинство встроенных математических функций также включают в себя такое преобразование. + +`"default"` +: Происходит редко, когда оператор "не уверен", какой тип ожидать. + + Например, бинарный плюс `+` может работать как со строками (объединяя их в одну), так и с числами (складывая их). Поэтому, если бинарный плюс получает объект в качестве аргумента, он использует хинт `"default"` для его преобразования. + + Также, если объект сравнивается с помощью `==` со строкой, числом или символом, тоже неясно, какое преобразование следует выполнить, поэтому используется хинт `"default"`. + + ```js + // бинарный плюс использует хинт "default" + let total = obj1 + obj2; + + // obj == number использует хинт "default" + if (user == 1) { ... }; + ``` + + Операторы сравнения больше/меньше, такие как `<` `>`, также могут работать как со строками, так и с числами. Тем не менее, по историческим причинам, они используют хинт `"number"`, а не `"default"`. + +Впрочем на практике, всё немного проще. + +Все встроенные объекты, за исключением одного (объект `Date`, который мы рассмотрим позже), реализуют `"default"` преобразование тем же способом, что и `"number"`. И нам следует поступать так же. + +**Чтобы выполнить преобразование, JavaScript пытается найти и вызвать три следующих метода объекта:** + +1. Вызвать `obj[Symbol.toPrimitive](hint)` - метод с символьным ключом `Symbol.toPrimitive` (системный символ), если такой метод существует, +2. Иначе, если хинт равен `"string"` + - попробовать вызвать `obj.toString()` или `obj.valueOf()`, смотря какой из них существует. +3. Иначе, если хинт равен `"number"` или `"default"` + - попробовать вызвать `obj.valueOf()` или `obj.toString()`, смотря какой из них существует. + +## Symbol.toPrimitive + +Давайте начнём с первого метода. Есть встроенный символ с именем `Symbol.toPrimitive`, который следует использовать для обозначения метода преобразования, вот так: + +```js +obj[Symbol.toPrimitive] = function(hint) { + // вот код для преобразования этого объекта в примитив + // он должен вернуть примитивное значение + // hint = чему-то из "string", "number", "default" +}; +``` + +Если метод `Symbol.toPrimitive` существует, он используется для всех хинтов, и больше никаких методов не требуется. + +Например, здесь объект `user` реализует его: + +```js run +let user = { + name: "John", + money: 1000, + + [Symbol.toPrimitive](hint) { + alert(`hint: ${hint}`); + return hint == "string" ? `{name: "${this.name}"}` : this.money; + } +}; + +// демонстрация результатов преобразований: +alert(user); // hint: string -> {name: "John"} +alert(+user); // hint: number -> 1000 +alert(user + 500); // hint: default -> 1500 +``` + +Как мы можем видеть из кода, `user` становится либо строкой со своим описанием, либо суммой денег в зависимости от преобразования. Единый метод `user[Symbol.toPrimitive]` обрабатывает все случаи преобразования. + + +## toString/valueOf + +Если нет `Symbol.toPrimitive`, тогда JavaScript пытается найти методы `toString` и `valueOf`: + +- Для хинта `"string"`: вызвать метод `toString`, а если он не существует или возвращает объект вместо примитивного значения, то `valueOf` (таким образом, `toString` имеет приоритет при строковом преобразовании). +- Для других хинтов: вызвать метод `valueOf`, а если он не существует или возвращает объект вместо примитивного значения, то `toString` (таким образом, `valueOf` имеет приоритет для математических операций). + +Методы `toString` и `valueOf` берут своё начало с древних времён. Это не символы (символов тогда ещё не было), а скорее просто "обычные" методы со строковыми именами. Они предоставляют альтернативный "старомодный" способ реализации преобразования. + +Эти методы должны возвращать примитивное значение. Если `toString` или `valueOf` возвращает объект, то он игнорируется (так же, как если бы метода не было). + +По умолчанию обычный объект имеет следующие методы `toString` и `valueOf`: + +- Метод `toString` возвращает строку `"[object Object]"`. +- Метод `valueOf` возвращает сам объект. + +Взгляните на пример: + +```js run +let user = {name: "John"}; + +alert(user); // [object Object] +alert(user.valueOf() === user); // true +``` + +Таким образом, если мы попытаемся использовать объект в качестве строки, как например в `alert` или вроде того, то по умолчанию мы увидим `[object Object]`. + +Значение по умолчанию `valueOf` упоминается здесь только для полноты картины, чтобы избежать какой-либо путаницы. Как вы можете видеть, он возвращает сам объект и поэтому игнорируется. Не спрашивайте меня почему, это по историческим причинам. Так что мы можем предположить, что его не существует. + +Давайте применим эти методы для настройки преобразования. + +Для примера, используем их в реализации всё того же объекта `user`. Но уже используя комбинацию `toString` и `valueOf` вместо `Symbol.toPrimitive`: + +```js run +let user = { + name: "John", + money: 1000, + + // для хинта равного "string" + toString() { + return `{name: "${this.name}"}`; + }, + + // для хинта равного "number" или "default" + valueOf() { + return this.money; + } + +}; + +alert(user); // toString -> {name: "John"} +alert(+user); // valueOf -> 1000 +alert(user + 500); // valueOf -> 1500 +``` + +Как видим, получилось то же поведение, что и в предыдущем примере с `Symbol.toPrimitive`. + +Довольно часто нам нужно единое "универсальное" место для обработки всех примитивных преобразований. В этом случае мы можем реализовать только `toString`: + +```js run +let user = { + name: "John", + + toString() { + return this.name; + } +}; + +alert(user); // toString -> John +alert(user + 500); // toString -> John500 +``` + +В отсутствие `Symbol.toPrimitive` и `valueOf`, `toString` обработает все примитивные преобразования. + +### Преобразование может вернуть любой примитивный тип + +Важная вещь, которую следует знать обо всех методах преобразования примитивов, заключается в том, что они не обязательно возвращают подсказанный хинтом примитив. + +Нет никакого контроля над тем, вернёт ли `toString` именно строку, или чтобы метод `Symbol.toPrimitive` возвращал именно число для хинта `"number"`. + +Единственное обязательное условие: эти методы должны возвращать примитив, а не объект. + +```smart header="Историческая справка" +По историческим причинам, если `toString` или `valueOf` вернёт объект, то ошибки не будет, но такое значение будет проигнорировано (как если бы метода вообще не существовало). Это всё потому, что в древние времена в JavaScript не было хорошей концепции "ошибки". + +А вот `Symbol.toPrimitive` уже "четче", этот метод *обязан* возвращать примитив, иначе будет ошибка. +``` + +## Дальнейшие преобразования + +Как мы уже знаем, многие операторы и функции выполняют преобразования типов, например, умножение `*` преобразует операнды в числа. + +Если мы передаём объект в качестве аргумента, то в вычислениях будут две стадии: +1. Объект преобразуется в примитив (с использованием правил, описанных выше). +2. Если необходимо для дальнейших вычислений, этот примитив преобразуется дальше. + +Например: + +```js run +let obj = { + // toString обрабатывает все преобразования в случае отсутствия других методов + toString() { + return "2"; + } +}; + +alert(obj * 2); // 4, объект был преобразован к примитиву "2", затем умножение сделало его числом +``` + +1. Умножение `obj * 2` сначала преобразует объект в примитив (это строка `"2"`). +2. Затем `"2" * 2` становится `2 * 2` (строка преобразуется в число). + +А вот, к примеру, бинарный плюс в подобной ситуации соединил бы строки, так как он совсем не брезгует строк: + +```js run +let obj = { + toString() { + return "2"; + } +}; + +alert(obj + 2); // "22" ("2" + 2), преобразование к примитиву вернуло строку => конкатенация +``` + +## Итого + +Преобразование объекта в примитив вызывается автоматически многими встроенными функциями и операторами, которые ожидают примитив в качестве значения. + +Существует всего 3 типа (хинта) для этого: +- `"string"` (для `alert` и других операций, которым нужна строка) +- `"number"` (для математических операций) +- `"default"` (для некоторых других операторов, обычно объекты реализуют его как `"number"`) + +Спецификация явно описывает для каждого оператора, какой ему следует использовать хинт. + +Алгоритм преобразования таков: + +1. Сначала вызывается метод `obj[Symbol.toPrimitive](hint)`, если он существует, +2. В случае, если хинт равен `"string"` + - происходит попытка вызвать `obj.toString()` и `obj.valueOf()`, смотря что есть. +3. В случае, если хинт равен `"number"` или `"default"` + - происходит попытка вызвать `obj.valueOf()` и `obj.toString()`, смотря что есть. + +Все эти методы должны возвращать примитив (если определены). + +На практике часто бывает достаточно реализовать только `obj.toString()` в качестве универсального метода для преобразований к строке, который должен возвращать удобочитаемое представление объекта для целей логирования или отладки. diff --git a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md index a2f73fe359..f0892c22c0 100644 --- a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md +++ b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md @@ -9,14 +9,14 @@ str.test = 5; // (*) alert(str.test); ``` -В зависимости от того, используете ли вы cтрогий режим (`use strict`) или нет, результат может быть: +В зависимости от того, используете ли вы строгий режим (`use strict`) или нет, результат может быть: 1. `undefined` (без strict) 2. Ошибка (strict mode) Почему? Давайте посмотрим что происходит в строке кода, отмеченной `(*)`: 1. В момент обращения к свойству `str` создаётся "объект-обёртка". -2. В cтрогом режиме, попытка изменения этого объекта выдаёт ошибку. +2. В строгом режиме, попытка изменения этого объекта выдаёт ошибку. 3. Без строгого режима, операция продолжается, объект получает свойство `test`, но после этого он удаляется, так что на последней линии `str` больше не имеет свойства `test`. **Данный пример наглядно показывает, что примитивы не являются объектами.** diff --git a/1-js/05-data-types/01-primitives-methods/article.md b/1-js/05-data-types/01-primitives-methods/article.md index 97e46258c6..ba94c8f491 100644 --- a/1-js/05-data-types/01-primitives-methods/article.md +++ b/1-js/05-data-types/01-primitives-methods/article.md @@ -1,4 +1,4 @@ -# Методы у примитивов +# Методы примитивов JavaScript позволяет нам работать с примитивными типами данных - строками, числами и т.д., как будто они являются объектами. У них есть и методы. Мы изучим их позже, а сначала разберём, как это всё работает, потому что, конечно, примитивы - не объекты. @@ -7,7 +7,7 @@ JavaScript позволяет нам работать с примитивным Примитив - Это - значение «примитивного» типа. -- Есть 6 примитивных типов: `string`, `number`, `boolean`, `symbol`, `null` и `undefined`. +- Есть 7 примитивных типов: `string`, `number`, `boolean`, `symbol`, `null`, `undefined` и `bigint`. Объект @@ -39,16 +39,16 @@ roma.sayHi(); // Привет, дружище! Вот парадокс, с которым столкнулся создатель JavaScript: -- Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно, если бы мы могли работать с ними через вызовы методов. -- Примитивы должны быть лёгкими и быстрыми. +- Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно, если бы мы могли обращаться к ним при помощи методов. +- Примитивы должны быть лёгкими и быстрыми насколько это возможно. Выбранное решение, хотя выглядит оно немного неуклюже: 1. Примитивы остаются примитивами. Одно значение, как и хотелось. 2. Язык позволяет осуществлять доступ к методам и свойствам строк, чисел, булевых значений и символов. -3. Чтобы это работало, при таком доступе создаётся специальный "объект-обёртка", который предоставляет нужный функционал, а после удаляется. +3. Чтобы это работало, при таком доступе создаётся специальный "объект-обёртка", который предоставляет нужную функциональность, а после удаляется. -Каждый примитив имеет свой собственный "объект-обёртку", которые называются: `String`, `Number`, `Boolean` и `Symbol`. Таким образом, они имеют разный набор методов. +Каждый примитив имеет свой собственный "объект-обёртку", которые называются: `String`, `Number`, `Boolean`, `Symbol` и `BigInt`. Таким образом, они имеют разный набор методов. К примеру, существует метод [str.toUpperCase()](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase), который возвращает строку в верхнем регистре. @@ -73,9 +73,9 @@ alert( str.toUpperCase() ); // ПРИВЕТ Число имеет собственный набор методов. Например, [toFixed(n)](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) округляет число до n знаков после запятой. ```js run -let n = 1.23456; +let num = 1.23456; -alert( n.toFixed(2) ); // 1.23 +alert( num.toFixed(2) ); // 1.23 ``` Более подробно с различными свойствами и методами мы познакомимся в главах и . diff --git a/1-js/05-data-types/02-number/2-why-rounded-down/solution.md b/1-js/05-data-types/02-number/2-why-rounded-down/solution.md index e894e28e6e..cde03da773 100644 --- a/1-js/05-data-types/02-number/2-why-rounded-down/solution.md +++ b/1-js/05-data-types/02-number/2-why-rounded-down/solution.md @@ -28,6 +28,6 @@ alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000 ```js run -alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4 +alert( Math.round(6.35 * 10) / 10 ); // 6.35 -> 63.5 -> 64(rounded) -> 6.4 ``` diff --git a/1-js/05-data-types/02-number/9-random-int-min-max/solution.md b/1-js/05-data-types/02-number/9-random-int-min-max/solution.md index d0407ee2d5..da814a2789 100644 --- a/1-js/05-data-types/02-number/9-random-int-min-max/solution.md +++ b/1-js/05-data-types/02-number/9-random-int-min-max/solution.md @@ -23,25 +23,11 @@ alert( randomInteger(1, 3) ); число от 2.5 ... до 2.9999999999 округлится до 3 ``` -Теперь становится понятно, что `1` получает в 2 раза меньше значений, чем `2`. Тоже самое с `3`. +Теперь становится понятно, что `1` получает в 2 раза меньше значений, чем `2`. То же самое с `3`. # Правильное решение задачи -Есть много правильных решений этой задачи. Одно из них - правильно указать границы интервала. Чтобы выровнять интервалы, мы можем генерировать числа от `0.5 до 3.5`, это позволит добавить *необходимые вероятности* к `min` и `max`: - -```js run -*!* -function randomInteger(min, max) { - // получить случайное число от (min-0.5) до (max+0.5) - let rand = min - 0.5 + Math.random() * (max - min + 1); - return Math.round(rand); -} -*/!* - -alert( randomInteger(1, 3) ); -``` - -Другое правильное решение - это использовать `Math.floor` для получения случайного числа от `min` до `max+1`: +Есть много правильных решений этой задачи. Одно из них - использовать `Math.floor` для получения случайного числа от `min` до `max+1`: ```js run *!* diff --git a/1-js/05-data-types/02-number/article.md b/1-js/05-data-types/02-number/article.md index 8afd771a24..28bf6154db 100644 --- a/1-js/05-data-types/02-number/article.md +++ b/1-js/05-data-types/02-number/article.md @@ -1,8 +1,10 @@ # Числа -Все числа в JavaScript хранятся в 64-битном формате [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), который также называют "числа с плавающей точкой двойной точности" (double precision floating point numbers). +В современном JavaScript существует два типа чисел: +1. Обычные числа в JavaScript хранятся в 64-битном формате [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754-1985), который также называют "числа с плавающей точкой двойной точности" (double precision floating point numbers). Это числа, которые мы будем использовать чаще всего. Мы поговорим о них в этой главе. +2. `BigInt` числа дают возможность работать с целыми числами произвольной длины. Они нужны достаточно редко и используются в случаях, когда необходимо работать со значениями более чем (253-1) или менее чем -(253-1). Так как `BigInt` числа нужны достаточно редко, мы рассмотрим их в отдельной главе . -Давайте глубже изучим, как работать с числами в JavaScript. +В данной главе мы рассмотрим только первый тип чисел: числа типа `number`. Давайте глубже изучим, как с ними работать в JavaScript. ## Способы записи числа @@ -12,46 +14,53 @@ let billion = 1000000000; ``` -Но в реальной жизни мы обычно опускаем запись множества нулей, так как можно легко ошибиться. Укороченная запись может выглядеть как `"1млрд"` или `"7.3млрд"` для 7 миллиардов 300 миллионов. Такой принцип работает для всех больших чисел. +Мы также можем использовать символ нижнего подчёркивания `_` в качестве разделителя: -В JavaScript можно использовать букву `"e"`, чтобы укоротить запись числа. Она добавляется к числу и заменяет указанное количество нулей: +```js +let billion = 1_000_000_000 +``` + +Символ нижнего подчёркивания `_` -- это «[синтаксический сахар](https://ru.wikipedia.org/wiki/Синтаксический_сахар)», он делает число более читабельным. Движок JavaScript попросту игнорирует `_` между цифрами, поэтому в примере выше получается точно такой же миллиард, как и в первом случае. + +Однако в реальной жизни мы в основном стараемся не писать длинные последовательности нулей, так как можно легко ошибиться. Укороченная запись может выглядеть как `"1млрд"` или `"7.3млрд"` для 7 миллиардов 300 миллионов. Такой принцип работает для всех больших чисел. + +В JavaScript, чтобы укоротить запись числа, мы можем добавить к нему букву `"e"` и указать необходимое количество нулей: ```js run let billion = 1e9; // 1 миллиард, буквально: 1 и 9 нулей -alert( 7.3e9 ); // 7.3 миллиардов (7,300,000,000) +alert( 7.3e9 ); // 7.3 миллиарда (7,300,000,000) ``` -Другими словами, `"e"` производит операцию умножения числа на 1 с указанным количеством нулей. +Другими словами, `"e"` умножает число на `1` с указанным количеством нулей. ```js -1e3 = 1 * 1000 -1.23e6 = 1.23 * 1000000 +1e3 === 1 * 1000 // e3 означает *1000 +1.23e6 === 1.23 * 1000000 // e6 означает *1000000 ``` - -Сейчас давайте запишем что-нибудь очень маленькое. К примеру, 1 микросекунду (одна миллионная секунды): +А сейчас давайте запишем что-нибудь очень маленькое. К примеру, 1 микросекунду (одна миллионная секунды): ```js -let ms = 0.000001; +let mcs = 0.000001; ``` -Записать микросекунду в укороченном виде нам поможет `"e"`. +В этом случае нам также поможет `"e"`. Если мы хотим избежать записи длинной последовательности из нулей, мы можем сделать так: ```js -let ms = 1e-6; // шесть нулей, слева от 1 +let ms = 1e-6; // шесть нулей слева от 1 ``` -Если мы подсчитаем количество нулей `0.000001`, их будет 6. Естественно, верная запись `1e-6`. +Если мы подсчитаем количество нулей в `0.000001`, их будет 6. Естественно, верная запись `1e-6`. -Другими словами, отрицательное число после `"e"` подразумевает деление на 1 с указанным количеством нулей: +Другими словами, отрицательное число после `"e"` подразумевает деление на `1` с указанным количеством нулей: ```js -// -3 делится на 1 с 3 нулями -1e-3 = 1 / 1000 (=0.001) +// 1 делится на 1 с 3 нулями +1e-3 === 1 / 1000 (=0.001) -// -6 делится на 1 с 6 нулями -1.23e-6 = 1.23 / 1000000 (=0.00000123) +// 1.23 делится на 1 с 6 нулями +1.23e-6 === 1.23 / 1000000 (=0.00000123) ``` ### Шестнадцатеричные, двоичные и восьмеричные числа @@ -62,14 +71,13 @@ let ms = 1e-6; // шесть нулей, слева от 1 ```js run alert( 0xff ); // 255 -alert( 0xFF ); // 255 (тоже самое, регистр не имеет значения) +alert( 0xFF ); // 255 (то же самое, регистр не имеет значения) ``` -Не так часто используются двоичные и восьмеричные числа, но они также поддерживаются `0b` для двоичных и `0o` для восьмеричных: - +Двоичные и восьмеричные числа используются не так часто, но они также поддерживаются: `0b` для двоичных и `0o` для восьмеричных: ```js run -let a = 0b11111111; // бинарная форма записи числа 255 +let a = 0b11111111; // двоичная (бинарная) форма записи числа 255 let b = 0o377; // восьмеричная форма записи числа 255 alert( a == b ); // true, с двух сторон число 255 @@ -149,7 +157,7 @@ alert( num.toString(2) ); // 11111111 ```js run let num = 1.23456; - alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 + alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 ``` 2. Метод [toFixed(n)](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) округляет число до `n` знаков после запятой и возвращает строковое представление результата. @@ -177,7 +185,7 @@ alert( num.toString(2) ); // 11111111 ## Неточные вычисления -Внутри JavaScript число представлено в виде 64-битного формата [IEEE-754](https://ru.wikipedia.org/wiki/IEEE_754-1985). Для хранения числа используется 64 бита: 52 из них используется для хранения цифр, 11 из них для хранения положения десятичной точки (если число целое, то хранится 0), и один бит отведён на хранение знака. +Внутри JavaScript число представлено в виде 64-битного формата [IEEE-754](https://ru.wikipedia.org/wiki/IEEE_754-1985). Для хранения числа используется 64 бита: 52 из них используется для хранения цифр, 11 для хранения положения десятичной точки и один бит отведён на хранение знака. Если число слишком большое, оно переполнит 64-битное хранилище, JavaScript вернёт бесконечность: @@ -207,6 +215,12 @@ alert( 0.1 + 0.2 ); // 0.30000000000000004 Число хранится в памяти в бинарной форме, как последовательность бит - единиц и нулей. Но дроби, такие как `0.1`, `0.2`, которые выглядят довольно просто в десятичной системе счисления, на самом деле являются бесконечной дробью в двоичной форме. +```js run +alert(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101 +alert(0.2.toString(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` становится бесконечной дробью. @@ -225,14 +239,14 @@ alert( 0.1.toFixed(20) ); // 0.10000000000000000555 Вот почему `0.1 + 0.2` - это не совсем `0.3`. ```smart header="Не только в JavaScript" -Справедливости ради заметим, что ошибка в точности вычислений для чисел с плавающей точкой сохраняется в любом другом языке, где используется формат IEEE 754, включая PHP, Java, C, Perl, Ruby. +Справедливости ради заметим, что ошибка в точности вычислений для чисел с плавающей точкой сохраняется в любом другом языке, где используется формат IEEE 754, включая PHP, Java, C, Perl и Ruby. ``` Можно ли обойти проблему? Конечно, наиболее надёжный способ — это округлить результат используя метод [toFixed(n)](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed): ```js run let sum = 0.1 + 0.2; -alert( sum.toFixed(2) ); // 0.30 +alert( sum.toFixed(2) ); // "0.30" ``` Помните, что метод `toFixed` всегда возвращает строку. Это гарантирует, что результат будет с заданным количеством цифр в десятичной части. Также это удобно для форматирования цен в интернет-магазине `$0.30`. В других случаях можно использовать унарный оператор `+`, чтобы преобразовать строку в число: @@ -242,7 +256,7 @@ let sum = 0.1 + 0.2; alert( +sum.toFixed(2) ); // 0.3 ``` -Также можно временно умножить число на 100 (или на большее), чтобы привести его к целому, выполнить математические действия, а после разделить обратно. Суммируя целые числа, мы уменьшаем погрешность, но она все равно появляется при финальном делении: +Также можно временно умножить число на 100 (или на большее), чтобы привести его к целому, выполнить математические действия, а после разделить обратно. Суммируя целые числа, мы уменьшаем погрешность, но она всё равно появляется при финальном делении: ```js run alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 @@ -293,7 +307,7 @@ alert( 9999999999999999 ); // покажет 10000000000000000 alert( isNaN("str") ); // true ``` - Нужна ли нам эта функция? Разве не можем ли мы просто сравнить `=== NaN`? К сожалению, нет. Значение `NaN` уникально тем, что оно не является равным ни чему другому, даже самому себе: + Нужна ли нам эта функция? Разве не можем ли мы просто сравнить `=== NaN`? К сожалению, нет. Значение `NaN` уникально тем, что оно не является равным ничему другому, даже самому себе: ```js run alert( NaN === NaN ); // false @@ -311,13 +325,42 @@ alert( 9999999999999999 ); // покажет 10000000000000000 ```js run -let num = +prompt("Enter a number", ''); +let num = +prompt("Введите число:", ''); // вернёт true всегда, кроме ситуаций, когда аргумент - Infinity/-Infinity или не число alert( isFinite(num) ); ``` -Помните, что пустая строка интерпретируется как `0` во всех числовых функциях, включая`isFinite`. +Помните, что пустая строка интерпретируется как `0` во всех числовых функциях, включая`isFinite`. + +````smart header="`Number.isNaN` и `Number.isFinite`" +Методы [Number.isNaN](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) и [Number.isFinite](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) - это более "строгие" версии функций `isNaN` и `isFinite`. Они не преобразуют аргумент в число, а наоборот - первым делом проверяют, является ли аргумент числом (принадлежит ли он к типу `number`). + +- `Number.isNaN(value)` возвращает `true` только в том случае, если аргумент принадлежит к типу `number` и является `NaN`. Во всех остальных случаях возвращает `false`. + + ```js run + alert( Number.isNaN(NaN) ); // true + alert( Number.isNaN("str" / 2) ); // true + + // Обратите внимание на разный результат: + alert( Number.isNaN("str") ); // false, так как "str" является строкой, а не числом + alert( isNaN("str") ); // true, так как isNaN сначала преобразует строку "str" в число и в результате преобразования получает NaN + ``` + +- `Number.isFinite(value)` возвращает `true` только в том случае, если аргумент принадлежит к типу `number` и не является `NaN/Infinity/-Infinity`. Во всех остальных случаях возвращает `false`. + + ```js run + alert( Number.isFinite(123) ); // true + alert( Number.isFinite(Infinity) ); // false + alert( Number.isFinite(2 / 0) ); // false + + // Обратите внимание на разный результат: + alert( Number.isFinite("123") ); // false, так как "123" является строкой, а не числом + alert( isFinite("123") ); // true, так как isFinite сначала преобразует строку "123" в число 123 + ``` + +Не стоит считать `Number.isNaN` и `Number.isFinite` более "корректными" версиями функций `isNaN` и `isFinite`. Это дополняющие друг-друга инструменты для разных задач. +```` ```smart header="Сравнение `Object.is`" @@ -407,10 +450,10 @@ alert( parseInt('2n9c', 36) ); // 123456 ## Итого -Чтобы писать большие числа: +Чтобы писать числа с большим количеством нулей: -- Используйте краткую форму записи больших чисел - `"e"`, с указанным количеством нулей. Например: `123e6` это `123` с 6-ю нулями. -- Отрицательное число после `"e"` приводит к делению числа на 1 с указанным количеством нулей. +- Используйте краткую форму записи чисел - `"e"`, с указанным количеством нулей. Например: `123e6` это `123` с 6-ю нулями `123000000`. +- Отрицательное число после `"e"` приводит к делению числа на 1 с указанным количеством нулей. Например: `123e-6` это `0.000123` (`123` миллионных). Для других систем счисления: @@ -418,6 +461,13 @@ alert( parseInt('2n9c', 36) ); // 123456 - `parseInt(str, base)` преобразует строку в целое число в соответствии с указанной системой счисления: `2 ≤ base ≤ 36`. - `num.toString(base)` представляет число в строковом виде в указанной системе счисления `base`. +Для проверки на `NaN` и `Infinity`: + +- `isNaN(value)` преобразует аргумент в число и проверяет, является ли оно `NaN` +- `Number.isNaN(value)` проверяет, является ли аргумент числом, и если да, то проверяет, является ли оно `NaN` +- `isFinite(value)` преобразует аргумент в число и проверяет, что оно не является `NaN/Infinity/-Infinity` +- `Number.isFinite(value)` проверяет, является ли аргумент числом, и если да, то проверяет, что оно не является `NaN/Infinity/-Infinity` + Для преобразования значений типа `12pt` и `100px` в число: - Используйте `parseInt/parseFloat` для "мягкого" преобразования строки в число, данные функции по порядку считывают число из строки до тех пор пока не возникнет ошибка. diff --git a/1-js/05-data-types/03-string/3-truncate/solution.md b/1-js/05-data-types/03-string/3-truncate/solution.md index d9d1d6e1aa..4f53946cb8 100644 --- a/1-js/05-data-types/03-string/3-truncate/solution.md +++ b/1-js/05-data-types/03-string/3-truncate/solution.md @@ -1,6 +1,6 @@ Строка, которую мы возвращаем, должна быть не длиннее `maxlength`, поэтому, если мы обрезаем строку, то мы должны убрать на один символ больше, чем `maxlength` — чтобы хватило места на многоточие. -Имейте в виду, что в качестве многоточия здесь используется `…` — ровно один специальный юникодный символ. Это не то же самое, что `...` — три точки. +Имейте в виду, что в качестве многоточия здесь используется `…` — ровно один специальный Юникодный символ. Это не то же самое, что `...` — три точки. ```js run demo function truncate(str, maxlength) { diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md index 32133f9fc6..11acb7b03c 100644 --- a/1-js/05-data-types/03-string/article.md +++ b/1-js/05-data-types/03-string/article.md @@ -1,3 +1,4 @@ + # Строки В JavaScript любые текстовые данные являются строками. Не существует отдельного типа "символ", который есть в ряде других языков. @@ -48,7 +49,7 @@ let guestList = "Guests: // Error: Unexpected token ILLEGAL Одинарные и двойные кавычки в языке с незапамятных времён: тогда потребность в многострочных строках не учитывалась. Что касается обратных кавычек, они появились существенно позже, и поэтому они гибче. -Обратные кавычки также позволяют задавать "шаблонную функцию" перед первой обратной кавычкой. Используемый синтаксис: func`string`. Автоматически вызываемая функция `func` получает строку и встроенные в неё выражения и может их обработать. Подробнее об этом можно прочитать в [документации](mdn:/JavaScript/Reference/Template_literals#Tagged_templates). Если перед строкой есть выражение, то шаблонная строка называется "теговым шаблоном". Это позволяет использовать свою шаблонизацию для строк, но на практике теговые шаблоны применяются редко. +Обратные кавычки также позволяют задавать "шаблонную функцию" перед первой обратной кавычкой. Используемый синтаксис: func`string`. Автоматически вызываемая функция `func` получает строку и встроенные в неё выражения и может их обработать. Подробнее об этом можно прочитать в [документации](mdn:/JavaScript/Reference/template_strings#%D0%A2%D0%B5%D0%B3%D0%BE%D0%B2%D1%8B%D0%B5_%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D1%8B). Если перед строкой есть выражение, то шаблонная строка называется "теговым шаблоном". Это позволяет использовать свою шаблонизацию для строк, но на практике теговые шаблоны применяются редко. ## Спецсимволы @@ -78,29 +79,13 @@ alert(str1 == str2); // true | Символ | Описание | |-----------|-------------| |`\n`|Перевод строки| -|`\r`|Возврат каретки: самостоятельно не используется. В текстовых файлах Windows для перевода строки используется комбинация символов `\r\n`. | -|`\'`, `\"`|Кавычки| +|`\r`|В текстовых файлах Windows для перевода строки используется комбинация символов `\r\n`, а на других ОС это просто `\n`. Это так по историческим причинам, ПО под Windows обычно понимает и просто `\n`. | +|`\'`, `\"`, \\`|Кавычки| |`\\`|Обратный слеш| |`\t`|Знак табуляции| |`\b`, `\f`, `\v`| Backspace, Form Feed и Vertical Tab — оставлены для обратной совместимости, сейчас не используются. | -|`\xXX`|Символ с шестнадцатеричным юникодным кодом `XX`, например, `'\x7A'` — то же самое, что `'z'`.| -|`\uXXXX`|Символ в кодировке UTF-16 с шестнадцатеричным кодом `XXXX`, например, `\u00A9` — юникодное представление знака копирайта, `©`. Код должен состоять ровно из 4 шестнадцатеричных цифр. | -|`\u{X…XXXXXX}` (от 1 до 6 шестнадцатеричных цифр)|Символ в кодировке UTF-32 с шестнадцатеричным кодом от U+0000 до U+10FFFF. Некоторые редкие символы кодируются двумя 16-битными словами и занимают 4 байта. Так можно вставлять символы с длинным кодом. | - -Примеры с Юникодом: - -```js run -// © -alert( "\u00A9" ); - -// Длинные юникодные коды -// 佫, редкий китайский иероглиф -alert( "\u{20331}" ); -// 😍, лицо с улыбкой и глазами в форме сердец -alert( "\u{1F60D}" ); -``` -Все спецсимволы начинаются с обратного слеша, `\` — так называемого "символа экранирования". +Как вы можете видеть, все спецсимволы начинаются с обратного слеша, `\` — так называемого "символа экранирования". Он также используется, если необходимо вставить в строку кавычку. @@ -146,35 +131,38 @@ alert( `My\n`.length ); // 3 ## Доступ к символам -Получить символ, который занимает позицию `pos`, можно с помощью квадратных скобок: `[pos]`. Также можно использовать метод `charAt`: [str.charAt(pos)](mdn:js/String/charAt). Первый символ занимает нулевую позицию: +Получить символ, который занимает позицию `pos`, можно с помощью квадратных скобок: `[pos]`. Также можно использовать метод [str.at(pos)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at). Первый символ занимает нулевую позицию: ```js run let str = `Hello`; // получаем первый символ alert( str[0] ); // H -alert( str.charAt(0) ); // H +alert( str.at(0) ); // H // получаем последний символ alert( str[str.length - 1] ); // o +alert( str.at(-1) ); // o ``` -Квадратные скобки — современный способ получить символ, в то время как `charAt` существует в основном по историческим причинам. +Как вы можете видеть, преимущество метода `.at(pos)` заключается в том, что он допускает отрицательную позицию. Если `pos` -- отрицательное число, то отсчет ведется от конца строки. + +Таким образом, `.at(-1)` означает последний символ, а `.at(-2)` -- тот, что перед ним, и т.д. -Разница только в том, что если символ с такой позицией отсутствует, тогда `[]` вернёт `undefined`, а `charAt` — пустую строку: +Квадратные скобки всегда возвращают `undefined` для отрицательных индексов. Например: ```js run let str = `Hello`; -alert( str[1000] ); // undefined -alert( str.charAt(1000) ); // '' (пустая строка) +alert( str[-2] ); // undefined +alert( str.at(-2) ); // l ``` Также можно перебрать строку посимвольно, используя `for..of`: ```js run for (let char of "Hello") { - alert(char); // H,e,l,l,o (char — сначала "H", потом "e", потом "l" и т. д.) + alert(char); // H,e,l,l,o (char — сначала "H", потом "e", потом "l" и т.д.) } ``` @@ -216,7 +204,7 @@ alert( 'Interface'.toLowerCase() ); // interface Если мы захотим перевести в нижний регистр какой-то конкретный символ: -```js +```js run alert( 'Interface'[0].toLowerCase() ); // 'i' ``` @@ -313,7 +301,7 @@ if (str.indexOf("Widget") != -1) { ``` #### Трюк с побитовым НЕ -Существует старый трюк с использованием [побитового оператора НЕ](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#.7E_.28Bitwise_NOT.29) — `~`. Он преобразует число в 32-разрядное целое со знаком (signed 32-bit integer). Дробная часть, в случае, если она присутствует, отбрасывается. Затем все биты числа инвертируются. +Существует старый трюк с использованием [побитового оператора НЕ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_NOT) — `~`. Он преобразует число в 32-разрядное целое со знаком (signed 32-bit integer). Дробная часть, в случае, если она присутствует, отбрасывается. Затем все биты числа инвертируются. На практике это означает простую вещь: для 32-разрядных целых чисел значение `~n` равно `-(n+1)`. @@ -372,8 +360,8 @@ alert( "Midget".includes("id", 3) ); // false, поиск начат с пози Методы [str.startsWith](mdn:js/String/startsWith) и [str.endsWith](mdn:js/String/endsWith) проверяют, соответственно, начинается ли и заканчивается ли строка определённой строкой: ```js run -alert( "Widget".startsWith("Wid") ); // true, "Wid" — начало "Widget" -alert( "Widget".endsWith("get") ); // true, "get" — окончание "Widget" +alert( "*!*Wid*/!*get".startsWith("Wid") ); // true, "Wid" — начало "Widget" +alert( "Wid*!*get*/!*".endsWith("get") ); // true, "get" — окончание "Widget" ``` ## Получение подстроки @@ -410,9 +398,10 @@ alert( "Widget".endsWith("get") ); // true, "get" — окончание "Widget ``` `str.substring(start [, end])` -: Возвращает часть строки *между* `start` и `end`. +: Возвращает часть строки *между* `start` и `end` (не включая) `end`. - Это — почти то же, что и `slice`, но можно задавать `start` больше `end`. + Это — почти то же, что и `slice`, но можно задавать `start` больше `end`. + Если `start` больше `end`, то метод `substring` сработает так, как если бы аргументы были поменяны местами. Например: @@ -450,12 +439,14 @@ alert( "Widget".endsWith("get") ); // true, "get" — окончание "Widget alert( str.substr(-4, 2) ); ``` + Этот метод находится в [Annex B](https://tc39.es/ecma262/#sec-string.prototype.substr) спецификации языка. Это означает, что его должны поддерживать только браузерные движки JavaScript, и использовать его не рекомендуется. Но на практике он поддерживается везде. + Давайте подытожим, как работают эти методы, чтобы не запутаться: | метод | выбирает… | отрицательные значения | |--------|-----------|-----------| | `slice(start, end)` | от `start` до `end` (не включая `end`) | можно передавать отрицательные значения | -| `substring(start, end)` | между `start` и `end` | отрицательные значения равнозначны `0` | +| `substring(start, end)` | между `start` и `end` (не включая `end`) | отрицательные значения равнозначны `0` | | `substr(start, length)` | `length` символов, начиная от `start` | значение `start` может быть отрицательным | ```smart header="Какой метод выбрать?" @@ -505,13 +496,6 @@ alert( "Widget".endsWith("get") ); // true, "get" — окончание "Widget alert( String.fromCodePoint(90) ); // Z ``` - Также можно добавлять юникодные символы по их кодам, используя `\u` с шестнадцатеричным кодом символа: - - ```js run - // 90 — 5a в шестнадцатеричной системе счисления - alert( '\u005a' ); // Z - ``` - Давайте сделаем строку, содержащую символы с кодами от `65` до `220` — это латиница и ещё некоторые распространённые символы: ```js run @@ -534,13 +518,13 @@ alert( str ); - Все строчные буквы идут после заглавных, так как их коды больше. - Некоторые буквы, такие как `Ö`, вообще находятся вне основного алфавита. У этой буквы код больше, чем у любой буквы от `a` до `z`. -### Правильное сравнение +### Правильное сравнение [#correct-comparisons] "Правильный" алгоритм сравнения строк сложнее, чем может показаться, так как разные языки используют разные алфавиты. Поэтому браузеру нужно знать, какой язык использовать для сравнения. -К счастью, все современные браузеры (для IE10− нужна дополнительная библиотека [Intl.JS](https://github.com/andyearnshaw/Intl.js/)) поддерживают стандарт [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), обеспечивающий правильное сравнение строк на разных языках с учётом их правил. +К счастью, все современные браузеры (для IE10− нужна дополнительная библиотека [Intl.JS](https://github.com/andyearnshaw/Intl.js/)) поддерживают стандарт [ECMA 402](https://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), обеспечивающий правильное сравнение строк на разных языках с учётом их правил. Для этого есть соответствующий метод. @@ -558,119 +542,12 @@ alert( 'Österreich'.localeCompare('Zealand') ); // -1 У этого метода есть два дополнительных аргумента, которые указаны в [документации](mdn:js/String/localeCompare). Первый позволяет указать язык (по умолчанию берётся из окружения) — от него зависит порядок букв. Второй — определить дополнительные правила, такие как чувствительность к регистру, а также следует ли учитывать различия между `"a"` и `"á"`. -## Как всё устроено, Юникод - -```warn header="Глубокое погружение в тему" -Этот раздел более подробно описывает, как устроены строки. Такие знания пригодятся, если вы намерены работать с эмодзи, редкими математическими символами, иероглифами, либо с ещё какими-то редкими символами. - -Если вы не планируете их поддерживать, эту секцию можно пропустить. -``` - -### Суррогатные пары - -Многие символы возможно записать одним 16-битным словом: это и буквы большинства европейских языков, и числа, и даже многие иероглифы. - -Но 16 битов — это 65536 комбинаций, так что на все символы этого, разумеется, не хватит. Поэтому редкие символы записываются двумя 16-битными словами — это также называется "суррогатная пара". - -Длина таких строк — `2`: - -```js run -alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X -alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY -alert( '𩷶'.length ); // 2, редкий китайский иероглиф -``` - -Обратите внимание, суррогатные пары не существовали, когда был создан JavaScript, поэтому язык не обрабатывает их адекватно! - -Ведь в каждой из этих строк только один символ, а `length` показывает длину `2`. - -`String.fromCodePoint` и `str.codePointAt` — два редких метода, правильно работающие с суррогатными парами, но они и появились в языке недавно. До них были только [String.fromCharCode](mdn:js/String/fromCharCode) и [str.charCodeAt](mdn:js/String/charCodeAt). Эти методы, вообще, делают то же самое, что `fromCodePoint/codePointAt`, но не работают с суррогатными парами. - -Получить символ, представленный суррогатной парой, может быть не так просто, потому что суррогатная пара интерпретируется как два символа: - -```js run -alert( '𝒳'[0] ); // странные символы… -alert( '𝒳'[1] ); // …части суррогатной пары -``` - -Части суррогатной пары не имеют смысла сами по себе, так что вызовы `alert` в этом примере покажут лишь мусор. - -Технически, суррогатные пары возможно обнаружить по их кодам: если код символа находится в диапазоне `0xd800..0xdbff`, то это — первая часть суррогатной пары. Следующий символ — вторая часть — имеет код в диапазоне `0xdc00..0xdfff`. Эти два диапазона выделены исключительно для суррогатных пар по стандарту. - -В данном случае: - -```js run -// charCodeAt не поддерживает суррогатные пары, поэтому возвращает код для их частей - -alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, между 0xd800 и 0xdbff -alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, между 0xdc00 и 0xdfff -``` - -Дальше в главе будут ещё способы работы с суррогатными парами. Для этого есть и специальные библиотеки, но нет достаточно широко известной, чтобы предложить её здесь. - -### Диакритические знаки и нормализация - -Во многих языках есть символы, состоящие из некоторого основного символа со знаком сверху или снизу. - -Например, буква `a` — это основа для `àáâäãåā`. Наиболее используемые составные символы имеют свой собственный код в таблице UTF-16. Но не все, в силу большого количества комбинаций. - -Чтобы поддерживать любые комбинации, UTF-16 позволяет использовать несколько юникодных символов: основной и дальше один или несколько особых символов-знаков. - -Например, если после `S` добавить специальный символ "точка сверху" (код `\u0307`), отобразится Ṡ. - -```js run -alert( 'S\u0307' ); // Ṡ -``` - -Если надо добавить сверху (или снизу) ещё один знак — без проблем, просто добавляем соответствующий символ. - -Например, если добавить символ "точка снизу" (код `\u0323`), отобразится S с точками сверху и снизу: `Ṩ`. - -Добавляем два символа: - -```js run -alert( 'S\u0307\u0323' ); // Ṩ -``` - -Это даёт большую гибкость, но из-за того, что порядок дополнительных символов может быть различным, мы получаем проблему сравнения символов: можно представить по-разному символы, которые ничем визуально не отличаются. - -Например: - -```js run -let s1 = 'S\u0307\u0323'; // Ṩ, S + точка сверху + точка снизу -let s2 = 'S\u0323\u0307'; // Ṩ, S + точка снизу + точка сверху - -alert( `s1: ${s1}, s2: ${s2}` ); - -alert( s1 == s2 ); // false, хотя на вид символы одинаковы (?!) -``` - -Для решения этой проблемы есть алгоритм "юникодной нормализации", приводящий каждую строку к единому "нормальному" виду. - -Его реализует метод [str.normalize()](mdn:js/String/normalize). - -```js run -alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true -``` - -Забавно, но в нашем случае `normalize()` "схлопывает" последовательность из трёх символов в один: `\u1e68` — S с двумя точками. - -```js run -alert( "S\u0307\u0323".normalize().length ); // 1 - -alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true -``` - -Разумеется, так происходит не всегда. Просто Ṩ — это достаточно часто используемый символ, поэтому создатели UTF-16 включили его в основную таблицу и присвоили ему код. - -Подробнее о правилах нормализации и составлении символов можно прочитать в дополнении к стандарту Юникод: [Unicode Normalization Forms](http://www.unicode.org/reports/tr15/). Для большинства практических целей информации из этого раздела достаточно. - ## Итого - Есть три типа кавычек. Строки, использующие обратные кавычки, могут занимать более одной строки в коде и включать выражения `${…}`. - Строки в JavaScript кодируются в UTF-16. -- Есть специальные символы, такие как `\n`, и можно добавить символ по его юникодному коду, используя `\u…`. -- Для получения символа используйте `[]`. +- Есть специальные символы, такие как разрыв строки `\n`. +- Для получения символа используйте `[]` или метод `at`. - Для получения подстроки используйте `slice` или `substring`. - Для того, чтобы перевести строку в нижний или верхний регистр, используйте `toLowerCase/toUpperCase`. - Для поиска подстроки используйте `indexOf` или `includes/startsWith/endsWith`, когда надо только проверить, есть ли вхождение. @@ -682,4 +559,6 @@ alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true - `str.repeat(n)` — повторяет строку `n` раз. - …и другие, которые вы можете найти в [справочнике](mdn:js/String). -Также есть методы для поиска и замены с использованием регулярных выражений. Но это отдельная большая тема, поэтому ей посвящена отдельная глава учебника . +Для строк предусмотрены методы для поиска и замены с использованием регулярных выражений. Но это отдельная большая тема, поэтому ей посвящена отдельная глава учебника . + +Также, на данный момент важно знать, что строки основаны на кодировке Юникод, и поэтому иногда могут возникать проблемы со сравнениями. Подробнее о Юникоде в главе . diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/solution.md b/1-js/05-data-types/04-array/10-maximal-subarray/solution.md index 71b339c82a..3abf9e3a72 100644 --- a/1-js/05-data-types/04-array/10-maximal-subarray/solution.md +++ b/1-js/05-data-types/04-array/10-maximal-subarray/solution.md @@ -91,4 +91,4 @@ alert( getMaxSubSum([-1, -2, -3]) ); // 0 Этот алгоритм требует ровно 1 проход по массиву и его оценка сложности O(n). -Больше информации об алгоритме тут: [Задача поиска максимальной суммы подмассива](http://en.wikipedia.org/wiki/Maximum_subarray_problem). Если всё ещё не очевидно как это работает, просмотрите алгоритм в примерах выше, это будет лучше всяких слов. +Больше информации об алгоритме тут: [Задача поиска максимальной суммы подмассива](https://en.wikipedia.org/wiki/Maximum_subarray_problem). Если всё ещё не очевидно как это работает, просмотрите алгоритм в примерах выше, это будет лучше всяких слов. diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/task.md b/1-js/05-data-types/04-array/10-maximal-subarray/task.md index 443d99789b..788913ec45 100644 --- a/1-js/05-data-types/04-array/10-maximal-subarray/task.md +++ b/1-js/05-data-types/04-array/10-maximal-subarray/task.md @@ -13,12 +13,12 @@ importance: 2 Например: ```js -getMaxSubSum([-1, *!*2, 3*/!*, -9]) = 5 (сумма выделенных) -getMaxSubSum([*!*2, -1, 2, 3*/!*, -9]) = 6 -getMaxSubSum([-1, 2, 3, -9, *!*11*/!*]) = 11 -getMaxSubSum([-2, -1, *!*1, 2*/!*]) = 3 -getMaxSubSum([*!*100*/!*, -9, 2, -3, 5]) = 100 -getMaxSubSum([*!*1, 2, 3*/!*]) = 6 (берём все) +getMaxSubSum([-1, *!*2, 3*/!*, -9]) == 5 (сумма выделенных элементов) +getMaxSubSum([*!*2, -1, 2, 3*/!*, -9]) == 6 +getMaxSubSum([-1, 2, 3, -9, *!*11*/!*]) == 11 +getMaxSubSum([-2, -1, *!*1, 2*/!*]) == 3 +getMaxSubSum([*!*100*/!*, -9, 2, -3, 5]) == 100 +getMaxSubSum([*!*1, 2, 3*/!*]) == 6 (берём все) ``` Если все элементы отрицательные - ничего не берём(подмассив пустой) и сумма равна "0": diff --git a/1-js/05-data-types/04-array/2-create-array/task.md b/1-js/05-data-types/04-array/2-create-array/task.md index 413a7f4e0e..630f777a1b 100644 --- a/1-js/05-data-types/04-array/2-create-array/task.md +++ b/1-js/05-data-types/04-array/2-create-array/task.md @@ -10,7 +10,7 @@ importance: 5 2. Добавьте "Рок-н-ролл" в конец. 3. Замените значение в середине на "Классика". Ваш код для поиска значения в середине должен работать для массивов с любой длиной. 4. Удалите первый элемент массива и покажите его. -5. Вставьте "Рэп" и "Регги" в начало массива. +5. Вставьте `Рэп` и `Регги` в начало массива. Массив по ходу выполнения операций: diff --git a/1-js/05-data-types/04-array/3-call-array-this/solution.md b/1-js/05-data-types/04-array/3-call-array-this/solution.md index 097ba62a04..bc8bab16b9 100644 --- a/1-js/05-data-types/04-array/3-call-array-this/solution.md +++ b/1-js/05-data-types/04-array/3-call-array-this/solution.md @@ -9,7 +9,7 @@ arr.push(function() { alert( this ); }) -arr[2](); // "a","b",function +arr[2](); // a,b,function(){...} ``` У массива в итоге 3 элемента: сначала их было 2, плюс функция. diff --git a/1-js/05-data-types/04-array/3-call-array-this/task.md b/1-js/05-data-types/04-array/3-call-array-this/task.md index 6cba90761d..7016df653a 100644 --- a/1-js/05-data-types/04-array/3-call-array-this/task.md +++ b/1-js/05-data-types/04-array/3-call-array-this/task.md @@ -11,7 +11,7 @@ let arr = ["a", "b"]; arr.push(function() { alert( this ); -}) +}); arr[2](); // ? ``` diff --git a/1-js/05-data-types/04-array/array-pop.svg b/1-js/05-data-types/04-array/array-pop.svg index b6752bfebc..b97b0c5c97 100644 --- a/1-js/05-data-types/04-array/array-pop.svg +++ b/1-js/05-data-types/04-array/array-pop.svg @@ -1 +1 @@ -0123"Apple""Orange""Pear""Lemon"length = 4очистить012"Apple""Orange""Pear"length = 3 \ No newline at end of file +0123"Apple""Orange""Pear""Lemon"length = 4очистить012"Apple""Orange""Pear"length = 3 \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-shift.svg b/1-js/05-data-types/04-array/array-shift.svg index d3d229769a..5319324c5b 100644 --- a/1-js/05-data-types/04-array/array-shift.svg +++ b/1-js/05-data-types/04-array/array-shift.svg @@ -1 +1 @@ -123"Orange""Pear""Lemon"length = 423"Orange""Pear""Lemon"length = 3очиститьпередвинуть элементы влево0"Apple"012"Orange""Pear""Lemon"11 \ No newline at end of file +123"Orange""Pear""Lemon"length = 423"Orange""Pear""Lemon"length = 3очиститьпередвинуть элементы влево0"Apple"012"Orange""Pear""Lemon"11 \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-speed.svg b/1-js/05-data-types/04-array/array-speed.svg index 5660cd5ec7..41f7d998b4 100644 --- a/1-js/05-data-types/04-array/array-speed.svg +++ b/1-js/05-data-types/04-array/array-speed.svg @@ -1 +1 @@ -0123popunshiftpushshift \ No newline at end of file +0123popunshiftpushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/article.md b/1-js/05-data-types/04-array/article.md index eebf753e55..0229729c0c 100644 --- a/1-js/05-data-types/04-array/article.md +++ b/1-js/05-data-types/04-array/article.md @@ -2,7 +2,7 @@ Объекты позволяют хранить данные со строковыми ключами. Это замечательно. -Но довольно часто мы понимаем, что нам необходима *упорядоченная коллекция* данных, в которой присутствуют 1-й, 2-й, 3-й элементы и т.д. Например, она понадобится нам для хранения списка чего-либо: пользователей, товаров, элементов HTML и т.д. +Но довольно часто мы понимаем, что нам необходима *упорядоченная коллекция* данных, в которой присутствуют 1-й, 2-й, 3-й элементы и т.д. Например, она понадобится нам для хранения списка чего-либо: пользователей, товаров, элементов HTML и т.д. В этом случае использовать объект неудобно, так как он не предоставляет методов управления порядком элементов. Мы не можем вставить новое свойство "между" уже существующими. Объекты просто не предназначены для этих целей. @@ -91,6 +91,38 @@ let fruits = [ "Висячая запятая" упрощает процесс добавления/удаления элементов, так как все строки становятся идентичными. ```` +## Получение последних элементов при помощи "at" + +[recent browser="new"] + +Допустим, нам нужен последний элемент массива. + +Некоторые языки программирования позволяют использовать отрицательные индексы для той же цели, как-то так: `fruits[-1]`. + +Однако, в JavaScript такая запись не сработает. Её результатом будет `undefined`, поскольку индекс в квадратных скобках понимается буквально. + +Мы можем явно вычислить индекс последнего элемента, а затем получить к нему доступ вот так: `fruits[fruits.length - 1]`. + +```js run +let fruits = ["Apple", "Orange", "Plum"]; +alert( fruits[fruits.length-1] ); // Plum +``` + +Немного громоздко, не так ли? Нам нужно дважды написать имя переменной. + +К счастью, есть более короткий синтаксис: `fruits.at(-1)`: + +```js run +let fruits = ["Apple", "Orange", "Plum"]; +// то же самое, что и fruits[fruits.length-1] +alert( fruits.at(-1) ); // Plum +``` + +Другими словами, `arr.at(i)`: + +- это ровно то же самое, что и `arr[i]`, если `i >= 0`. +- для отрицательных значений `i`, он отступает от конца массива. + ## Методы pop/push, shift/unshift [Очередь](https://ru.wikipedia.org/wiki/Очередь_(программирование)) – один из самых распространённых вариантов применения массива. В области компьютерных наук так называется упорядоченная коллекция элементов, поддерживающая два вида операций: @@ -134,6 +166,8 @@ let fruits = [ alert( fruits ); // Яблоко, Апельсин ``` + И `fruits.pop()` и `fruits.at(-1)` возвращают последний элемент массива, но `fruits.pop()` также изменяет массив, удаляя его. + `push` : Добавляет элемент в конец массива: @@ -152,7 +186,7 @@ let fruits = [ `shift` : Удаляет из массива первый элемент и возвращает его: - ```js + ```js run let fruits = ["Яблоко", "Апельсин", "Груша"]; alert( fruits.shift() ); // удаляем Яблоко и выводим его @@ -163,7 +197,7 @@ let fruits = [ `unshift` : Добавляет элемент в начало массива: - ```js + ```js run let fruits = ["Апельсин", "Груша"]; fruits.unshift('Яблоко'); @@ -171,7 +205,7 @@ let fruits = [ alert( fruits ); // Яблоко, Апельсин, Груша ``` -Методы `push` и `unshift` могут добавлять сразу несколько элементов: +Методы `push` и `unshift` могут добавлять сразу несколько элементов: ```js run let fruits = ["Яблоко"]; @@ -189,7 +223,7 @@ alert( fruits ); Массивы расширяют объекты, так как предусматривают специальные методы для работы с упорядоченными коллекциями данных, а также свойство `length`. Но в основе всё равно лежит объект. -Следует помнить, что в JavaScript существует всего 7 основных типов данных. Массив является объектом и, следовательно, ведёт себя как объект. +Следует помнить, что в JavaScript существует 8 основных типов данных. Массив является объектом и, следовательно, ведёт себя как объект. Например, копируется по ссылке: @@ -318,7 +352,7 @@ for (let key in arr) { 2. Цикл `for..in` оптимизирован под произвольные объекты, не массивы, и поэтому в 10-100 раз медленнее. Увеличение скорости выполнения может иметь значение только при возникновении узких мест. Но мы всё же должны представлять разницу. -В общем, не следует использовать цикл `for..in` для массивов. +В общем, не следует использовать цикл `for..in` для массивов. ## Немного о "length" @@ -424,31 +458,84 @@ alert( "1" + 1 ); // "11" alert( "1,2" + 1 ); // "1,21" ``` +## Не сравнивайте массивы при помощи == + +В JavaScript, в отличие от некоторых других языков программирования, массивы не следует сравнивать при помощи оператора `==`. + +У этого оператора нет специального подхода к массивам, он работает с ними, как и с любыми другими объектами. + +Давайте ещё раз напомним правила: + +- Два объекта равны друг другу `==` только в том случае, если они ссылаются на один и тот же объект. +- Если один из аргументов `==` является объектом, а другой - примитивом, то объект преобразуется в примитив, как описано в главе . +- ...За исключением `null` и `undefined`, которые равны `==` друг другу и ничему больше. + +Оператор строгого равенства `===` ещё проще, так как он не преобразует типы. + +Итак, если мы всё же сравниваем массивы с помощью `==`, то они никогда не будут одинаковыми, если только мы не сравним две переменные, которые ссылаются на один и тот же массив + +Например: +```js run +alert( [] == [] ); // false +alert( [0] == [0] ); // false +``` + +Технически эти массивы являются разными объектами. Так что они не равны. Оператор `==` не выполняет поэлементное сравнение. + +Сравнение с примитивами также может дать, казалось бы, странные результаты: + +```js run +alert( 0 == [] ); // true + +alert('0' == [] ); // false +``` + +Здесь, в обоих случаях, мы сравниваем примитив с объектом массива. Таким образом, массив `[]` преобразуется в примитив с целью сравнения и становится пустой строкой `''`. + +Затем продолжается процесс сравнения с примитивами, как описано в главе : + +```js run +// после того, как [] был преобразован в '' +alert( 0 == '' ); // true, так как '' преобразуется в число 0 + +alert('0' == '' ); // false, нет преобразования типов, разные строки +``` + +Так как же сравнить массивы? + +Это просто: не используйте оператор `==`. Вместо этого сравните их по элементам в цикле или используя методы итерации, описанные в следующей главе. + + ## Итого Массив – это особый тип объекта, предназначенный для работы с упорядоченным набором элементов. -- Объявление: +Объявление: - ```js - // квадратные скобки (обычно) - let arr = [item1, item2...]; +```js +// квадратные скобки (обычно) +let arr = [item1, item2...]; - // new Array (очень редко) - let arr = new Array(item1, item2...); - ``` +// new Array (очень редко) +let arr = new Array(item1, item2...); +``` - Вызов `new Array(number)` создаёт массив с заданной длиной, но без элементов. +Вызов `new Array(number)` создаёт массив с заданной длиной, но без элементов. - Свойство `length` отражает длину массива или, если точнее, его последний цифровой индекс плюс один. Длина корректируется автоматически методами массива. - Если мы уменьшаем `length` вручную, массив укорачивается. -Мы можем использовать массив как двустороннюю очередь, используя следующие операции: +Получение элементов: + +- Мы можем получить элемент по его индексу, например `arr[0]`. +- Также мы можем использовать метод `at(i)` для получения элементов с отрицательным индексом, для отрицательных значений `i`, он отступает от конца массива. В остальном он работает так же, как `arr[i]`, если `i >= 0`. + +Мы можем использовать массив как двустороннюю очередь, используя следующие операции: - `push(...items)`добавляет `items` в конец массива. - `pop()` удаляет элемент в конце массива и возвращает его. - `shift()` удаляет элемент в начале массива и возвращает его. -- `unshift(...items)` добавляет `items` в начало массива. +- `unshift(...items)` добавляет `items` в начало массива. Чтобы пройтись по элементам массива: - `for (let i=0; ipushshift \ No newline at end of file +pushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/stack.svg b/1-js/05-data-types/04-array/stack.svg index dcc600e714..ae0c474d0f 100644 --- a/1-js/05-data-types/04-array/stack.svg +++ b/1-js/05-data-types/04-array/stack.svg @@ -1 +1 @@ -pushpop \ No newline at end of file +pushpop \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/task.md b/1-js/05-data-types/05-array-methods/11-array-unique/task.md index 9b49312e4f..ddfdae46c5 100644 --- a/1-js/05-data-types/05-array-methods/11-array-unique/task.md +++ b/1-js/05-data-types/05-array-methods/11-array-unique/task.md @@ -21,3 +21,4 @@ let strings = ["кришна", "кришна", "харе", "харе", alert( unique(strings) ); // кришна, харе, :-O ``` + diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js new file mode 100644 index 0000000000..8dea23a06a --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js @@ -0,0 +1,6 @@ +function groupById(array) { + return array.reduce((obj, value) => { + obj[value.id] = value; + return obj; + }, {}) +} diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js new file mode 100644 index 0000000000..e48ba138db --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js @@ -0,0 +1,21 @@ +describe("groupById", function() { + + it("creates an object grouped by id", function() { + let users = [ + {id: 'john', name: "John Smith", age: 20}, + {id: 'ann', name: "Ann Smith", age: 24}, + {id: 'pete', name: "Pete Peterson", age: 31}, + ]; + + assert.deepEqual(groupById(users), { + john: {id: 'john', name: "John Smith", age: 20}, + ann: {id: 'ann', name: "Ann Smith", age: 24}, + pete: {id: 'pete', name: "Pete Peterson", age: 31}, + }); + }); + + it("works with an empty array", function() { + users = []; + assert.deepEqual(groupById(users), {}); + }); +}); diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md b/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md @@ -0,0 +1 @@ + diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/task.md b/1-js/05-data-types/05-array-methods/12-reduce-object/task.md new file mode 100644 index 0000000000..9164e39e33 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/task.md @@ -0,0 +1,37 @@ +importance: 4 + +--- + +# Создайте объект с ключами из массива + +Допустим, мы получили массив пользователей в виде `{id:..., name:..., age:... }`. + +Создайте функцию `groupById(arr)`, которая создаст из него объект с `id` в качестве ключа и элементами массива в качестве значений. + +Например: + +```js +let users = [ + {id: 'john', name: "John Smith", age: 20}, + {id: 'ann', name: "Ann Smith", age: 24}, + {id: 'pete', name: "Pete Peterson", age: 31}, +]; + +let usersById = groupById(users); + +/* +после вызова у нас должно получиться: + +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`. + +Используйте метод `.reduce` в решении. diff --git a/1-js/05-data-types/05-array-methods/2-filter-range/task.md b/1-js/05-data-types/05-array-methods/2-filter-range/task.md index 512e786e97..3fb9dd3be9 100644 --- a/1-js/05-data-types/05-array-methods/2-filter-range/task.md +++ b/1-js/05-data-types/05-array-methods/2-filter-range/task.md @@ -4,7 +4,7 @@ importance: 4 # Фильтрация по диапазону -Напишите функцию `filterRange(arr, a, b)`, которая принимает массив `arr`, ищет в нём элементы между `a` и `b` и отдаёт массив этих элементов. +Напишите функцию `filterRange(arr, a, b)`, которая принимает массив `arr`, ищет элементы со значениями больше или равными `a` и меньше или равными `b` и возвращает результат в виде массива. Функция должна возвращать новый массив и не изменять исходный. diff --git a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js index db32d9a115..241b74c6ed 100644 --- a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js @@ -4,13 +4,13 @@ describe("filterRangeInPlace", function() { let arr = [5, 3, 8, 1]; - filterRangeInPlace(arr, 1, 4); + filterRangeInPlace(arr, 2, 5); - assert.deepEqual(arr, [3, 1]); + assert.deepEqual(arr, [5, 3]); }); it("doesn't return anything", function() { assert.isUndefined(filterRangeInPlace([1,2,3], 1, 4)); }); -}); \ No newline at end of file +}); diff --git a/1-js/05-data-types/05-array-methods/4-sort-back/task.md b/1-js/05-data-types/05-array-methods/4-sort-back/task.md index ad6f4190e8..382c207219 100644 --- a/1-js/05-data-types/05-array-methods/4-sort-back/task.md +++ b/1-js/05-data-types/05-array-methods/4-sort-back/task.md @@ -2,12 +2,12 @@ importance: 4 --- -# Сортировать в обратном порядке +# Сортировать в порядке по убыванию ```js let arr = [5, 2, 1, -10, 8]; -// ... ваш код для сортировки в обратном порядке +// ... ваш код для сортировки по убыванию alert( arr ); // 8, 5, 2, 1, -10 ``` diff --git a/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md b/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md index a0f6cbf5ca..85c8fd622c 100644 --- a/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md +++ b/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md @@ -1,5 +1,3 @@ - Обратите внимание, как хранятся методы. Они просто добавляются к внутреннему объекту. - Все тесты и числовые преобразования выполняются в методе `calculate`. В будущем он может быть расширен для поддержки более сложных выражений. - -[js src="_js/solution.js"] diff --git a/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md b/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md index 854e1ef4b8..554f38bc15 100644 --- a/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md +++ b/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md @@ -1,6 +1,6 @@ ```js run no-beautify function sortByAge(arr) { - arr.sort((a, b) => a.age > b.age ? 1 : -1); + arr.sort((a, b) => a.age - b.age); } let vasya = { name: "Вася", age: 25 }; diff --git a/1-js/05-data-types/05-array-methods/article.md b/1-js/05-data-types/05-array-methods/article.md index 296ce93b6e..5d1354e183 100644 --- a/1-js/05-data-types/05-array-methods/article.md +++ b/1-js/05-data-types/05-array-methods/article.md @@ -30,23 +30,23 @@ alert( arr[1] ); // undefined alert( arr.length ); // 3 ``` -Вроде бы, элемент и был удалён, но при проверке оказывается, что массив всё ещё имеет 3 элемента `arr.length == 3`. +Элемент был удалён, но в массиве всё ещё три элемента, мы можем увидеть, что `arr.length == 3`. -Это нормально, потому что всё, что делает `delete obj.key` - это удаляет значение с данным ключом `key`. Это нормально для объектов, но для массивов мы обычно хотим, чтобы оставшиеся элементы сдвинулись и заняли освободившееся место. Мы ждём, что массив станет короче. +Это естественно, потому что `delete obj.key` удаляет значение по ключу `key`. Это всё, что он делает. Хорошо для объектов. Но для массивов мы обычно хотим, чтобы оставшиеся элементы сдвинулись и заняли освободившееся место. Мы ждём, что массив станет короче. -Поэтому для этого нужно использовать специальные методы. +Поэтому нужно использовать специальные методы. -Метод [arr.splice(str)](mdn:js/Array/splice) – это универсальный "швейцарский нож" для работы с массивами. Умеет всё: добавлять, удалять и заменять элементы. +Метод [arr.splice](mdn:js/Array/splice) – это универсальный «швейцарский нож» для работы с массивами. Умеет всё: добавлять, удалять и заменять элементы. -Его синтаксис: +Синтаксис: ```js -arr.splice(index[, deleteCount, elem1, ..., elemN]) +arr.splice(start[, deleteCount, elem1, ..., elemN]) ``` -Он начинает с позиции `index`, удаляет `deleteCount` элементов и вставляет `elem1, ..., elemN` на их место. Возвращает массив из удалённых элементов. +Он изменяет `arr` начиная с индекса `start`: удаляет `deleteCount` элементов и затем вставляет `elem1, ..., elemN` на их место. Возвращает массив из удалённых элементов. -Этот метод проще всего понять, рассмотрев примеры. +Этот метод легко понять, рассмотрев примеры. Начнём с удаления: @@ -54,13 +54,13 @@ arr.splice(index[, deleteCount, elem1, ..., elemN]) let arr = ["Я", "изучаю", "JavaScript"]; *!* -arr.splice(1, 1); // начиная с позиции 1, удалить 1 элемент +arr.splice(1, 1); // начиная с индекса 1, удалить 1 элемент */!* alert( arr ); // осталось ["Я", "JavaScript"] ``` -Легко, правда? Начиная с позиции `1`, он убрал `1` элемент. +Легко, правда? Начиная с индекса `1`, он убрал `1` элемент. В следующем примере мы удалим 3 элемента и заменим их двумя другими. @@ -89,7 +89,7 @@ alert( removed ); // "Я", "изучаю" <-- массив из удалённы ```js run let arr = ["Я", "изучаю", "JavaScript"]; -// с позиции 2 +// с индекса 2 // удалить 0 элементов // вставить "сложный", "язык" arr.splice(2, 0, "сложный", "язык"); @@ -98,7 +98,7 @@ alert( arr ); // "Я", "изучаю", "сложный", "язык", "JavaScript ``` ````smart header="Отрицательные индексы разрешены" -В этом и в других методах массива допускается использование отрицательного индекса. Он позволяет начать отсчёт элементов с конца, как тут: +В этом и в других методах массива допускается использование отрицательных индексов. Они определяют позицию с конца массива, как тут: ```js run let arr = [1, 2, 5]; @@ -116,13 +116,13 @@ alert( arr ); // 1,2,3,4,5 Метод [arr.slice](mdn:js/Array/slice) намного проще, чем похожий на него `arr.splice`. -Его синтаксис: +Синтаксис: ```js arr.slice([start], [end]) ``` -Он возвращает новый массив, в который копирует элементы, начиная с индекса `start` и до `end` (не включая `end`). Оба индекса `start` и `end` могут быть отрицательными. В таком случае отсчёт будет осуществляться с конца массива. +Он возвращает новый массив, в который копирует все элементы с индекса `start` до `end` (не включая `end`). `start` и `end` могут быть отрицательными, в этом случае отсчёт позиции будет вестись с конца массива. Это похоже на строковый метод `str.slice`, но вместо подстрок возвращает подмассивы. @@ -131,18 +131,18 @@ arr.slice([start], [end]) ```js run let arr = ["t", "e", "s", "t"]; -alert( arr.slice(1, 3) ); // e,s (копирует с 1 по 3) +alert( arr.slice(1, 3) ); // e,s (копирует с 1 до 3) alert( arr.slice(-2) ); // s,t (копирует с -2 до конца) ``` -Можно вызвать `slice` и вообще без аргументов: `arr.slice()` создаёт копию массива `arr`. Это часто используют, чтобы создать копию массива для дальнейших преобразований, которые не должны менять исходный массив. +Можно вызвать `slice` без аргументов: `arr.slice()` создаёт копию `arr`. Это часто используют, чтобы создать копию массива для дальнейших преобразований, которые не должны менять исходный массив. ### concat Метод [arr.concat](mdn:js/Array/concat) создаёт новый массив, в который копирует данные из других массивов и дополнительные значения. -Его синтаксис: +Синтаксис: ```js arr.concat(arg1, arg2...) @@ -150,9 +150,9 @@ arr.concat(arg1, arg2...) Он принимает любое количество аргументов, которые могут быть как массивами, так и простыми значениями. -В результате мы получаем новый массив, включающий в себя элементы из `arr`, а также `arg1`, `arg2` и так далее... +В результате -- новый массив, включающий в себя элементы из `arr`, затем `arg1`, `arg2` и так далее. -Если аргумент `argN` -- массив, то все его элементы копируются. Иначе скопируется сам аргумент. +Если аргумент `argN` -- массив, то копируются все его элементы. Иначе копируется сам аргумент. Например: @@ -169,7 +169,7 @@ alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6 ``` -Обычно он просто копирует элементы из массивов. Другие объекты, даже если они выглядят как массивы, добавляются как есть: +Обычно он копирует только элементы из массивов. Другие объекты, даже если они выглядят как массивы, добавляются как есть: ```js run let arr = [1, 2]; @@ -182,9 +182,7 @@ let arrayLike = { alert( arr.concat(arrayLike) ); // 1,2,[object Object] ``` -...Но если объект имеет специальное свойство `Symbol.isConcatSpreadable`, то он обрабатывается `concat` как массив: вместо него добавляются его числовые свойства. - -Для корректной обработки в объекте должны быть числовые свойства и `length`: +...Но если массивоподобный объект имеет специальное свойство `Symbol.isConcatSpreadable`, то он обрабатывается как массив, с помощью `concat`: вместо него добавляются его элементы: ```js run let arr = [1, 2]; @@ -205,7 +203,7 @@ alert( arr.concat(arrayLike) ); // 1,2,что-то,ещё Метод [arr.forEach](mdn:js/Array/forEach) позволяет запускать функцию для каждого элемента массива. -Его синтаксис: +Синтаксис: ```js arr.forEach(function(item, index, array) { // ... делать что-то с item @@ -216,31 +214,32 @@ arr.forEach(function(item, index, array) { ```js run // Вызов alert для каждого элемента -["Bilbo", "Gandalf", "Nazgul"].forEach(alert); +["Бильбо", "Гэндальф", "Назгул"].forEach(alert); ``` -А этот вдобавок расскажет и о своей позиции в массиве: +А этот вдобавок расскажет и о позиции элемента в целевом массиве: ```js run -["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { - alert(`${item} имеет позицию ${index} в ${array}`); +["Бильбо", "Гэндальф", "Назгул"].forEach((item, index, array) => { + alert(`У ${item} индекс ${index} в ${array}`); }); ``` -Результат функции (если она вообще что-то возвращает) отбрасывается и игнорируется. +Результат функции (если она что-то возвращает) отбрасывается и игнорируется. ## Поиск в массиве -Далее рассмотрим методы, которые помогут найти что-нибудь в массиве. +Теперь рассмотрим методы поиска в массиве. ### indexOf/lastIndexOf и includes -Методы [arr.indexOf](mdn:js/Array/indexOf), [arr.lastIndexOf](mdn:js/Array/lastIndexOf) и [arr.includes](mdn:js/Array/includes) имеют одинаковый синтаксис и делают по сути то же самое, что и их строковые аналоги, но работают с элементами вместо символов: +У методов [arr.indexOf](mdn:js/Array/indexOf) и [arr.includes](mdn:js/Array/includes) одинаковый синтаксис и они делают по сути то же самое, что и их строковые аналоги, но работают с элементами вместо символов: + +- `arr.indexOf(item, from)` ищет `item` начиная с индекса `from` и возвращает номер индекса, на котором был найден искомый элемент, в противном случае `-1`. +- `arr.includes(item, from)` ищет `item` начиная с индекса `from` и возвращает `true`, если поиск успешен. -- `arr.indexOf(item, from)` ищет `item`, начиная с индекса `from`, и возвращает индекс, на котором был найден искомый элемент, в противном случае `-1`. -- `arr.lastIndexOf(item, from)` -- то же самое, но ищет справа налево. -- `arr.includes(item, from)` -- ищет `item`, начиная с индекса `from`, и возвращает `true`, если поиск успешен. +Обычно эти методы используются только с одним аргументом: искомым `item`. По умолчанию поиск ведется с начала. Например: @@ -254,25 +253,37 @@ alert( arr.indexOf(null) ); // -1 alert( arr.includes(1) ); // true ``` -Обратите внимание, что методы используют строгое сравнение `===`. Таким образом, если мы ищем `false`, он находит именно `false`, а не ноль. +Пожалуйста, обратите внимание, что методы используют строгое сравнение `===`. Таким образом, если мы ищем `false`, он находит именно `false`, а не ноль. -Если мы хотим проверить наличие элемента, и нет необходимости знать его точный индекс, тогда предпочтительным является `arr.includes`. +Если мы хотим проверить наличие элемента в массиве и нет необходимости знать его индекс, предпочтительно использовать `arr.includes`. -Кроме того, очень незначительным отличием `includes` является то, что он правильно обрабатывает `NaN` в отличие от `indexOf/lastIndexOf`: +Метод [arr.lastIndexOf](mdn:js/Array/lastIndexOf) похож на `indexOf`, но ищет справа налево. + +```js run +let fruits = ['Яблоко', 'Апельсин', 'Яблоко'] + +alert( fruits.indexOf('Яблоко') ); // 0 (первый 'Яблоко') +alert( fruits.lastIndexOf('Яблоко') ); // 2 (последний 'Яблоко') +``` + +````smart header="Метод `includes` правильно обрабатывает `NaN`" +Незначительная, но заслуживающая внимания особенность `includes` -- он правильно обрабатывает `NaN`, в отличие от `indexOf`: ```js run const arr = [NaN]; -alert( arr.indexOf(NaN) ); // -1 (должен быть 0, но === проверка на равенство не работает для NaN) +alert( arr.indexOf(NaN) ); // -1 (неверно, должен быть 0) alert( arr.includes(NaN) );// true (верно) ``` +Это связано с тем, что `includes` был добавлен в JavaScript гораздо позже и использует более современный алгоритм сравнения. +```` -### find и findIndex +### find и findIndex/findLastIndex Представьте, что у нас есть массив объектов. Как нам найти объект с определённым условием? Здесь пригодится метод [arr.find](mdn:js/Array/find). -Его синтаксис таков: +Синтаксис: ```js let result = arr.find(function(item, index, array) { // если true - возвращается текущий элемент и перебор прерывается @@ -288,7 +299,7 @@ let result = arr.find(function(item, index, array) { Если функция возвращает `true`, поиск прерывается и возвращается `item`. Если ничего не найдено, возвращается `undefined`. -Например, у нас есть массив пользователей, каждый из которых имеет поля `id` и `name`. Попробуем найти того, кто с `id == 1`: +Например, у нас есть массив пользователей, каждый из которых имеет поля `id` и `name`. Найдем пользователя с `id == 1`: ```js run let users = [ @@ -302,23 +313,42 @@ let user = users.find(item => item.id == 1); alert(user.name); // Вася ``` -В реальной жизни массивы объектов - обычное дело, поэтому метод `find` крайне полезен. +В реальной жизни массивы объектов -- обычное дело, поэтому метод `find` крайне полезен. + +Обратите внимание, что в данном примере мы передаём `find` функцию `item => item.id == 1` с одним аргументом. Это типично, другие аргументы этой функции используются редко. + +У метода [arr.findIndex](mdn:js/Array/findIndex) такой же синтаксис, но он возвращает индекс, на котором был найден элемент, а не сам элемент. Значение `-1` возвращается, если ничего не найдено. + +Метод [arr.findLastIndex](mdn:js/Array/findLastIndex) похож на `findIndex`, но ищет справа налево, наподобие `lastIndexOf`. + +Например: + +```js run +let users = [ + {id: 1, name: "Вася"}, + {id: 2, name: "Петя"}, + {id: 3, name: "Маша"}, + {id: 4, name: "Вася"} +]; -Обратите внимание, что в данном примере мы передаём `find` функцию `item => item.id == 1`, с одним аргументом. Это типично, дополнительные аргументы этой функции используются редко. +// Найти индекс первого Васи +alert(users.findIndex(user => user.name == 'Вася')); // 0 -Метод [arr.findIndex](mdn:js/Array/findIndex) - по сути, то же самое, но возвращает индекс, на котором был найден элемент, а не сам элемент, и `-1`, если ничего не найдено. +// Найти индекс последнего Васи +alert(users.findLastIndex(user => user.name == 'Вася')); // 3 +``` ### filter -Метод `find` ищет один (первый попавшийся) элемент, на котором функция-колбэк вернёт `true`. +Метод `find` ищет один (первый) элемент, который заставит функцию вернуть `true`. -На тот случай, если найденных элементов может быть много, предусмотрен метод [arr.filter(fn)](mdn:js/Array/filter). +Если найденных элементов может быть много, можно использовать [arr.filter(fn)](mdn:js/Array/filter). -Синтаксис этого метода схож с `find`, но `filter` возвращает массив из всех подходящих элементов: +Синтаксис схож с `find`, но `filter` возвращает массив из всех подходящих элементов: ```js let results = arr.filter(function(item, index, array) { - // если true - элемент добавляется к результату, и перебор продолжается + // если `true` -- элемент добавляется к results и перебор продолжается // возвращается пустой массив в случае, если ничего не найдено }); ``` @@ -342,7 +372,6 @@ alert(someUsers.length); // 2 Перейдём к методам преобразования и упорядочения массива. - ### map Метод [arr.map](mdn:js/Array/map) является одним из наиболее полезных и часто используемых. @@ -360,15 +389,15 @@ let result = arr.map(function(item, index, array) { Например, здесь мы преобразуем каждый элемент в его длину: ```js run -let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); -alert(lengths); // 5,7,6 +let lengths = ["Бильбо", "Гэндальф", "Назгул"].map(item => item.length); +alert(lengths); // 6,8,6 ``` ### sort(fn) Вызов [arr.sort()](mdn:js/Array/sort) сортирует массив *на месте*, меняя в нём порядок элементов. -Он возвращает отсортированный массив, но обычно возвращаемое значение игнорируется, так как изменяется сам `arr`. +Он также возвращает отсортированный массив, но обычно возвращаемое значение игнорируется, так как изменяется сам `arr`. Например: @@ -383,7 +412,7 @@ alert( arr ); // *!*1, 15, 2*/!* Не заметили ничего странного в этом примере? -Порядок стал `1, 15, 2`. Это неправильно! Но почему? +Порядок стал `1, 15, 2`. Это неправильно. Но почему? **По умолчанию элементы сортируются как строки.** @@ -392,6 +421,7 @@ alert( arr ); // *!*1, 15, 2*/!* Чтобы использовать наш собственный порядок сортировки, нам нужно предоставить функцию в качестве аргумента `arr.sort()`. Функция должна для пары значений возвращать: + ```js function compare(a, b) { if (a > b) return 1; // если первое значение больше второго @@ -420,23 +450,23 @@ alert(arr); // *!*1, 2, 15*/!* Теперь всё работает как надо. -Давайте возьмём паузу и подумаем, что же происходит. Упомянутый ранее массив `arr` может быть массивом чего угодно, верно? Он может содержать числа, строки, объекты или что-то ещё. У нас есть набор *каких-то элементов*. Чтобы отсортировать его, нам нужна *функция, определяющая порядок*, которая знает, как сравнивать его элементы. По умолчанию элементы сортируются как строки. +Сделаем отступление и подумаем, что происходит. `arr` может быть массивом чего угодно, верно? Он может содержать числа, строки, объекты или что-то ещё. У нас есть набор *каких-то элементов*. Чтобы отсортировать его, нам нужна *упорядочивающая функция*, которая знает, как сравнивать его элементы. По умолчанию элементы сортируются как строки. -Метод `arr.sort(fn)` реализует общий алгоритм сортировки. Нам не нужно заботиться о том, как он работает внутри (в большинстве случаев это оптимизированная [быстрая сортировка](https://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0)). Она проходится по массиву, сравнивает его элементы с помощью предоставленной функции и переупорядочивает их. Всё, что остаётся нам, это предоставить `fn`, которая делает это сравнение. +Метод `arr.sort(fn)` реализует общий алгоритм сортировки. Нам не нужно заботиться о том, как он работает внутри (в большинстве случаев это оптимизированная [быстрая сортировка](https://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0) или [Timsort](https://ru.wikipedia.org/wiki/Timsort)). Она проходится по массиву, сравнивает его элементы с помощью предоставленной функции и переупорядочивает их. Всё, что нам нужно, -- предоставить `fn`, которая делает сравнение. Кстати, если мы когда-нибудь захотим узнать, какие элементы сравниваются -- ничто не мешает нам вывести их на экран: ```js run [1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); + return a - b; }); ``` -В процессе работы алгоритм может сравнивать элемент с другими по нескольку раз, но он старается сделать как можно меньше сравнений. - +В процессе работы алгоритм может сравнивать элемент со множеством других, но он старается сделать как можно меньше сравнений. ````smart header="Функция сравнения может вернуть любое число" -На самом деле от функции сравнения требуется любое положительное число, чтобы сказать "больше", и отрицательное число, чтобы сказать "меньше". +На самом деле от функции сравнения требуется любое положительное число, чтобы сказать «больше», и отрицательное число, чтобы сказать «меньше». Это позволяет писать более короткие функции: @@ -450,7 +480,7 @@ alert(arr); // *!*1, 2, 15*/!* ```` ````smart header="Лучше использовать стрелочные функции" -Помните [стрелочные функции](info:function-expressions-arrows#arrow-functions)? Можно использовать их здесь для того, чтобы сортировка выглядела более аккуратной: +Помните [стрелочные функции](info:arrow-functions-basics)? Можно использовать их здесь для того, чтобы сортировка выглядела более аккуратной: ```js arr.sort( (a, b) => a - b ); @@ -459,6 +489,22 @@ arr.sort( (a, b) => a - b ); Будет работать точно так же, как и более длинная версия выше. ```` +````smart header="Используйте `localeCompare` для строк" +Помните алгоритм сравнения [строк](info:string#correct-comparisons)? По умолчанию, он сравнивает буквы по их кодам. + +Для многих алфавитов лучше использовать метод `str.localeCompare`, для правильной сортировки букв, таких как `Ö`. + +Например, отсортируем несколько стран на немецком языке: + +```js run +let countries = ['Österreich', 'Andorra', 'Vietnam']; + +alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (неправильно) + +alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (правильно!) +``` +```` + ### reverse Метод [arr.reverse](mdn:js/Array/reverse) меняет порядок элементов в `arr` на обратный. @@ -524,36 +570,38 @@ alert( str ); // Вася;Петя;Маша ### reduce/reduceRight -Если нам нужно перебрать массив -- мы можем использовать `forEach`, `for` или `for..of`. +Когда нам нужно перебрать массив -- мы можем использовать `forEach`, `for` или `for..of`. -Если нам нужно перебрать массив и вернуть данные для каждого элемента -- мы используем `map`. +Когда нам нужно перебрать массив и вернуть данные для каждого элемента -- мы можем использовать `map`. -Методы [arr.reduce](mdn:js/Array/reduce) и [arr.reduceRight](mdn:js/Array/reduceRight) похожи на методы выше, но они немного сложнее. Они используются для вычисления какого-нибудь единого значения на основе всего массива. +Методы [arr.reduce](mdn:js/Array/reduce) и [arr.reduceRight](mdn:js/Array/reduceRight) похожи на методы выше, но они немного сложнее. Они используются для вычисления единого значения на основе всего массива. Синтаксис: ```js -let value = arr.reduce(function(previousValue, item, index, array) { +let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]); ``` -Функция применяется по очереди ко всем элементам массива и "переносит" свой результат на следующий вызов. +Функция применяется по очереди ко всем элементам массива и «переносит» свой результат на следующий вызов. Аргументы: -- `previousValue` -- результат предыдущего вызова этой функции, равен `initial` при первом вызове (если передан `initial`), +- `accumulator` -- результат предыдущего вызова этой функции, равен `initial` при первом вызове (если передан `initial`), - `item` -- очередной элемент массива, -- `index` -- его индекс, +- `index` -- его позиция, - `array` -- сам массив. -При вызове функции результат её вызова на предыдущем элементе массива передаётся как первый аргумент. +При вызове функции результат её предыдущего вызова передаётся на следующий вызов в качестве первого аргумента. + +Так, первый аргумент является по сути аккумулятором, который хранит объединённый результат всех предыдущих вызовов функции. По окончании он становится результатом `reduce`. -Звучит сложновато, но всё становится проще, если думать о первом аргументе как "аккумулирующем" результат предыдущих вызовов функции. По окончании он становится результатом `reduce`. +Звучит сложно? Этот метод проще всего понять на примере. -Тут мы получим сумму всех элементов массива всего одной строкой: +Тут мы получим сумму всех элементов массива одной строкой: ```js run let arr = [1, 2, 3, 4, 5]; @@ -563,19 +611,19 @@ let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15 ``` -Здесь мы использовали наиболее распространённый вариант `reduce`, который использует только 2 аргумента. +Функция, переданная в `reduce`, использует только два аргумента, этого обычно достаточно. -Давайте детальнее разберём, как он работает. +Разберём детально как это работает. 1. При первом запуске `sum` равен `initial` (последний аргумент `reduce`), то есть `0`, а `current` -- первый элемент массива, равный `1`. Таким образом, результат функции равен `1`. -2. При втором запуске `sum = 1`, и к нему мы добавляем второй элемент массива (`2`). +2. При втором запуске `sum = 1`, к нему мы добавляем второй элемент массива (`2`) и возвращаем. 3. При третьем запуске `sum = 3`, к которому мы добавляем следующий элемент, и так далее... Поток вычислений получается такой: ![](reduce.svg) -В виде таблицы, где каждая строка –- вызов функции на очередном элементе массива: +Или в виде таблицы, где каждая строка показывает вызов функции на очередном элементе массива: | |`sum`|`current`|`result`| |---|-----|---------|---------| @@ -585,7 +633,6 @@ alert(result); // 15 |четвёртый вызов|`6`|`4`|`10`| |пятый вызов|`10`|`5`|`15`| - Здесь отчётливо видно, как результат предыдущего вызова передаётся в первый аргумент следующего. Мы также можем опустить начальное значение: @@ -615,15 +662,13 @@ let arr = []; arr.reduce((sum, current) => sum + current); ``` - Поэтому рекомендуется всегда указывать начальное значение. Метод [arr.reduceRight](mdn:js/Array/reduceRight) работает аналогично, но проходит по массиву справа налево. - ## Array.isArray -Массивы не образуют отдельный тип языка. Они основаны на объектах. +Массивы не образуют отдельный тип данных. Они основаны на объектах. Поэтому `typeof` не может отличить простой объект от массива: @@ -640,7 +685,7 @@ alert(Array.isArray({})); // false alert(Array.isArray([])); // true ``` -## Большинство методов поддерживают "thisArg" +## Большинство методов поддерживают «thisArg» Почти все методы массива, которые вызывают функции -- такие как `find`, `filter`, `map`, за исключением метода `sort`, принимают необязательный параметр `thisArg`. @@ -653,12 +698,12 @@ arr.find(func, thisArg); arr.filter(func, thisArg); arr.map(func, thisArg); // ... -// thisArg - это необязательный последний аргумент +// thisArg -- необязательный последний аргумент ``` Значение параметра `thisArg` становится `this` для `func`. -Например, вот тут мы используем метод объекта `army` как фильтр, и `thisArg` передаёт ему контекст: +Например, тут мы используем метод объекта `army` как фильтр, и `thisArg` передаёт ему контекст: ```js run let army = { @@ -695,12 +740,12 @@ alert(soldiers[1].age); // 23 Шпаргалка по методам массива: - Для добавления/удаления элементов: - - `push (...items)` -- добавляет элементы в конец, + - `push(...items)` -- добавляет элементы в конец, - `pop()` -- извлекает элемент с конца, - `shift()` -- извлекает элемент с начала, - `unshift(...items)` -- добавляет элементы в начало. - - `splice(pos, deleteCount, ...items)` -- начиная с индекса `pos`, удаляет `deleteCount` элементов и вставляет `items`. - - `slice(start, end)` -- создаёт новый массив, копируя в него элементы с позиции `start` до `end` (не включая `end`). + - `splice(pos, deleteCount, ...items)` -- начиная с индекса `pos` удаляет `deleteCount` элементов и вставляет `items`. + - `slice(start, end)` -- создаёт новый массив, копируя в него элементы с индекса `start` до `end` (не включая `end`). - `concat(...items)` -- возвращает новый массив: копирует все члены текущего массива и добавляет к нему `items`. Если какой-то из `items` является массивом, тогда берутся его элементы. - Для поиска среди элементов: @@ -714,25 +759,39 @@ alert(soldiers[1].age); // 23 - Для преобразования массива: - `map(func)` -- создаёт новый массив из результатов вызова `func` для каждого элемента. - - `sort(func)` -- сортирует массив "на месте", а потом возвращает его. - - `reverse()` -- "на месте" меняет порядок следования элементов на противоположный и возвращает изменённый массив. + - `sort(func)` -- сортирует массив «на месте», а потом возвращает его. + - `reverse()` -- «на месте» меняет порядок следования элементов на противоположный и возвращает изменённый массив. - `split/join` -- преобразует строку в массив и обратно. - - `reduce(func, initial)` -- вычисляет одно значение на основе всего массива, вызывая `func` для каждого элемента и передавая промежуточный результат между вызовами. + - `reduce/reduceRight(func, initial)` -- вычисляет одно значение на основе всего массива, вызывая `func` для каждого элемента и передавая промежуточный результат между вызовами. - Дополнительно: - `Array.isArray(arr)` проверяет, является ли `arr` массивом. -Обратите внимание, что методы `sort`, `reverse` и `splice` изменяют исходный массив. +Пожалуйста, обратите внимание, что методы `push`, `pop`, `shift`, `unshift`, `sort`, `reverse` и `splice` изменяют исходный массив. -Изученных нами методов достаточно в 99% случаев, но существуют и другие. +Эти методы -- самые используемые, их достаточно в 99% случаев. Но существуют и другие: - [arr.some(fn)](mdn:js/Array/some)/[arr.every(fn)](mdn:js/Array/every) проверяет массив. Функция `fn` вызывается для каждого элемента массива аналогично `map`. Если какие-либо/все результаты вызовов являются `true`, то метод возвращает `true`, иначе `false`. + Эти методы ведут себя примерно так же, как операторы `||` и `&&`: если `fn` возвращает истинное значение, `arr.some()` немедленно возвращает `true` и останавливает перебор остальных элементов; если `fn` возвращает ложное значение, `arr.every()` немедленно возвращает `false` и также прекращает перебор остальных элементов. + + Мы можем использовать `every` для сравнения массивов: + + ```js run + function arraysEqual(arr1, arr2) { + return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]); + } + + alert( arraysEqual([1, 2], [1, 2])); // true + ``` + - [arr.fill(value, start, end)](mdn:js/Array/fill) -- заполняет массив повторяющимися `value`, начиная с индекса `start` до `end`. -- [arr.copyWithin(target, start, end)](mdn:js/Array/copyWithin) -- копирует свои элементы, начиная со `start` и заканчивая `end`, в *собственную* позицию `target` (перезаписывает существующие). +- [arr.copyWithin(target, start, end)](mdn:js/Array/copyWithin) -- копирует свои элементы, начиная с позиции `start` и заканчивая `end`, в *себя*, на позицию `target` (перезаписывая существующие). + +- [arr.flat(depth)](mdn:js/Array/flat)/[arr.flatMap(fn)](mdn:js/Array/flatMap) создаёт новый плоский массив из многомерного массива. Полный список есть в [справочнике MDN](mdn:js/Array). diff --git a/1-js/05-data-types/05-array-methods/reduce.svg b/1-js/05-data-types/05-array-methods/reduce.svg index 47797bf7c2..180941dc29 100644 --- a/1-js/05-data-types/05-array-methods/reduce.svg +++ b/1-js/05-data-types/05-array-methods/reduce.svg @@ -1 +1 @@ -1sum 0 current 12sum 0+1 current 23sum 0+1+2 current 34sum 0+1+2+3 current 45sum 0+1+2+3+4 current 50+1+2+3+4+5 = 15 \ No newline at end of file +1sum 0 current 12sum 0+1 current 23sum 0+1+2 current 34sum 0+1+2+3 current 45sum 0+1+2+3+4 current 50+1+2+3+4+5 = 15 \ No newline at end of file diff --git a/1-js/05-data-types/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md index 8d32922bfd..cfd643aeb3 100644 --- a/1-js/05-data-types/06-iterable/article.md +++ b/1-js/05-data-types/06-iterable/article.md @@ -1,11 +1,11 @@ # Перебираемые объекты -*Перебираемые* (или *итерируемые*) объекты - это концепция, которая позволяет использовать любой объект в цикле `for..of`. +*Перебираемые* (или *итерируемые*) объекты - это обобщение массивов. Концепция, которая позволяет использовать любой объект в цикле `for..of`. Конечно же, сами массивы являются перебираемыми объектами. Но есть и много других встроенных перебираемых объектов, например, строки. -Если объект не является массивом, но представляет собой коллекцию каких-то элементов, то удобно использовать цикл `for..of` для их перебора, так что давайте посмотрим, как это сделать. +Если объект не является массивом, но представляет собой коллекцию каких-то элементов (список, набор), то удобно использовать цикл `for..of` для их перебора, так что давайте посмотрим, как это сделать. ## Symbol.iterator @@ -30,7 +30,7 @@ let range = { 1. Когда цикл `for..of` запускается, он вызывает этот метод один раз (или выдаёт ошибку, если метод не найден). Этот метод должен вернуть *итератор* -- объект с методом `next`. 2. Дальше `for..of` работает *только с этим возвращённым объектом*. 3. Когда `for..of` хочет получить следующее значение, он вызывает метод `next()` этого объекта. -4. Результат вызова `next()` должен иметь вид `{done: Boolean, value: any}`, где `done=true` означает, что итерация закончена, в противном случае `value` содержит очередное значение. +4. Результат вызова `next()` должен иметь вид `{done: Boolean, value: any}`, где `done=true` означает, что цикл завершён, в противном случае `value` содержит очередное значение. Вот полная реализация `range` с пояснениями: @@ -44,10 +44,11 @@ let range = { range[Symbol.iterator] = function() { // ...она возвращает объект итератора: - // 2. Далее, for..of работает только с этим итератором, запрашивая у него новые значения + // 2. Далее, for..of работает только с этим итератором, + // запрашивая у него новые значения return { current: this.from, - last: this.to, + last: this.to, // 3. next() вызывается на каждой итерации цикла for..of next() { @@ -72,7 +73,7 @@ for (let num of range) { - У самого `range` нет метода `next()`. - Вместо этого другой объект, так называемый "итератор", создаётся вызовом `range[Symbol.iterator]()`, и именно его `next()` генерирует значения. -Таким образом, итератор отделён от самого итерируемого объекта. +Таким образом, объект итератор отделён от самого итерируемого объекта. Технически мы можем объединить их и использовать сам `range` как итератор, чтобы упростить код. @@ -114,7 +115,6 @@ for (let num of range) { Конечно же, цикл `for..of` с таким итерируемым объектом будет бесконечным. Но мы всегда можем прервать его, используя `break`. ``` - ## Строка - перебираемый объект Среди встроенных перебираемых объектов наиболее широко используются массивы и строки. @@ -149,7 +149,9 @@ let str = "Hello"; // делает то же самое, что и // for (let char of str) alert(char); +*!* let iterator = str[Symbol.iterator](); +*/!* while (true) { let result = iterator.next(); @@ -215,13 +217,15 @@ alert(arr.pop()); // World (метод работает) То же самое происходит с итерируемым объектом: -```js -// range взят из примера выше +```js run +// range взят из примера в начале статьи + let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (преобразование массива через toString работает) ``` Полный синтаксис `Array.from` позволяет указать необязательную "трансформирующую" функцию: + ```js Array.from(obj[, mapFn, thisArg]) ``` @@ -230,8 +234,8 @@ Array.from(obj[, mapFn, thisArg]) Например: -```js -// range взят из примера выше +```js run +// range взят из примера в начале статьи // возводим каждое число в квадрат let arr = Array.from(range, num => num * num); @@ -267,7 +271,7 @@ for (let char of str) { alert(chars); ``` -...Но гораздо короче. +...Но гораздо короче. Мы можем даже создать `slice`, который поддерживает суррогатные пары: @@ -284,17 +288,16 @@ alert( slice(str, 1, 3) ); // 😂𩷶 alert( str.slice(1, 3) ); // мусор (две части различных суррогатных пар) ``` - ## Итого Объекты, которые можно использовать в цикле `for..of`, называются *итерируемыми*. - Технически итерируемые объекты должны иметь метод `Symbol.iterator`. - Результат вызова `obj[Symbol.iterator]` называется *итератором*. Он управляет процессом итерации. - - Итератор должен иметь метод `next()`, который возвращает объект `{done: Boolean, value: any}`, где `done:true` сигнализирует об окончании итерации, в противном случае `value` - следующее значение. + - Итератор должен иметь метод `next()`, который возвращает объект `{done: Boolean, value: any}`, где `done:true` сигнализирует об окончании процесса итерации, в противном случае `value` - следующее значение. - Метод `Symbol.iterator` автоматически вызывается циклом `for..of`, но можно вызвать его и напрямую. - Встроенные итерируемые объекты, такие как строки или массивы, также реализуют метод `Symbol.iterator`. -- Строковой итератор знает про суррогатные пары. +- Строковый итератор знает про суррогатные пары. Объекты, имеющие индексированные свойства и `length`, называются *псевдомассивами*. Они также могут иметь другие свойства и методы, но у них нет встроенных методов массивов. diff --git a/1-js/05-data-types/06-iterable/head.html b/1-js/05-data-types/06-iterable/head.html new file mode 100644 index 0000000000..14edae5dcd --- /dev/null +++ b/1-js/05-data-types/06-iterable/head.html @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/1-js/05-data-types/07-map-set/01-array-unique-map/task.md b/1-js/05-data-types/07-map-set/01-array-unique-map/task.md index 8d82f7ecfb..e8d2294bec 100644 --- a/1-js/05-data-types/07-map-set/01-array-unique-map/task.md +++ b/1-js/05-data-types/07-map-set/01-array-unique-map/task.md @@ -19,7 +19,7 @@ let values = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; -alert( unique(values) ); // Hare, Krishna, :-O +alert( unique(values) ); // Hare,Krishna,:-O ``` P.S. Здесь мы используем строки, но значения могут быть любого типа. diff --git a/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md index ca2e9723ec..aaec82046f 100644 --- a/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md +++ b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md @@ -45,7 +45,7 @@ let sorted = arr[i] // PAN Два разных слова `'PAN'` и `'nap'` принимают ту же самую форму после сортировки букв -- `'anp'`. -Следующая линия помещает слово в объект Map: +Следующая строчка кода помещает слово в объект Map: ```js map.set(sorted, word); diff --git a/1-js/05-data-types/07-map-set/02-filter-anagrams/task.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/task.md index dfd764e684..8cfbbab8ca 100644 --- a/1-js/05-data-types/07-map-set/02-filter-anagrams/task.md +++ b/1-js/05-data-types/07-map-set/02-filter-anagrams/task.md @@ -21,7 +21,7 @@ cheaters - hectares - teachers ```js let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; -alert( aclean(arr) ); // "nap,teachers,ear" or "PAN,cheaters,era" +alert( aclean(arr) ); // "nap,teachers,ear" или "PAN,cheaters,era" ``` Из каждой группы анаграмм должно остаться только одно слово, не важно какое. diff --git a/1-js/05-data-types/07-map-set/article.md b/1-js/05-data-types/07-map-set/article.md index 0f6ca1084d..efff26e8f9 100644 --- a/1-js/05-data-types/07-map-set/article.md +++ b/1-js/05-data-types/07-map-set/article.md @@ -10,17 +10,17 @@ ## Map -[Map](mdn:js/Map) -- это коллекция ключ/значение, как и `Object`. Но основное отличие в том, что `Map` позволяет использовать ключи любого типа. +[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) -- это коллекция ключ/значение, как и `Object`. Но основное отличие в том, что `Map` позволяет использовать ключи любого типа. Методы и свойства: -- `new Map()` -- создаёт коллекцию. -- `map.set(key, value)` -- записывает по ключу `key` значение `value`. -- `map.get(key)` -- возвращает значение по ключу или `undefined`, если ключ `key` отсутствует. -- `map.has(key)` -- возвращает `true`, если ключ `key` присутствует в коллекции, иначе `false`. -- `map.delete(key)` -- удаляет элемент по ключу `key`. -- `map.clear()` -- очищает коллекцию от всех элементов. -- `map.size` -- возвращает текущее количество элементов. +- [`new Map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) -- создаёт коллекцию. +- [`map.set(key, value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set) -- записывает по ключу `key` значение `value`. +- [`map.get(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) -- возвращает значение по ключу или `undefined`, если ключ `key` отсутствует. +- [`map.has(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) -- возвращает `true`, если ключ `key` присутствует в коллекции, иначе `false`. +- [`map.delete(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete) -- удаляет элемент (пару "ключ/значение") по ключу `key`. +- [`map.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) -- очищает коллекцию от всех элементов. +- [`map.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size) -- возвращает текущее количество элементов. Например: @@ -41,6 +41,12 @@ alert(map.size); // 3 Как мы видим, в отличие от объектов, ключи не были приведены к строкам. Можно использовать любые типы данных для ключей. +```smart header="`map[key]` это не совсем правильный способ использования `Map`" +Хотя `map[key]` также работает, например, мы можем установить `map[key] = 2`, в этом случае`map` рассматривался бы как обычный JavaScript объект, таким образом это ведёт ко всем соответствующим ограничениям (только строки/символьные ключи и так далее). + +Поэтому нам следует использовать методы `map`: `set`, `get` и так далее. +``` + **Map может использовать объекты в качестве ключей.** Например: @@ -57,24 +63,26 @@ visitsCountMap.set(john, 123); alert(visitsCountMap.get(john)); // 123 ``` -Использование объектов в качестве ключей -- это одна из известных и часто применяемых возможностей объекта `Map`. При строковых ключах обычный объект `Object` может подойти, но для ключей-объектов - уже нет. +Использование объектов в качестве ключей - одна из наиболее заметных и важных функций `Map`. Это то что невозможно для `Object`. Строка в качестве ключа в `Object` - это нормально, но мы не можем использовать другой `Object` в качестве ключа в `Object`. -Попробуем заменить `Map` на `Object` в примере выше: +Давайте попробуем заменить `Map` на `Object`: ```js run let john = { name: "John" }; +let ben = { name: "Ben" }; let visitsCountObj = {}; // попробуем использовать объект -visitsCountObj[john] = 123; // возьмём объект john как ключ +visitsCountObj[ben] = 234; // пробуем использовать объект ben в качестве ключа +visitsCountObj[john] = 123; // пробуем использовать объект john в качестве ключа, при этом объект ben будет замещён *!* -// Вот как это было записано! -alert( visitsCountObj["[object Object]"] ); // 123 +// Вот что там было записано! +alert( visitsCountObj["[object Object]"] ); // 123 */!* ``` -Так как `visitsCountObj` -- это объект, то все ключи он автоматически преобразует к строке, в итоге получился строковой ключ `"[object Object]"`. Это не то, чего мы хотим. +Так как `visitsCountObj` является объектом, он преобразует все ключи `Object`, такие как `john` и `ben`, в одну и ту же строку `"[object Object]"`. Это определенно не то, чего мы хотим. ```smart header="Как объект `Map` сравнивает ключи" Чтобы сравнивать ключи, объект `Map` использует алгоритм [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). Это почти такое же сравнение, что и `===`, с той лишь разницей, что `NaN` считается равным `NaN`. Так что `NaN` также может использоваться в качестве ключа. @@ -96,9 +104,9 @@ map.set("1", "str1") Для перебора коллекции `Map` есть 3 метода: -- `map.keys()` -- возвращает итерируемый объект по ключам, -- `map.values()` -- возвращает итерируемый объект по значениям, -- `map.entries()` -- возвращает итерируемый объект по парам вида `[ключ, значение]`, этот вариант используется по умолчанию в `for..of`. +- [`map.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys) -- возвращает итерируемый объект по ключам, +- [`map.values()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values) -- возвращает итерируемый объект по значениям, +- [`map.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) -- возвращает итерируемый объект по парам вида `[ключ, значение]`, этот вариант используется по умолчанию в `for..of`. Например: @@ -157,7 +165,7 @@ alert( map.get('1') ); // str1 Так что мы можем создать `Map` из обычного объекта следующим образом: -```js +```js run let obj = { name: "John", age: 30 @@ -166,11 +174,12 @@ let obj = { *!* let map = new Map(Object.entries(obj)); */!* + +alert( map.get('name') ); // John ``` Здесь `Object.entries` возвращает массив пар ключ-значение: `[ ["name","John"], ["age", 30] ]`. Это именно то, что нужно для создания `Map`. - ## Object.fromEntries: Object из Map Мы только что видели, как создать `Map` из обычного объекта при помощи `Object.entries(obj)`. @@ -184,7 +193,7 @@ let prices = Object.fromEntries([ ['meat', 4] ]); -// now prices = { banana: 1, orange: 2, meat: 4 } +// prices = { banana: 1, orange: 2, meat: 4 } alert(prices.orange); // 2 ``` @@ -202,7 +211,7 @@ map.set('orange', 2); map.set('meat', 4); *!* -let obj = Object.fromEntries(map.entries()); // make a plain object (*) +let obj = Object.fromEntries(map.entries()); // создаём обычный объект (*) */!* // готово! @@ -211,7 +220,7 @@ let obj = Object.fromEntries(map.entries()); // make a plain object (*) alert(obj.orange); // 2 ``` -Вызов `map.entries()` возвращает массив пар ключ/значение, как раз в нужном формате для `Object.fromEntries`. +Вызов `map.entries()` возвращает итерируемый объект пар ключ/значение, как раз в нужном формате для `Object.fromEntries`. Мы могли бы написать строку `(*)` ещё короче: @@ -223,16 +232,16 @@ let obj = Object.fromEntries(map); // убрать .entries() ## Set -Объект `Set` -- это особый вид коллекции: "множество" значений (без ключей), где каждое значение может появляться только один раз. +Объект [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) -- это особый вид коллекции: "множество" значений (без ключей), где каждое значение может появляться только один раз. Его основные методы это: -- `new Set(iterable)` -- создаёт `Set`, и если в качестве аргумента был предоставлен итерируемый объект (обычно это массив), то копирует его значения в новый `Set`. -- `set.add(value)` -- добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект `set`. -- `set.delete(value)` -- удаляет значение, возвращает `true` если `value` было в множестве на момент вызова, иначе `false`. -- `set.has(value)` -- возвращает `true`, если значение присутствует в множестве, иначе `false`. -- `set.clear()` -- удаляет все имеющиеся значения. -- `set.size` -- возвращает количество элементов в множестве. +- [`new Set(iterable)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) -- создаёт `Set`, и если в качестве аргумента был предоставлен итерируемый объект (обычно это массив), то копирует его значения в новый `Set`. +- [`set.add(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add) -- добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект `set`. +- [`set.delete(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete) -- удаляет значение, возвращает `true`, если `value` было в множестве на момент вызова, иначе `false`. +- [`set.has(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) -- возвращает `true`, если значение присутствует в множестве, иначе `false`. +- [`set.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear) -- удаляет все имеющиеся значения. +- [`set.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/size) -- возвращает количество элементов в множестве. Основная "изюминка" - это то, что при повторных вызовах `set.add()` с одним и тем же значением ничего не происходит, за счёт этого как раз и получается, что каждое значение появляется один раз. @@ -285,38 +294,38 @@ set.forEach((value, valueAgain, set) => { `Set` имеет те же встроенные методы, что и `Map`: -- `set.keys()` -- возвращает перебираемый объект для значений, -- `set.values()` -- то же самое, что и `set.keys()`, присутствует для обратной совместимости с `Map`, -- `set.entries()` -- возвращает перебираемый объект для пар вида `[значение, значение]`, присутствует для обратной совместимости с `Map`. +- [`set.values()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values) -- возвращает перебираемый объект для значений, +- [`set.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/keys) -- то же самое, что и `set.values()`, присутствует для обратной совместимости с `Map`, +- [`set.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries) -- возвращает перебираемый объект для пар вида `[значение, значение]`, присутствует для обратной совместимости с `Map`. ## Итого -`Map` -- коллекция пар ключ-значение. +[`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) -- коллекция пар ключ-значение. Методы и свойства: -- `new Map([iterable])` -- создаёт коллекцию, можно указать перебираемый объект (обычно массив) из пар `[ключ,значение]` для инициализации. -- `map.set(key, value)` -- записывает по ключу `key` значение `value`. -- `map.get(key)` -- возвращает значение по ключу или `undefined`, если ключ `key` отсутствует. -- `map.has(key)` -- возвращает `true`, если ключ `key` присутствует в коллекции, иначе `false`. -- `map.delete(key)` -- удаляет элемент по ключу `key`. -- `map.clear()` -- очищает коллекцию от всех элементов. -- `map.size` -- возвращает текущее количество элементов. +- [`new Map([iterable])`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) -- создаёт коллекцию, можно указать перебираемый объект (обычно массив) из пар `[ключ,значение]` для инициализации. +- [`map.set(key, value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set) -- записывает по ключу `key` значение `value`. +- [`map.get(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) -- возвращает значение по ключу или `undefined`, если ключ `key` отсутствует. +- [`map.has(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) -- возвращает `true`, если ключ `key` присутствует в коллекции, иначе `false`. +- [`map.delete(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete) -- удаляет элемент по ключу `key`. +- [`map.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) -- очищает коллекцию от всех элементов. +- [`map.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size) -- возвращает текущее количество элементов. Отличия от обычного объекта `Object`: - Что угодно может быть ключом, в том числе и объекты. - Есть дополнительные методы, свойство `size`. -`Set` -- коллекция уникальных значений, так называемое "множество". +[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) -- коллекция уникальных значений, так называемое "множество". Методы и свойства: -- `new Set([iterable])` -- создаёт `Set`, можно указать перебираемый объект со значениями для инициализации. -- `set.add(value)` -- добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект `set`. -- `set.delete(value)` -- удаляет значение, возвращает `true` если `value` было в множестве на момент вызова, иначе `false`. -- `set.has(value)` -- возвращает `true`, если значение присутствует в множестве, иначе `false`. -- `set.clear()` -- удаляет все имеющиеся значения. -- `set.size` -- возвращает количество элементов в множестве. +- [`new Set(iterable)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) -- создаёт `Set`, можно указать перебираемый объект со значениями для инициализации. +- [`set.add(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add) -- добавляет значение (если оно уже есть, то ничего не делает), возвращает тот же объект `set`. +- [`set.delete(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete) -- удаляет значение, возвращает `true` если `value` было в множестве на момент вызова, иначе `false`. +- [`set.has(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) -- возвращает `true`, если значение присутствует в множестве, иначе `false`. +- [`set.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear) -- удаляет все имеющиеся значения. +- [`set.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/size) -- возвращает количество элементов в множестве. Перебор `Map` и `Set` всегда осуществляется в порядке добавления элементов, так что нельзя сказать, что это -- неупорядоченные коллекции, но поменять порядок элементов или получить элемент напрямую по его номеру нельзя. diff --git a/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md index 09e0415000..215aa27cfd 100644 --- a/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md +++ b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md @@ -14,10 +14,10 @@ let messages = [ ]; ``` -У вас есть к ним доступ, но управление этим массивом происходит где-то ещё. Добавляются новые сообщений и удаляются старые, и вы не знаете в какой момент это может произойти. +У вас есть к ним доступ, но управление этим массивом происходит где-то ещё. Добавляются новые сообщения и удаляются старые, и вы не знаете в какой момент это может произойти. Имея такую вводную информацию, решите, какую структуру данных вы могли бы использовать для ответа на вопрос "было ли сообщение прочитано?". Структура должна быть подходящей, чтобы можно было однозначно сказать, было ли прочитано это сообщение для каждого объекта сообщения. P.S. Когда сообщение удаляется из массива `messages`, оно должно также исчезать из структуры данных. -P.P.S. Нам не следует модифицировать сами объекты сообщений, добавлять туда свойства. Если сообщения принадлежат какиму-то другому коду, то это может привести к плохим последствиям. +P.P.S. Нам не следует модифицировать сами объекты сообщений, добавлять туда свойства. Если сообщения принадлежат какому-то другому коду, то это может привести к плохим последствиям. diff --git a/1-js/05-data-types/08-weakmap-weakset/article.md b/1-js/05-data-types/08-weakmap-weakset/article.md index 846142feb5..4c7f717587 100644 --- a/1-js/05-data-types/08-weakmap-weakset/article.md +++ b/1-js/05-data-types/08-weakmap-weakset/article.md @@ -4,10 +4,11 @@ Как мы знаем из главы , движок JavaScript хранит значения в памяти до тех пор, пока они достижимы (то есть, эти значения могут быть использованы). Например: + ```js let john = { name: "John" }; -// объект доступен, переменная john -- это ссылка на него +// объект доступен, переменная john — это ссылка на него // перепишем ссылку john = null; @@ -88,7 +89,7 @@ john = null; // перезаписываем ссылку на объект // объект john удалён из памяти! ``` -Сравните это поведение с поведением обычной `Map`, пример которого был приведён ранее. Теперь `john` существует только как ключ в `WeakMap` и может быть удалён оттуда автоматически. +Сравните это поведение с поведением обычного `Map`, пример которого был приведён ранее. Теперь `john` существует только как ключ в `WeakMap` и может быть удалён оттуда автоматически. `WeakMap` не поддерживает перебор и методы `keys()`, `values()`, `entries()`, так что нет способа взять все ключи или значения из неё. @@ -107,7 +108,6 @@ john = null; // перезаписываем ссылку на объект ## Пример: дополнительные данные - В основном, `WeakMap` используется в качестве *дополнительного хранилища данных*. Если мы работаем с объектом, который "принадлежит" другому коду, может быть даже сторонней библиотеке, и хотим сохранить у себя какие-то данные для него, которые должны существовать лишь пока существует этот объект, то `WeakMap` - как раз то, что нужно. @@ -121,7 +121,7 @@ weakMap.set(john, "секретные документы"); Давайте рассмотрим один пример. -Предположим, у нас есть код, которые ведёт учёт посещений для пользователей. Информация хранится в коллекции `Map`: объект, представляющий пользователя, является ключом, а количество визитов -- значением. Когда пользователь нас покидает (его объект удаляется сборщиком мусора), то больше нет смысла хранить соответствующий счётчик посещений. +Предположим, у нас есть код, который ведёт учёт посещений для пользователей. Информация хранится в коллекции `Map`: объект, представляющий пользователя, является ключом, а количество визитов -- значением. Когда пользователь нас покидает (его объект удаляется сборщиком мусора), то больше нет смысла хранить соответствующий счётчик посещений. Вот пример реализации счётчика посещений с использованием `Map`: @@ -142,8 +142,7 @@ function countUser(user) { // 📁 main.js let john = { name: "John" }; -countUser(john); //ведём подсчёт посещений -countUser(john); +countUser(john); // ведём подсчёт посещений // пользователь покинул нас john = null; @@ -275,7 +274,7 @@ alert(visitedSet.has(mary)); // false john = null; -// структура данных visitedSet будет очищена автоматически +// структура данных visitedSet будет очищена автоматически (объект john будет удалён из visitedSet) ``` Наиболее значительным ограничением `WeakMap` и `WeakSet` является то, что их нельзя перебрать или взять всё содержимое. Это может доставлять неудобства, но не мешает `WeakMap/WeakSet` выполнять их главную задачу -- быть дополнительным хранилищем данных для объектов, управляемых из каких-то других мест в коде. diff --git a/1-js/05-data-types/09-keys-values-entries/article.md b/1-js/05-data-types/09-keys-values-entries/article.md index eebae73f31..fd5d88041b 100644 --- a/1-js/05-data-types/09-keys-values-entries/article.md +++ b/1-js/05-data-types/09-keys-values-entries/article.md @@ -11,7 +11,7 @@ - `Map` - `Set` -- `Array` (кроме `arr.values()`) +- `Array` Простые объекты также можно перебирать похожими методами, но синтаксис немного отличается. @@ -90,7 +90,7 @@ let prices = { *!* let doublePrices = Object.fromEntries( - // преобразовать в массив, затем map, затем fromEntries обратно объект + // преобразовать в массив, затем map, затем fromEntries (обратно в объект) Object.entries(prices).map(([key, value]) => [key, value * 2]) ); */!* diff --git a/1-js/05-data-types/10-destructuring-assignment/article.md b/1-js/05-data-types/10-destructuring-assignment/article.md index bd8e1d1ebb..0983421102 100644 --- a/1-js/05-data-types/10-destructuring-assignment/article.md +++ b/1-js/05-data-types/10-destructuring-assignment/article.md @@ -1,24 +1,29 @@ + # Деструктурирующее присваивание В JavaScript есть две чаще всего используемые структуры данных - это `Object` и `Array`. -Объекты позволяют нам создавать одну сущность, которая хранит элементы данных по ключам, а массивы - хранить упорядоченные коллекции данных. +- Объекты позволяют нам создавать одну сущность, которая хранит элементы данных по ключам. +- Массивы позволяют нам собирать элементы данных в упорядоченный список. Но когда мы передаём их в функцию, то ей может понадобиться не объект/массив целиком, а элементы по отдельности. -*Деструктурирующее присваивание* -- это специальный синтаксис, который позволяет нам "распаковать" массивы или объекты в кучу переменных, так как иногда они более удобны. Деструктуризация также прекрасно работает со сложными функциями, которые имеют много параметров, значений по умолчанию и так далее. +*Деструктурирующее присваивание* -- это специальный синтаксис, который позволяет нам "распаковать" массивы или объекты в несколько переменных, так как иногда они более удобны. + +Деструктуризация также прекрасно работает со сложными функциями, которые имеют много параметров, значений по умолчанию и так далее. Скоро мы увидим это. ## Деструктуризация массива -Пример деструктуризации массива: +Вот пример деструктуризации массива на переменные: ```js // у нас есть массив с именем и фамилией -let arr = ["Ilya", "Kantor"] +let arr = ["Ilya", "Kantor"]; *!* // деструктурирующее присваивание -// записывает firstName=arr[0], surname=arr[1] +// записывает firstName = arr[0] +// и surname = arr[1] let [firstName, surname] = arr; */!* @@ -32,12 +37,17 @@ alert(surname); // Kantor ```js let [firstName, surname] = "Ilya Kantor".split(' '); +alert(firstName); // Ilya +alert(surname); // Kantor ``` +Как вы можете видеть, синтаксис прост. Однако есть несколько странных моментов. Давайте посмотрим больше примеров, чтобы лучше понять это. + ````smart header="\"Деструктуризация\" не означает \"разрушение\"." "Деструктурирующее присваивание" не уничтожает массив. Оно вообще ничего не делает с правой частью присваивания, его задача - только скопировать нужные значения в переменные. Это просто короткий вариант записи: + ```js // let [firstName, surname] = arr; let firstName = arr[0]; @@ -46,7 +56,7 @@ let surname = arr[1]; ```` ````smart header="Пропускайте элементы, используя запятые" -Ненужные элементы массива также могут быть отброшены через запятую: +Нежелательные элементы массива также могут быть отброшены с помощью дополнительной запятой: ```js run *!* @@ -61,33 +71,30 @@ alert( title ); // Consul ```` ````smart header="Работает с любым перебираемым объектом с правой стороны" - ...На самом деле мы можем использовать любой перебираемый объект, не только массивы: ```js -let [a, b, c] = "abc"; // ["a", "b", "c"] +let [a, b, c] = "abc"; let [one, two, three] = new Set([1, 2, 3]); ``` - ```` ````smart header="Присваивайте чему угодно с левой стороны" - Мы можем использовать что угодно "присваивающее" с левой стороны. Например, можно присвоить свойству объекта: + ```js run let user = {}; [user.name, user.surname] = "Ilya Kantor".split(' '); alert(user.name); // Ilya +alert(user.surname); // Kantor ``` - ```` ````smart header="Цикл с .entries()" - В предыдущей главе мы видели метод [Object.entries(obj)](mdn:js/Object/entries). Мы можем использовать его с деструктуризацией для цикличного перебора ключей и значений объекта: @@ -102,7 +109,7 @@ let user = { *!* for (let [key, value] of Object.entries(user)) { */!* - alert(`${key}:${value}`); // name:John, then age:30 + alert(`${key}:${value}`); // name:John, затем age:30 } ``` @@ -114,31 +121,69 @@ user.set("name", "John"); user.set("age", "30"); *!* +// Map перебирает как пары [ключ, значение], что очень удобно для деструктурирования for (let [key, value] of user) { */!* - alert(`${key}:${value}`); // name:John, then age:30 + alert(`${key}:${value}`); // name:John, затем age:30 } ``` ```` + +````smart header="Трюк обмена переменных" +Существует хорошо известный трюк для обмена значений двух переменных с использованием деструктурирующего присваивания: + +```js run +let guest = "Jane"; +let admin = "Pete"; + +// Давайте поменяем местами значения: сделаем guest = "Pete", а admin = "Jane" +*!* +[guest, admin] = [admin, guest]; +*/!* + +alert(`${guest} ${admin}`); // Pete Jane (успешно заменено!) +``` + +Здесь мы создаём временный массив из двух переменных и немедленно деструктурируем его в порядке замены. + +Таким образом, мы можем поменять местами даже более двух переменных. +```` + ### Остаточные параметры "..." -Если мы хотим не просто получить первые значения, но и собрать все остальные, то мы можем добавить ещё один параметр, который получает остальные значения, используя оператор "остаточные параметры" -- троеточие (`"..."`): +Обычно, если массив длиннее, чем список слева, "лишние" элементы опускаются. + +Например, здесь берутся только первые два элемента, а остальные просто игнорируются: ```js run -let [name1, name2, *!*...rest*/!*] = ["Julius", "Caesar", *!*"Consul", "of the Roman Republic"*/!*]; +let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; alert(name1); // Julius alert(name2); // Caesar +// Дальнейшие элементы нигде не присваиваются +``` + +Если мы хотим не просто получить первые значения, но и собрать все остальные, то мы можем добавить ещё один параметр, который получает остальные значения, используя оператор "остаточные параметры" -- троеточие (`"..."`): + +```js run +let [name1, name2, *!*...rest*/!*] = ["Julius", "Caesar", *!*"Consul", "of the Roman Republic"*/!*]; *!* -// Обратите внимание, что `rest` является массивом. +// rest это массив элементов, начиная с 3-го alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2 */!* ``` -Переменная `rest` является массивом из оставшихся элементов. Вместо `rest` можно использовать любое другое название переменной, просто убедитесь, что перед переменной есть три точки и она стоит на последнем месте в деструктурирующем присваивании. +Переменная `rest` является массивом из оставшихся элементов. + +Вместо `rest` можно использовать любое другое название переменной, просто убедитесь, что перед переменной есть три точки и она стоит на последнем месте в деструктурирующем присваивании. + +```js run +let [name1, name2, *!*...titles*/!*] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; +// теперь titles = ["Consul", "of the Roman Republic"] +``` ### Значения по умолчанию @@ -153,7 +198,7 @@ alert(firstName); // undefined alert(surname); // undefined ``` -Если нам необходимо указать значения по умолчанию, то мы можем использовать `=`: +Если мы хотим, чтобы значение "по умолчанию" заменило отсутствующее, мы можем указать его с помощью `=`: ```js run *!* @@ -167,7 +212,7 @@ alert(surname); // Anonymous (значение по умолчанию) Значения по умолчанию могут быть гораздо более сложными выражениями или даже функциями. Они выполняются, только если значения отсутствуют. -Например, здесь мы используем функцию `prompt` для указания двух значений по умолчанию. Но она будет запущена только для отсутствующего значения: +Например, здесь мы используем функцию `prompt` для указания двух значений по умолчанию. ```js run // prompt запустится только для surname @@ -177,7 +222,7 @@ alert(name); // Julius (из массива) alert(surname); // результат prompt ``` - +Обратите внимание, `prompt` будет запущен только для пропущенного значения (`surname`). ## Деструктуризация объекта @@ -209,7 +254,9 @@ alert(width); // 100 alert(height); // 200 ``` -Свойства `options.title`, `options.width` и `options.height` присваиваются соответствующим переменным. Порядок не имеет значения. Вот так - тоже работает: +Свойства `options.title`, `options.width` и `options.height` присваиваются соответствующим переменным. + +Порядок не имеет значения. Вот так - тоже работает: ```js // изменён порядок в let {...} @@ -480,8 +527,9 @@ showMenu(options); ``` Полный синтаксис - такой же, как для деструктурирующего присваивания: + ```js -function({ +function showMenu({ incomingProperty: varName = defaultValue ... }) @@ -513,16 +561,19 @@ showMenu(); // Menu 100 200 - Деструктуризация позволяет разбивать объект или массив на переменные при присвоении. - Полный синтаксис для объекта: + ```js - let {prop : varName = default, ...rest} = object + let {prop : varName = defaultValue, ...rest} = object ``` + + Cвойство `prop` объекта `object` здесь должно быть присвоено переменной `varName`. Если в объекте отсутствует такое свойство, переменной `varName` присваивается значение по умолчанию. Свойства, которые не были упомянуты, копируются в объект `rest`. - Полный синтаксис для массива: ```js - let [item1 = default, item2, ...rest] = array + let [item1 = defaultValue, item2, ...rest] = array ``` Первый элемент отправляется в `item1`; второй отправляется в `item2`, все остальные элементы попадают в массив `rest`. diff --git a/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg index 0b650020bc..8a1ff1a934 100644 --- a/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg +++ b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/05-data-types/11-date/8-format-date-relative/solution.md b/1-js/05-data-types/11-date/8-format-date-relative/solution.md index a08f8810bc..a98fe45c8a 100644 --- a/1-js/05-data-types/11-date/8-format-date-relative/solution.md +++ b/1-js/05-data-types/11-date/8-format-date-relative/solution.md @@ -4,7 +4,7 @@ function formatDate(date) { let diff = new Date() - date; // разница в миллисекундах - if (diff < 1000) { // меньше 1 минуты + if (diff < 1000) { // меньше 1 секунды return 'прямо сейчас'; } @@ -62,6 +62,8 @@ function formatDate(date) { year = year.toString().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 'прямо сейчас'; diff --git a/1-js/05-data-types/11-date/article.md b/1-js/05-data-types/11-date/article.md index 73396608c8..40402e0f92 100644 --- a/1-js/05-data-types/11-date/article.md +++ b/1-js/05-data-types/11-date/article.md @@ -33,6 +33,13 @@ Это -- легковесное численное представление даты. Из таймстампа всегда можно получить дату с помощью `new Date(timestamp)` и преобразовать существующий объект `Date` в таймстамп, используя метод `date.getTime()` (см. ниже). + Датам до 1 января 1970 будут соответствовать отрицательные таймстампы, например: + ```js run + // 31 декабря 1969 года + let Dec31_1969 = new Date(-24 * 3600 * 1000); + alert( Dec31_1969 ); + ``` + `new Date(datestring)` : Если аргумент всего один, и это строка, то из неё "прочитывается" дата. Алгоритм разбора - такой же, как в `Date.parse`, который мы рассмотрим позже. @@ -50,7 +57,7 @@ `new Date(year, month, date, hours, minutes, seconds, ms)` : Создать объект `Date` с заданными компонентами в местном часовом поясе. Обязательны только первые два аргумента. - - `year` должен состоять из четырёх цифр: значение `2013` корректно, `98` -- нет. + - `year` должен состоять из четырёх цифр. Для совместимости также принимаются 2 цифры и рассматриваются как `19xx`, к примеру, `98` здесь это тоже самое, что и `1998`, но настоятельно рекомендуется всегда использовать 4 цифры. - `month` начинается с `0` (январь) по `11` (декабрь). - Параметр `date` здесь представляет собой день месяца. Если параметр не задан, то принимается значение `1`. - Если параметры `hours/minutes/seconds/ms` отсутствуют, их значением становится `0`. @@ -62,7 +69,7 @@ new Date(2011, 0, 1); // то же самое, так как часы и проч. равны 0 ``` - Минимальная точность – 1 мс (1/1000 секунды): + Максимальная точность – 1 мс (до 1/1000 секунды): ```js run let date = new Date(2011, 0, 1, 2, 3, 4, 567); @@ -117,7 +124,7 @@ alert( date.getUTCHours() ); : Для заданной даты возвращает таймстамп - количество миллисекунд, прошедших с 1 января 1970 года UTC+0. [getTimezoneOffset()](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset) -: Возвращает разницу в минутах между местным часовым поясом и UTC: +: Возвращает разницу в минутах между UTC и местным часовым поясом: ```js run // если вы в часовом поясе UTC-1, то выводится 60 @@ -341,7 +348,7 @@ let time1 = 0; let time2 = 0; *!* -// bench(upperSlice) и bench(upperLoop) поочерёдно запускаются 10 раз +// bench(diffSubtract) и bench(diffGetTime) поочерёдно запускаются 10 раз for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); @@ -369,7 +376,7 @@ for (let i = 0; i < 10; i++) { ```warn header="Будьте осторожны с микробенчмарками" Современные интерпретаторы JavaScript выполняют множество оптимизаций. Они могут повлиять на результаты "искусственных тестов" по сравнению с "нормальным использованием", особенно если мы тестируем что-то очень маленькое, например, работу оператора или встроенной функции. Поэтому если хотите серьёзно понять производительность, пожалуйста, изучите, как работают интерпретаторы JavaScript. И тогда вам, вероятно, уже не понадобятся микробенчмарки. -Отличный набор статей о V8 можно найти на . +Отличный набор статей о V8 можно найти на . ``` ## Разбор строки с датой @@ -409,7 +416,7 @@ alert(date); - Счёт месяцев начинается с нуля (да, январь -- это нулевой месяц). - Дни недели в `getDay()` также отсчитываются с нуля, что соответствует воскресенью. - Объект `Date` самостоятельно корректируется при введении значений, выходящих за рамки допустимых. Это полезно для сложения/вычитания дней/месяцев/недель. -- Даты можно вычитать, и разность возвращается в миллисекундах. Так происходит, потому что при преобразовании в число объект `Date` становится таймстампом. +- Даты можно вычитать, их разность возвращается в миллисекундах. Так происходит, потому что при преобразовании в число объект `Date` становится таймстампом. - Используйте `Date.now()` для быстрого получения текущего времени в формате таймстампа. Учтите, что, в отличие от некоторых других систем, в JavaScript таймстамп в миллисекундах, а не в секундах. diff --git a/1-js/05-data-types/12-json/2-serialize-event-circular/solution.md b/1-js/05-data-types/12-json/2-serialize-event-circular/solution.md index 05e9365d39..0a6ebca77d 100644 --- a/1-js/05-data-types/12-json/2-serialize-event-circular/solution.md +++ b/1-js/05-data-types/12-json/2-serialize-event-circular/solution.md @@ -26,4 +26,6 @@ alert( JSON.stringify(meetup, function replacer(key, value) { */ ``` -Здесь нам также нужно проверить `key ==""`, чтобы исключить первый вызов, где значение `value` равно `meetup`. +Функция `replacer` будет вызвана для каждой пары `(key, value)`, и в первом вызове будет передан специальный «объект-обёртка»: `{"": meetup}`. + +Если мы реализуем только проверку `value == meetup`, то в результате получим `undefined`. Чтобы в первом вызове `replacer` не было удалено свойство, ссылающееся на `meetup`, нам также нужно добавить проверку `key != ""`. diff --git a/1-js/05-data-types/12-json/article.md b/1-js/05-data-types/12-json/article.md index 3507de1542..8865c342d7 100644 --- a/1-js/05-data-types/12-json/article.md +++ b/1-js/05-data-types/12-json/article.md @@ -27,7 +27,7 @@ alert(user); // {name: "John", age: 30} ## JSON.stringify -[JSON](https://ru.wikipedia.org/wiki/JSON) (JavaScript Object Notation) - это общий формат для представления значений и объектов. Его описание задокументировано в стандарте [RFC 4627](http://tools.ietf.org/html/rfc4627). Первоначально он был создан для JavaScript, но многие другие языки также имеют библиотеки, которые могут работать с ним. Таким образом, JSON легко использовать для обмена данными, когда клиент использует JavaScript, а сервер написан на Ruby/PHP/Java или любом другом языке. +[JSON](https://ru.wikipedia.org/wiki/JSON) (JavaScript Object Notation) - это общий формат для представления значений и объектов. Его описание задокументировано в стандарте [RFC 4627](https://datatracker.ietf.org/doc/html/rfc4627). Первоначально он был создан для JavaScript, но многие другие языки также имеют библиотеки, которые могут работать с ним. Таким образом, JSON легко использовать для обмена данными, когда клиент использует JavaScript, а сервер написан на Ruby/PHP/Java или любом другом языке. JavaScript предоставляет методы: @@ -104,7 +104,7 @@ JSON является независимой от языка специфика А именно: - Свойства-функции (методы). -- Символьные свойства. +- Символьные ключи и значения. - Свойства, содержащие `undefined`. ```js run @@ -275,6 +275,7 @@ name: John name: Alice place: [object Object] number: 23 +occupiedBy: [object Object] */ ``` @@ -327,7 +328,9 @@ alert(JSON.stringify(user, null, 2)); */ ``` -Параметр `space` применяется для логирования и красивого вывода. +Третьим аргументом также может быть строка. В этом случае строка будет использоваться для отступа вместо ряда пробелов. + +Параметр `space` применяется исключительно для логирования и красивого вывода. ## Пользовательский "toJSON" @@ -401,7 +404,7 @@ alert( JSON.stringify(meetup) ); Синтаксис: ```js -let value = JSON.parse(str, [reviver]); +let value = JSON.parse(str[, reviver]); ``` str @@ -439,15 +442,16 @@ JSON может быть настолько сложным, насколько let json = `{ *!*name*/!*: "John", // Ошибка: имя свойства без кавычек "surname": *!*'Smith'*/!*, // Ошибка: одинарные кавычки в значении (должны быть двойными) - *!*'isAdmin'*/!*: false // Ошибка: одинарные кавычки в ключе (должны быть двойными) - "birthday": *!*new Date(2000, 2, 3)*/!*, // Ошибка: не допускается конструктор "new", только значения. - "friends": [0,1,2,3] // Здесь всё в порядке + *!*'isAdmin'*/!*: false, // Ошибка: одинарные кавычки в ключе (должны быть двойными) + "birthday": *!*new Date(2000, 2, 3)*/!*, // Ошибка: не допускается конструктор "new", только значения + "gender": "male"*!* */!* // Ошибка: отсутствует запятая после непоследнего свойства + "friends": [0,1,2,3]*!*,*/!* // Ошибка: не должно быть запятой после последнего свойства }`; ``` Кроме того, JSON не поддерживает комментарии. Добавление комментария в JSON делает его недействительным. -Существует ещё один формат [JSON5](http://json5.org/), который поддерживает ключи без кавычек, комментарии и т.д. Но это самостоятельная библиотека, а не спецификация языка. +Существует ещё один формат [JSON5](https://json5.org/), который поддерживает ключи без кавычек, комментарии и т.д. Но это самостоятельная библиотека, а не спецификация языка. Обычный JSON настолько строг не потому, что его разработчики ленивы, а потому, что позволяет легко, надёжно и очень быстро реализовывать алгоритм кодирования и чтения. @@ -472,7 +476,7 @@ let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str); *!* -alert( meetup.date.getDate() ); // Error! +alert( meetup.date.getDate() ); // Ошибка! */!* ``` diff --git a/1-js/05-data-types/12-json/json-meetup.svg b/1-js/05-data-types/12-json/json-meetup.svg index 5dbb4a9abf..3fa32a261c 100644 --- a/1-js/05-data-types/12-json/json-meetup.svg +++ b/1-js/05-data-types/12-json/json-meetup.svg @@ -1 +1 @@ -number: 23title: "Conference"...placeoccupiedByparticipants \ No newline at end of file +number: 23title: "Conference"...placeoccupiedByparticipants \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md index 110b965293..7283fe945c 100644 --- a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md +++ b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md @@ -1,3 +1,4 @@ + Решение с помощью цикла: ```js run @@ -37,4 +38,4 @@ P.S. Надо ли говорить, что решение по формуле Вариант с циклом – второй по скорости. Он быстрее рекурсии, так как операций сложения столько же, но нет дополнительных вычислительных затрат на организацию вложенных вызовов. Поэтому рекурсия в данном случае работает медленнее всех. -P.P.S. Некоторые движки поддерживают оптимизацию "хвостового вызова": если рекурсивный вызов является самым последним в функции (как в `sumTo` выше), то внешней функции не нужно будет возобновлять выполнение и не нужно запоминать контекст его выполнения. В итоге требования к памяти снижаются, и сумма `sumTo(100000)` будет успешно вычислена. Но если JavaScript-движок не поддерживает это (большинство не поддерживают), будет ошибка: максимальный размер стека превышен, так как обычно существует ограничение на максимальный размер стека. +P.P.S. Некоторые движки поддерживают оптимизацию "хвостового вызова": если рекурсивный вызов является самым последним в функции, без каких-либо других вычислений, то внешней функции не нужно будет возобновлять выполнение и не нужно запоминать контекст его выполнения. В итоге требования к памяти снижаются. Но если JavaScript-движок не поддерживает это (большинство не поддерживают), будет ошибка: максимальный размер стека превышен, так как обычно существует ограничение на максимальный размер стека. diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg index 224735b63a..1313837f30 100644 --- a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg @@ -1 +1 @@ -fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file +fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/article.md b/1-js/06-advanced-functions/01-recursion/article.md index 5e29265545..4a8d323447 100644 --- a/1-js/06-advanced-functions/01-recursion/article.md +++ b/1-js/06-advanced-functions/01-recursion/article.md @@ -1,8 +1,9 @@ + # Рекурсия и стек Вернёмся к функциям и изучим их более подробно. -Наша первая тема будет *рекурсия*. +Нашей первой темой будет *рекурсия*. Если вы не новичок в программировании, то, возможно, уже знакомы с рекурсией и можете пропустить эту главу. @@ -61,7 +62,7 @@ pow(2, 4) = 16 if n==1 = x / pow(x, n) = - \ + \ else = x * pow(x, n - 1) ``` @@ -93,7 +94,7 @@ function pow(x, n) { ``` ```` -Общее количество вложенных вызовов (включая первый) называют *глубиной рекурсии*. В нашем случае она будет равна ровно `n`. +Общее количество вложенных вызовов (включая первый) называют *глубиной рекурсии*. В нашем случае она будет равна `n`. Максимальная глубина рекурсии ограничена движком JavaScript. Точно можно рассчитывать на 10000 вложенных вызовов, некоторые интерпретаторы допускают и больше, но для большинства из них 100000 вызовов -- за пределами возможностей. Существуют автоматические оптимизации, помогающие избежать переполнения стека вызовов ("оптимизация хвостовой рекурсии"), но они ещё не поддерживаются везде и работают только для простых случаев. @@ -278,7 +279,7 @@ function pow(x, n) { **Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.** -Но переделка рекурсии в цикл может быть нетривиальной, особенно когда в функции в зависимости от условий используются различные рекурсивные подвызовы, результаты которых объединяются, или когда ветвление более сложное. Оптимизация может быть ненужной и совершенно не стоящей усилий. +Но переделка рекурсии в цикл может быть нетривиальной, особенно когда в функции в зависимости от условий используются различные рекурсивные подвызовы, результаты которых объединяются, или когда ветвление более сложное. Оптимизация может быть ненужной и совершенно нестоящей усилий. Часто код с использованием рекурсии более короткий, лёгкий для понимания и поддержки. Оптимизация требуется не везде, как правило, нам важен хороший код, поэтому она и используется. @@ -401,9 +402,9 @@ alert(sumSalaries(company)); // 6700 Это снова рекурсивное определение. -Для лучшего понимания мы рассмотрим ещё одну рекурсивную структуру под названием "связанный список", которая в некоторых случаях может использоваться в качестве альтернативы массиву. +Для лучшего понимания мы рассмотрим ещё одну рекурсивную структуру под названием "связный список", которая в некоторых случаях может использоваться в качестве альтернативы массиву. -### Связанный список +### Связный список Представьте себе, что мы хотим хранить упорядоченный список объектов. @@ -417,11 +418,11 @@ let arr = [obj1, obj2, obj3]; Единственные структурные изменения, не требующие массовой переиндексации - это изменения, которые выполняются с конца массива: `arr.push/pop`. Таким образом, массив может быть довольно медленным для больших очередей, когда нам приходится работать с его началом. -Или же, если нам действительно нужны быстрые вставка/удаление, мы можем выбрать другую структуру данных, называемую [связанный список](https://ru.wikipedia.org/wiki/Связный_список). +Или же, если нам действительно нужны быстрые вставка/удаление, мы можем выбрать другую структуру данных, называемую [связный список](https://ru.wikipedia.org/wiki/Связный_список). -Элемент *связанного списка* определяется рекурсивно как объект с: +Элемент *связного списка* определяется рекурсивно как объект с: - `value`, -- `next` - свойство, ссылающееся на следующий *элемент связанного списка* или `null`, если это последний элемент. +- `next` - свойство, ссылающееся на следующий *элемент связного списка* или `null`, если это последний элемент. Пример: @@ -443,7 +444,7 @@ let list = { Графическое представление списка: -![связанный список](linked-list.svg) +![связный список](linked-list.svg) Альтернативный код для создания: @@ -452,6 +453,7 @@ let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; +list.next.next.next.next = null; ``` Здесь мы можем ещё лучше увидеть, что есть несколько объектов, каждый из которых имеет `value` и `next`, указывающий на соседа. Переменная `list` является первым объектом в цепочке, поэтому, следуя по указателям `next` из неё, мы можем попасть в любой элемент. @@ -463,7 +465,7 @@ let secondList = list.next.next; list.next.next = null; ``` -![разделение связанного списка](linked-list-split.svg) +![разделение связного списка](linked-list-split.svg) Для объединения: @@ -480,6 +482,7 @@ let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; +list.next.next.next.next = null; *!* // добавление нового элемента в список @@ -487,7 +490,7 @@ list = { value: "new item", next: list }; */!* ``` -![связанный список](linked-list-0.svg) +![связный список](linked-list-0.svg) Чтобы удалить элемент из середины списка, нужно изменить значение `next` предыдущего элемента: @@ -495,7 +498,7 @@ list = { value: "new item", next: list }; list.next = list.next.next; ``` -![связанный список](linked-list-remove-1.svg) +![связный список](linked-list-remove-1.svg) `list.next` перепрыгнуло с `1` на значение `2`. Значение `1` теперь исключено из цепочки. Если оно не хранится где-нибудь ещё, оно будет автоматически удалено из памяти. @@ -515,19 +518,20 @@ list.next = list.next.next; ## Итого Термины: + - *Рекурсия* - это термин в программировании, означающий вызов функцией самой себя. Рекурсивные функции могут быть использованы для элегантного решения определённых задач. Когда функция вызывает саму себя, это называется *шагом рекурсии*. *База* рекурсии - это такие аргументы функции, которые делают задачу настолько простой, что решение не требует дальнейших вложенных вызовов. - [Рекурсивно определяемая](https://en.wikipedia.org/wiki/Recursive_data_type) структура данных - это структура данных, которая может быть определена с использованием самой себя. - Например, связанный список может быть определён как структура данных, состоящая из объекта, содержащего ссылку на список (или null). + Например, связный список может быть определён как структура данных, состоящая из объекта, содержащего ссылку на список (или null). ```js list = { value, next -> list } ``` - Деревья, такие как дерево HTML-элементов или дерево отделов из этой главы, также являются рекурсивными: они разветвляются, и каждая ветвь может содержать другие ветви. + Деревья, такие как дерево HTML-элементов или дерево отделов из этой главы, также являются рекурсивными: у них есть ветви, и каждая ветвь может содержать другие ветви. Как мы видели в примере `sumSalary`, рекурсивные функции могут быть использованы для прохода по ним. diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0.svg b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg index f95e1bfcae..5d23c7a4c6 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-0.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg @@ -1 +1 @@ -value1nextvalue"new item"nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value1nextvalue"new item"nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg index 3828ec08c9..2f37449c47 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg @@ -1 +1 @@ -value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.svg b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg index 3db14be2f5..6c30721307 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-split.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg @@ -1 +1 @@ -value1nextvalue2nextvalue3nextvalue4nextnullnullsecondListlist \ No newline at end of file +value1nextvalue2nextvalue3nextvalue4nextnullnullsecondListlist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.svg b/1-js/06-advanced-functions/01-recursion/linked-list.svg index 6dd22939b4..c02744f390 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list.svg @@ -1 +1 @@ -value1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg index b0e6e54ac7..0901f76364 100644 --- a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg +++ b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg @@ -1 +1 @@ -pow(x,n)xx * pow(x, n-1)n == 1 ?ДаНетрекурсивный вызов до n==1 \ No newline at end of file +pow(x,n)xx * pow(x, n-1)n == 1 ?ДаНетрекурсивный вызов до n==1 \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg index 207271cbc4..bd874c5ba6 100644 --- a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg +++ b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md b/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md index 49baf99431..6aa76bddb9 100644 --- a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md +++ b/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md @@ -8,7 +8,7 @@ - `Object.assign(dest, src1, ..., srcN)` – копирует свойства из исходных объектов `src1..N` в целевой объект `dest`. - ...и так далее. -В этой главе мы узнаем, как сделать то же самое c нашими собственными функциями и как передавать таким функциям параметры в виде массива. +В этой главе мы узнаем, как сделать то же самое с нашими собственными функциями и как передавать таким функциям параметры в виде массива. ## Остаточные параметры (`...`) @@ -230,7 +230,7 @@ alert( Array.from(str) ); // П,р,и,в,е,т Как отличить их друг от друга: -- Если `...` располагается в конце списка аргументов функции, то это "остаточные параметры". Он собирает остальные неуказанные аргументы и делает из них массив. +- Если `...` располагается в конце списка параметров функции, то это "остаточные параметры". Он собирает остальные неуказанные аргументы и делает из них массив. - Если `...` встретился в вызове функции или где-либо ещё, то это "оператор расширения". Он извлекает элементы из массива. Полезно запомнить: diff --git a/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md new file mode 100644 index 0000000000..f1119ba08e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md @@ -0,0 +1,5 @@ +Ответ: **Pete**. + +Функция получает внешние переменные в том виде, в котором они находятся сейчас, она использует самые последние значения. + +Старые значения переменных нигде не сохраняются. Когда функция обращается к переменной, она берет текущее значение из своего или внешнего лексического окружения. diff --git a/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md new file mode 100644 index 0000000000..00d5f10099 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Учитывает ли функция последние изменения? + +Функция `sayHi` использует имя внешней переменной. Какое значение будет использоваться при выполнении функции? + +```js +let name = "John"; + +function sayHi() { + alert("Hi, " + name); +} + +name = "Pete"; + +sayHi(); // что будет показано: "John" или "Pete"? +``` + +Такие ситуации встречаются как при разработке для браузера, так и для сервера. Функция может быть назначена на выполнение позже, чем она была создана, например, после действия пользователя или сетевого запроса. + +Итак, вопрос: учитывает ли она последние изменения? diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/10-make-army/_js.view/solution.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/_js.view/solution.js rename to 1-js/06-advanced-functions/03-closure/10-make-army/_js.view/solution.js diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/source.js b/1-js/06-advanced-functions/03-closure/10-make-army/_js.view/source.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/_js.view/source.js rename to 1-js/06-advanced-functions/03-closure/10-make-army/_js.view/source.js diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/test.js b/1-js/06-advanced-functions/03-closure/10-make-army/_js.view/test.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/_js.view/test.js rename to 1-js/06-advanced-functions/03-closure/10-make-army/_js.view/test.js diff --git a/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-empty.svg b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-empty.svg new file mode 100644 index 0000000000..05a28069cf --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-empty.svg @@ -0,0 +1 @@ +outer<пусто>makeArmy() LexicalEnvironmentитерация while LexicalEnvironment<пусто><пусто><пусто>i: 10 diff --git a/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-for-fixed.svg b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-for-fixed.svg new file mode 100644 index 0000000000..0d144be083 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-for-fixed.svg @@ -0,0 +1 @@ +outermakeArmy() LexicalEnvironmentитерация for LexicalEnvironmenti: 0i: 1i: 2i: 10... diff --git a/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-while-fixed.svg b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-while-fixed.svg new file mode 100644 index 0000000000..0237830d83 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-while-fixed.svg @@ -0,0 +1 @@ +outerj: 0j: 1j: 2j: 10...makeArmy() LexicalEnvironmentитерация while LexicalEnvironment diff --git a/1-js/06-advanced-functions/03-closure/10-make-army/solution.md b/1-js/06-advanced-functions/03-closure/10-make-army/solution.md new file mode 100644 index 0000000000..fe79b85d8c --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/solution.md @@ -0,0 +1,128 @@ + +Давайте посмотрим, что происходит внутри `makeArmy`, и решение станет очевидным. + +1. Она создаёт пустой массив `shooters`: + + ```js + let shooters = []; + ``` +2. В цикле заполняет его `shooters.push(function...)`. + + Каждый элемент -- это функция, так что получится такой массив: + + ```js no-beautify + 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` из своего внешнего лексического окружения. + + Какое будет значение у `i`? + + Если мы посмотрим в исходный код: + + ```js + function makeArmy() { + ... + let i = 0; + while (i < 10) { + let shooter = function() { // функция shooter + alert( i ); // должна выводить порядковый номер + }; + shooters.push(shooter); // и добавлять стрелка в массив + i++; + } + ... + } + ``` + + ...Мы увидим, что оно живёт в лексическом окружении, связанном с текущим вызовом `makeArmy()`. Но, когда вызывается `army[5]()`, `makeArmy` уже завершила свою работу, и последнее значение `i`: `10` (конец цикла `while`). + + Как результат, все функции `shooter` получат одно и то же значение из внешнего лексического окружения: последнее значение `i=10`. + + ![](lexenv-makearmy-empty.svg) + + Как вы можете видеть выше, на каждой итерации блока `while {...}` создается новое лексическое окружение. Чтобы исправить это, мы можем скопировать значение `i` в переменную внутри блока `while {...}`, например, так: + + ```js run + function makeArmy() { + let shooters = []; + + let i = 0; + while (i < 10) { + *!* + let j = i; + */!* + let shooter = function() { // функция shooter + alert( *!*j*/!* ); // должна выводить порядковый номер + }; + shooters.push(shooter); + i++; + } + + return shooters; + } + + let army = makeArmy(); + + // теперь код работает правильно + army[0](); // 0 + army[5](); // 5 + ``` + + Здесь `let j = i` объявляет "итерационно-локальную" переменную `j` и копирует в нее `i`. Примитивы копируются "по значению", поэтому фактически мы получаем независимую копию `i`, принадлежащую текущей итерации цикла. + + Функции `shooter` работают правильно, потому что значение `i` теперь живет чуть ближе. Не в лексическом окружении `makeArmy()`, а в лексическом окружении, соответствующем текущей итерации цикла: + + ![](lexenv-makearmy-while-fixed.svg) + + Этой проблемы также можно было бы избежать, если бы мы использовали `for` в начале, например, так: + + ```js run demo + function makeArmy() { + + let shooters = []; + + *!* + for (let i = 0; i < 10; i++) { + */!* + let shooter = function() { // функция shooter + alert( i ); // должна выводить порядковый номер + }; + shooters.push(shooter); + } + + return shooters; + } + + let army = makeArmy(); + + army[0](); // 0 + army[5](); // 5 + ``` + + По сути, это то же самое, поскольку `for` на каждой итерации создает новое лексическое окружение со своей переменной `i`. Поэтому функция `shooter`, создаваемая на каждой итерации, ссылается на свою собственную переменную `i`, причем именно с этой итерации. + + ![](lexenv-makearmy-for-fixed.svg) + +Теперь, когда вы приложили столько усилий, чтобы прочитать это объяснение, а конечный вариант оказался так прост - использовать `for`, вы можете задаться вопросом -- стоило ли оно того? + +Что ж, если бы вы могли легко ответить на вопрос из задачи, вы бы не стали читать решение. Так что, должно быть, эта задача помогла вам лучше понять суть дела. + +Кроме того, действительно встречаются случаи, когда человек предпочитает `while`, а не `for`, и другие сценарии, где такие проблемы реальны. diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/task.md b/1-js/06-advanced-functions/03-closure/10-make-army/task.md similarity index 51% rename from 1-js/06-advanced-functions/03-closure/8-make-army/task.md rename to 1-js/06-advanced-functions/03-closure/10-make-army/task.md index 130909bb5e..dd1df3cbba 100644 --- a/1-js/06-advanced-functions/03-closure/8-make-army/task.md +++ b/1-js/06-advanced-functions/03-closure/10-make-army/task.md @@ -17,19 +17,25 @@ function makeArmy() { let shooter = function() { // функция shooter alert( i ); // должна выводить порядковый номер }; - shooters.push(shooter); + shooters.push(shooter); // и добавлять стрелка в массив i++; } - return shooters; + // ...а в конце вернуть массив из всех стрелков + return shooters; } let army = makeArmy(); -army[0](); // у 0-го стрелка будет номер 10 -army[5](); // и у 5-го стрелка тоже будет номер 10 -// ... у всех стрелков будет номер 10, вместо 0, 1, 2, 3... +*!* +// все стрелки выводят 10 вместо их порядковых номеров (0, 1, 2, 3...) +army[0](); // 10 от стрелка с порядковым номером 0 +army[1](); // 10 от стрелка с порядковым номером 1 +army[2](); // 10 ...и т.д. +*/!* ``` -Почему у всех стрелков одинаковые номера? Почините код, чтобы он работал как задумано. +Почему у всех стрелков одинаковые номера? + +Почините код, чтобы он работал как задумано. diff --git a/1-js/06-advanced-functions/03-closure/2-closure-variable-access/lexenv-nested-work.svg b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/lexenv-nested-work.svg new file mode 100644 index 0000000000..1d25eb1bc8 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/lexenv-nested-work.svg @@ -0,0 +1 @@ +makeWorker: function name: "John"<пусто>outerouterouternullname: "Pete" diff --git a/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md new file mode 100644 index 0000000000..f5e013d179 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md @@ -0,0 +1,9 @@ +Ответ: **Pete**. + +Функция `work()` в приведенном ниже коде получает `name` из места его происхождения через ссылку на внешнее лексическое окружение: + +![](lexenv-nested-work.svg) + +Таким образом, в результате мы получаем `"Pete"`. + +Но если бы в `makeWorker()` не было `let name`, то поиск шел бы снаружи и брал глобальную переменную, что мы видим из приведенной выше цепочки. В этом случае результатом было бы `"John"`. diff --git a/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md new file mode 100644 index 0000000000..6623f93304 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# Какие переменные доступны? + +Приведенная ниже функция `makeWorker` создает другую функцию и возвращает ее. Эта новая функция может быть вызвана из другого места. + +Будет ли она иметь доступ к внешним переменным из места своего создания, или из места вызова, или из обоих мест? + +```js +function makeWorker() { + let name = "Pete"; + + return function() { + alert(name); + }; +} + +let name = "John"; + +// создаём функцию +let work = makeWorker(); + +// вызываем её +work(); // что будет показано? +``` + +Какое значение будет показано? "Pete" или "John"? diff --git a/1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md b/1-js/06-advanced-functions/03-closure/3-counter-independent/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md rename to 1-js/06-advanced-functions/03-closure/3-counter-independent/solution.md diff --git a/1-js/06-advanced-functions/03-closure/1-counter-independent/task.md b/1-js/06-advanced-functions/03-closure/3-counter-independent/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/1-counter-independent/task.md rename to 1-js/06-advanced-functions/03-closure/3-counter-independent/task.md diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md b/1-js/06-advanced-functions/03-closure/4-counter-object-independent/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md rename to 1-js/06-advanced-functions/03-closure/4-counter-object-independent/solution.md diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md b/1-js/06-advanced-functions/03-closure/4-counter-object-independent/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md rename to 1-js/06-advanced-functions/03-closure/4-counter-object-independent/task.md diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md rename to 1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md similarity index 51% rename from 1-js/06-advanced-functions/03-closure/3-function-in-if/task.md rename to 1-js/06-advanced-functions/03-closure/5-function-in-if/task.md index a63078f9cb..9f2a9fcde6 100644 --- a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md +++ b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md @@ -1,8 +1,13 @@ -# Функция в if +importance: 5 + +--- +# Функция внутри if Посмотрите на код. Какой будет результат у вызова на последней строке? -```js run +**Обратите внимание:** результат зависит от режима выполнения кода. Здесь используется строгий режим `"use strict"`. + +```js let phrase = "Hello"; if (true) { diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md rename to 1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/4-closure-sum/task.md rename to 1-js/06-advanced-functions/03-closure/6-closure-sum/task.md diff --git a/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md b/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md new file mode 100644 index 0000000000..f2bc002d33 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md @@ -0,0 +1,40 @@ +Ответ: **ошибка**. + +Попробуйте запустить этот код: + +```js run +let x = 1; + +function func() { +*!* + console.log(x); // ReferenceError: Cannot access 'x' before initialization +*/!* + let x = 2; +} + +func(); +``` + +В этом примере мы можем наблюдать характерную разницу между "несуществующей" и "неинициализированной" переменной. + +Как вы могли прочитать в статье [](info:closure), переменная находится в неинициализированном состоянии с момента входа в блок кода (или функцию). И остается неинициализированной до соответствующего оператора `let`. + +Другими словами, переменная технически существует, но не может быть использована до `let`. + +Приведенный выше код демонстрирует это. + +```js +function func() { +*!* + // локальная переменная x известна движку с самого начала выполнения функции, + // но она является неинициализированной до let ("мёртвая зона") + // следовательно, ошибка +*/!* + + console.log(x); // ReferenceError: Cannot access 'x' before initialization + + let x = 2; +} +``` + +Эту зону временной непригодности переменной (от начала блока кода до `let`) иногда называют "мёртвой зоной". diff --git a/1-js/06-advanced-functions/03-closure/7-let-scope/task.md b/1-js/06-advanced-functions/03-closure/7-let-scope/task.md new file mode 100644 index 0000000000..134f44ec91 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-let-scope/task.md @@ -0,0 +1,21 @@ +importance: 4 + +--- + +# Видна ли переменная? + +Что выведет данный код? + +```js +let x = 1; + +function func() { + console.log(x); // ? + + let x = 2; +} + +func(); +``` + +P.S. В этой задаче есть подвох. Решение не очевидно. diff --git a/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md b/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md deleted file mode 100644 index bd57085eaf..0000000000 --- a/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md +++ /dev/null @@ -1,22 +0,0 @@ - - -```js run -let users = [ - { name: "John", age: 20, surname: "Johnson" }, - { name: "Pete", age: 18, surname: "Peterson" }, - { name: "Ann", age: 19, surname: "Hathaway" } -]; - -*!* -function byField(field) { - return (a, b) => a[field] > b[field] ? 1 : -1; -} -*/!* - -users.sort(byField('name')); -users.forEach(user => alert(user.name)); // Ann, John, Pete - -users.sort(byField('age')); -users.forEach(user => alert(user.name)); // Pete, Ann, John -``` - diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/solution.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/solution.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/source.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/source.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/test.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/test.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md similarity index 94% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md index 6800e72c5d..1ca8244082 100644 --- a/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md +++ b/1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md @@ -13,7 +13,7 @@ importance: 5 Они должны использоваться таким образом: -- `arr.filter(inBetween(3,6))` -- выбирает только значения межу 3 и 6 (включительно). +- `arr.filter(inBetween(3,6))` -- выбирает только значения между 3 и 6 (включительно). - `arr.filter(inArray([1,2,3]))` -- выбирает только элементы, совпадающие с одним из элементов массива Например: diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg deleted file mode 100644 index f2429ff21a..0000000000 --- a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg +++ /dev/null @@ -1 +0,0 @@ -outeri: 0i: 1i: 2i: 10...makeArmy() LexicalEnvironmentблочный LexicalEnvironment \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md b/1-js/06-advanced-functions/03-closure/8-make-army/solution.md deleted file mode 100644 index d7a1af2dd9..0000000000 --- a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md +++ /dev/null @@ -1,122 +0,0 @@ - -Давайте посмотрим, что происходит внутри `makeArmy`, и решение станет очевидным. - -1. Она создаёт пустой массив `shooters`: - - ```js - let shooters = []; - ``` -2. В цикле заполняет его `shooters.push(function...)`. - - Каждый элемент -- это функция, так что получится такой массив: - - ```js no-beautify - 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` из своего внешнего лексического окружения. - -Какое будет значение у `i`? - -Если мы посмотрим в исходный код: - -```js -function makeArmy() { - ... - let i = 0; - while (i < 10) { - let shooter = function() { // функция shooter - alert( i ); // должна выводить порядковый номер - }; - ... - } - ... -} -``` - -...Мы увидим, что оно живёт в лексическом окружении, связанном с текущим вызовом `makeArmy()`. Но, когда вызывается `army[5]()`, `makeArmy` уже завершила свою работу, и последнее значение `i`: 10 (конец цикла `while`). - -Как результат, все функции `shooter` получат одно и то же из внешнего окружения: последнее значение `i=10`. - -Мы можем это исправить, переместив определение переменной в цикл: - -```js run demo -function makeArmy() { - - let shooters = []; - -*!* - for(let i = 0; i < 10; i++) { -*/!* - let shooter = function() { // функция shooter - alert( i ); // должна выводить порядковый номер - }; - shooters.push(shooter); - - - } - - return shooters; -} - -let army = makeArmy(); - -army[0](); // 0 -army[5](); // 5 -``` - -Теперь она работает правильно, потому что каждый раз, когда выполняется блок кода `for (let i=0...) {...}`, для него создаётся новое лексическое окружение с соответствующей переменной `i`. - -Так что значение `i` теперь живёт немного ближе. Не в лексическом окружении `makeArmy()`, а в лексическом окружении, которое соответствует текущей итерации цикла. Вот почему теперь она работает. - -![](lexenv-makearmy.svg) - -Здесь мы переписали `while` в `for`. - -Можно использовать другой трюк, давайте рассмотрим его для лучшего понимания предмета: - -```js run -function makeArmy() { - let shooters = []; - - let i = 0; - while (i < 10) { -*!* - let j = i; -*/!* - let shooter = function() { // функция shooter - alert( *!*j*/!* ); // должна выводить порядковый номер - }; - shooters.push(shooter); - i++; - } - - return shooters; -} - -let army = makeArmy(); - -army[0](); // 0 -army[5](); // 5 -``` - -Цикл `while` так же, как и `for`, создаёт новое лексическое окружение для каждой итерации. Так что тут мы хотим убедиться, что он получит правильное значение для `shooter`. - -Мы копируем `let j = i`. Это создаёт локальную для итерации переменную `j` и копирует в неё `i`. Примитивы копируются "по значению", поэтому мы получаем совершенно независимую копию `i`, принадлежащую текущей итерации цикла. diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/solution.js new file mode 100644 index 0000000000..8a71c869d9 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/solution.js @@ -0,0 +1,3 @@ +function byField(fieldName){ + return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1; +} diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/source.js b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/source.js new file mode 100644 index 0000000000..23b4338340 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/source.js @@ -0,0 +1,5 @@ +function byField(fieldName){ + + // Your code goes here. + +} diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/test.js b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/test.js new file mode 100644 index 0000000000..802f28c4d8 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/test.js @@ -0,0 +1,39 @@ +describe("byField", function(){ + + let users = [ + { name: "John", age: 20, surname: "Johnson" }, + { name: "Pete", age: 18, surname: "Peterson" }, + { name: "Ann", age: 19, surname: "Hathaway" }, + ]; + + it("sorts users by name", function(){ + let nameSortedKey = [ + { name: "Ann", age: 19, surname: "Hathaway" }, + { name: "John", age: 20, surname: "Johnson"}, + { name: "Pete", age: 18, surname: "Peterson" }, + ]; + let nameSortedAnswer = users.sort(byField("name")); + assert.deepEqual(nameSortedKey, nameSortedAnswer); + }); + + it("sorts users by age", function(){ + let ageSortedKey = [ + { name: "Pete", age: 18, surname: "Peterson" }, + { name: "Ann", age: 19, surname: "Hathaway" }, + { name: "John", age: 20, surname: "Johnson"}, + ]; + let ageSortedAnswer = users.sort(byField("age")); + assert.deepEqual(ageSortedKey, ageSortedAnswer); + }); + + it("sorts users by surname", function(){ + let surnameSortedKey = [ + { name: "Ann", age: 19, surname: "Hathaway" }, + { name: "John", age: 20, surname: "Johnson"}, + { name: "Pete", age: 18, surname: "Peterson" }, + ]; + let surnameSortedAnswer = users.sort(byField("surname")); + assert.deepEqual(surnameSortedAnswer, surnameSortedKey); + }); + +}); diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/solution.md b/1-js/06-advanced-functions/03-closure/9-sort-by-field/solution.md new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/solution.md @@ -0,0 +1 @@ + diff --git a/1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md b/1-js/06-advanced-functions/03-closure/9-sort-by-field/task.md similarity index 53% rename from 1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md rename to 1-js/06-advanced-functions/03-closure/9-sort-by-field/task.md index 3ca0fdad68..ca436771d9 100644 --- a/1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/task.md @@ -8,29 +8,29 @@ importance: 5 ```js let users = [ - { name: "John", age: 20, surname: "Johnson" }, - { name: "Pete", age: 18, surname: "Peterson" }, - { name: "Ann", age: 19, surname: "Hathaway" } + { name: "Иван", age: 20, surname: "Иванов" }, + { name: "Пётр", age: 18, surname: "Петров" }, + { name: "Анна", age: 19, surname: "Каренина" } ]; ``` Обычный способ был бы таким: ```js -// по имени (Ann, John, Pete) +// по имени (Анна, Иван, Пётр) users.sort((a, b) => a.name > b.name ? 1 : -1); -// по возрасту (Pete, Ann, John) +// по возрасту (Пётр, Анна, Иван) users.sort((a, b) => a.age > b.age ? 1 : -1); ``` -Можем ли мы сделать его короче, скажем, вот таким? +Можем ли мы сделать его короче, например вот таким? ```js users.sort(byField('name')); users.sort(byField('age')); ``` -То есть, чтобы вместо функции, мы просто писали `byField(fieldName)`. +То есть чтобы вместо функции мы просто писали `byField(fieldName)`. Напишите функцию `byField`, которая может быть использована для этого. diff --git a/1-js/06-advanced-functions/03-closure/article.md b/1-js/06-advanced-functions/03-closure/article.md index c6fc7b8bea..0ba1d8fb1c 100644 --- a/1-js/06-advanced-functions/03-closure/article.md +++ b/1-js/06-advanced-functions/03-closure/article.md @@ -1,5 +1,5 @@ -# Замыкание +# Область видимости переменных, замыкание JavaScript - язык с сильным функционально-ориентированным уклоном. Он даёт нам много свободы. Функция может быть динамически создана, скопирована в другую переменную или передана как аргумент другой функции и позже вызвана из совершенно другого места. @@ -11,194 +11,90 @@ JavaScript - язык с сильным функционально-ориент Разные языки ведут себя по-разному в таких случаях, и в этой главе мы рассмотрим поведение JavaScript. -## Пара вопросов +```smart header="Мы будем говорить о переменных `let/const` здесь" +В JavaScript существует три способа объявить переменную: `let`, `const` (современные), и `var` (пережиток прошлого). -Для начала давайте рассмотрим две ситуации, а затем изучим внутренние механизмы шаг за шагом, чтобы вы смогли ответить на эти и более сложные вопросы в будущем. - -1. Функция `sayHi` использует внешнюю переменную `name`. Какое значение будет использовать функция при выполнении? - - ```js - let name = "John"; - - function sayHi() { - alert("Hi, " + name); - } - - name = "Pete"; - - *!* - sayHi(); // что будет показано: "John" или "Pete"? - */!* - ``` - - Такие ситуации распространены и в браузерной и в серверной разработке. Выполнение функции может быть запланировано позже, чем она была создана, например, после какого-нибудь пользовательского действия или сетевого запроса. - - Итак, вопрос в том, получит ли она доступ к последним изменениям? - - -2. Функция `makeWorker` создаёт другую функцию и возвращает её. Новая функция может быть вызвана откуда-то ещё. Получит ли она доступ к внешним переменным из места своего создания или места выполнения или из обоих? - - ```js - function makeWorker() { - let name = "Pete"; - - return function() { - alert(name); - }; - } - - let name = "John"; - - // create a function - let work = makeWorker(); - - // call it - *!* - work(); // что будет показано? "Pete" (из места создания) или "John" (из места выполнения) - */!* - ``` - - -## Лексическое Окружение - -Чтобы понять, что происходит, давайте для начала обсудим, что такое "переменная" на самом деле. - -В JavaScript у каждой выполняемой функции, блока кода и скрипта есть связанный с ними внутренний (скрытый) объект, называемый *лексическим окружением* `LexicalEnvironment`. - -Объект лексического окружения состоит из двух частей: - -1. *Environment Record* -- объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение `this`). - -2. Ссылка на *внешнее лексическое окружение* - то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок). - -**"Переменная" -- это просто свойство специального внутреннего объекта: Environment Record. "Получить или изменить переменную", означает, "получить или изменить свойство этого объекта".** - -Например, в этом простом коде только одно лексическое окружение: - -![лексическое окружение](lexical-environment-global.svg) - -Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом. - -На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на `null`. - -А вот как оно изменяется при объявлении и присваивании переменной: - -![лексическое окружение](lexical-environment-global-2.svg) - -Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода: - -1. В начале скрипта лексическое окружение пустое. -2. Появляется определение переменной `let phrase`. У неё нет присвоенного значения, поэтому присваивается `undefined`. -3. Переменной `phrase` присваивается значение. -4. Переменная `phrase` меняет значение. - -Пока что всё выглядит просто, правда? - -Итого: - -- Переменная -- это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом. -- Работа с переменными -- это на самом деле работа со свойствами этого объекта. - -### Function Declaration - -До сих пор мы рассматривали только переменные. Теперь рассмотрим Function Declaration. - -**В отличие от переменных, объявленных с помощью `let`, они полностью инициализируются не тогда, когда выполнение доходит до них, а раньше, когда создаётся лексическое окружение.** - -Для верхнеуровневых функций это означает момент, когда скрипт начинает выполнение. - -Вот почему мы можем вызвать функцию, объявленную через Function Declaration, до того, как она определена. - -Следующий код демонстрирует, что уже с самого начала в лексическом окружении что-то есть. Там есть `say`, потому что это Function Declaration. И позже там появится `phrase`, объявленное через `let`: - -![лексическое окружение](lexical-environment-global-3.svg) - - -### Внутреннее и внешнее лексическое окружение - -Теперь давайте продолжим и посмотрим, что происходит, когда функция получает доступ к внешней переменной. - -В течение вызова `say()` использует внешнюю переменную `phrase`. Давайте разберёмся подробно, что происходит. - -При запуске функции для неё автоматически создаётся новое лексическое окружение, для хранения локальных переменных и параметров вызова. - -Например, для `say("John")` это выглядит так (выполнение находится на строке, отмеченной стрелкой): - - - -![лексическое окружение](lexical-environment-simple.svg) +- В этой статье мы будем использовать переменные `let` в примерах. +- Переменные, объявленные с помощью `const`, ведут себя так же, так что эта статья и о них. +- Старые переменные `var` имеют несколько характерных отличий, они будут рассмотрены в главе . +``` -Итак, в процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное): +## Блоки кода -- внутреннее лексическое окружение соответствует текущему выполнению `say`. +Если переменная объявлена внутри блока кода `{...}`, то она видна только внутри этого блока. - В нём находится одна переменная `name`, аргумент функции. Мы вызываем `say("John")`, так что значение переменной `name` равно `"John"`. -- Внешнее лексическое окружение -- это глобальное лексическое окружение. +Например: - В нём находятся переменная `phrase` и сама функция. +```js run +{ + // выполняем некоторые действия с локальной переменной, которые не должны быть видны снаружи -У внутреннего лексического окружения есть ссылка `outer` на внешнее. + let message = "Hello"; // переменная видна только в этом блоке -**Когда код хочет получить доступ к переменной -- сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.** + alert(message); // Hello +} -Если переменная не была найдена, это будет ошибкой в `strict mode`. Без `strict mode`, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким именем. +alert(message); // ReferenceError: message is not defined +``` -Давайте посмотрим, как происходит поиск в нашем примере: +С помощью блоков `{...}` мы можем изолировать часть кода, выполняющую свою собственную задачу, с переменными, принадлежащими только ей: -- Когда `alert` внутри `say` хочет получить доступ к `name`, он немедленно находит переменную в лексическом окружении функции. -- Когда он хочет получить доступ к `phrase`, которой нет локально, он следует дальше по ссылке к внешнему лексическому окружению и находит переменную там. +```js run +{ + // показать сообщение + let message = "Hello"; + alert(message); +} -![поиск лексического окружения](lexical-environment-simple-lookup.svg) +{ + // показать другое сообщение + let message = "Goodbye"; + alert(message); +} +``` -Теперь у нас есть ответ на первый вопрос из начала главы. +````smart header="Без блоков была бы ошибка" +Обратите внимание, что без отдельных блоков возникнет ошибка, если мы используем let с существующим именем переменной: -**Функция получает текущее значение внешних переменных, то есть, их последнее значение** +```js run +// показать сообщение +let message = "Hello"; +alert(message); -Старые значения переменных нигде не сохраняются. Когда функция хочет получить доступ к переменной, она берёт её текущее значение из своего или внешнего лексического окружения. +// показать другое сообщение +let message = "Goodbye"; // SyntaxError: Identifier 'message' has already been declared +alert(message); +``` +```` -Так что, ответ на первый вопрос: `Pete`: +Для `if`, `for`, `while` и т.д. переменные, объявленные в блоке кода `{...}`, также видны только внутри: ```js run -let name = "John"; +if (true) { + let phrase = "Hello"; -function sayHi() { - alert("Hi, " + name); + alert(phrase); // Hello } -name = "Pete"; // (*) - -*!* -sayHi(); // Pete -*/!* +alert(phrase); // Ошибка, нет такой переменной! ``` +В этом случае после завершения работы `if` нижний `alert` не увидит `phrase`, что и приведет к ошибке. -Порядок выполнения кода, приведённого выше: - -1. В глобальном лексическом окружении есть `name: "John"`. -2. На строке `(*)` глобальная переменная изменяется, теперь `name: "Pete"`. -3. Момент, когда выполняется функция `sayHi()` и берёт переменную `name` извне. Теперь из глобального лексического окружения, где переменная уже равна `"Pete"`. +И это замечательно, поскольку это позволяет нам создавать блочно-локальные переменные, относящиеся только к ветви `if`. +То же самое можно сказать и про циклы `for` и `while`: -```smart header="Один вызов -- одно лексическое окружение" -Пожалуйста, обратите внимание, что новое лексическое окружение функции создаётся каждый раз, когда функция выполняется. - -И, если функция вызывается несколько раз, то для каждого вызова будет своё лексическое окружение, со своими, специфичными для этого вызова, локальными переменными и параметрами. -``` +```js run +for (let i = 0; i < 3; i++) { + // переменная i видна только внутри for + alert(i); // 0, потом 1, потом 2 +} -```smart header="Лексическое окружение -- это специальный внутренний объект" -"Лексическое окружение" -- это специальный внутренний объект. Мы не можем получить его в нашем коде и изменять напрямую. Сам движок JavaScript может оптимизировать его, уничтожать неиспользуемые переменные для освобождения памяти и выполнять другие внутренние уловки, но видимое поведение объекта должно оставаться таким, как было описано. +alert(i); // Ошибка, нет такой переменной! ``` +Визуально `let i = 0;` находится вне блока кода `{...}`, однако здесь в случае с `for` есть особенность: переменная, объявленная внутри `(...)`, считается частью блока. ## Вложенные функции @@ -226,23 +122,7 @@ function sayHiBye(firstName, lastName) { Что ещё интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте. Не важно где, она всё так же будет иметь доступ к тем же внешним переменным. -Например, здесь, вложенная функция присваивается новому объекту в [конструкторе](info:constructor-new): - -```js run -// функция-конструктор возвращает новый объект -function User(name) { - - // методом объекта становится вложенная функция - this.sayHi = function() { - alert(name); - }; -} - -let user = new User("John"); -user.sayHi(); // у кода метода "sayHi" есть доступ к внешней переменной "name" -``` - -А здесь мы просто создаём и возвращаем функцию "счётчик": +Ниже, `makeCounter` создает функцию «счётчик», которая при каждом вызове возвращает следующее число: ```js run function makeCounter() { @@ -260,313 +140,191 @@ alert( counter() ); // 1 alert( counter() ); // 2 ``` -Давайте продолжим с примером `makeCounter`. Он создаёт функцию "counter", которая возвращает следующее число при каждом вызове. Несмотря на простоту, немного модифицированные варианты этого кода применяются на практике, например, в [генераторе псевдослучайных чисел](https://ru.wikipedia.org/wiki/Генератор_псевдослучайных_чисел) и во многих других случаях. - -Как же это работает изнутри? - -Когда внутренняя функция начинает выполняться, начинается поиск переменной `count++` изнутри-наружу. Для примера выше порядок будет такой: - -![](lexical-search-order.svg) - -1. Локальные переменные вложенной функции... -2. Переменные внешней функции... -3. И так далее, пока не будут достигнуты глобальные переменные. - -В этом примере `count` будет найден на шаге `2`. Когда внешняя переменная модифицируется, она изменится там, где была найдена. Значит, `count++` найдёт внешнюю переменную и увеличит её значение в лексическом окружении, которому она принадлежит. Как если бы у нас было `let count = 1`. - -Теперь рассмотрим два вопроса: - -1. Можем ли мы каким-нибудь образом сбросить счётчик `count` из кода, который не принадлежит `makeCounter`? Например, после вызова `alert` в коде выше. -2. Если мы вызываем `makeCounter` несколько раз -- нам возвращается много функций `counter`. Они независимы или разделяют одну и ту же переменную `count`? +Несмотря на простоту этого примера, немного модифицированные его варианты применяются на практике, например, в [генераторе псевдослучайных чисел](https://ru.wikipedia.org/wiki/Генератор_псевдослучайных_чисел) и во многих других случаях. -Попробуйте ответить на эти вопросы перед тем, как продолжить чтение. +Как это работает? Если мы создадим несколько таких счётчиков, будут ли они независимыми друг от друга? Что происходит с переменными? -... +Понимание таких вещей полезно для повышения общего уровня владения JavaScript и для более сложных сценариев. Так что давайте немного углубимся. -Готовы? +## Лексическое окружение -Хорошо, давайте ответим на вопросы. +```warn header="Здесь водятся драконы!" +Глубокое техническое описание - впереди. -1. Такой возможности нет: `count` -- локальная переменная функции, мы не можем получить к ней доступ извне. -2. Для каждого вызова `makeCounter()` создаётся новое лексическое окружение функции, со своим собственным `count`. Так что, получившиеся функции `counter` -- независимы. - -Вот демо: - -```js run -function makeCounter() { - let count = 0; - return function() { - return count++; - }; -} - -let counter1 = makeCounter(); -let counter2 = makeCounter(); - -alert( counter1() ); // 0 -alert( counter1() ); // 1 - -alert( counter2() ); // 0 (независимо) +Как бы мне ни хотелось избежать низкоуровневых деталей языка, любое представление о JavaScript без них будет недостаточным и неполным, так что приготовьтесь. ``` -Надеюсь, ситуация с внешними переменными теперь ясна. Для большинства ситуаций такого понимания вполне достаточно, но в спецификации есть ряд деталей, которые мы, для простоты, опустили. Далее мы разберём происходящее ещё более подробно. - -## Окружение в деталях - -Вот что происходит в примере с `makeCounter` шаг за шагом. Пройдите их, чтобы убедиться, что вы разобрались с каждой деталью. - -Пожалуйста, обратите внимание на дополнительное свойство `[[Environment]]`, про которое здесь рассказано. Мы не упоминали о нём раньше для простоты. - -1. Когда скрипт только начинает выполняться, есть только глобальное лексическое окружение: - - ![](lexenv-nested-makecounter-1.svg) - - В этот начальный момент есть только функция `makeCounter`, потому что это Function Declaration. Она ещё не выполняется. - - **Все функции "при рождении" получают скрытое свойство `[[Environment]]`, которое ссылается на лексическое окружение места, где они были созданы.** - - Мы ещё не говорили об этом, это то, каким образом функции знают, где они были созданы. - - В данном случае, `makeCounter` создан в глобальном лексическом окружении, так что `[[Environment]]` содержит ссылку на него. - - Другими словами, функция навсегда запоминает ссылку на лексическое окружение, где она была создана. И `[[Environment]]` -- скрытое свойство функции, которое содержит эту ссылку. - -2. Код продолжает выполняться, объявляется новая глобальная переменная `counter`, которой присваивается результат вызова `makeCounter`. Вот снимок момента, когда интерпретатор находится на первой строке внутри `makeCounter()`: - - ![](lexenv-nested-makecounter-2.svg) - - В момент вызова `makeCounter()` создаётся лексическое окружение, для хранения его переменных и аргументов. - - Как и все лексические окружения, оно содержит две вещи: - 1. Environment Record с локальными переменными. В нашем случае `count` -- единственная локальная переменная (появляющаяся, когда выполняется строчка с `let count`). - 2. Ссылка на внешнее окружение, которая устанавливается в значение `[[Environment]]` функции. В данном случае, `[[Environment]]` функции `makeCounter` ссылается на глобальное лексическое окружение. - - Итак, теперь у нас есть два лексических окружения: первое -- глобальное, второе -- для текущего вызова `makeCounter`, с внешней ссылкой на глобальный объект. -3. В процессе выполнения `makeCounter()` создаётся небольшая вложенная функция. +Для большей наглядности объяснение разбито на несколько шагов. - Не имеет значения, какой способ объявления функции используется: Function Declaration или Function Expression. Все функции получают свойство `[[Environment]]`, которое ссылается на лексическое окружение, в котором они были созданы. То же самое происходит и с нашей новой маленькой функцией. +### Шаг 1. Переменные - Для нашей новой вложенной функции значением `[[Environment]]` будет текущее лексическое окружение `makeCounter()` (где она была создана): +В JavaScript у каждой выполняемой функции, блока кода `{...}` и скрипта есть связанный с ними внутренний (скрытый) объект, называемый *лексическим окружением* `LexicalEnvironment`. - ![](lexenv-nested-makecounter-3.svg) - - Пожалуйста, обратите внимание, что на этом шаге внутренняя функция была создана, но ещё не вызвана. Код внутри `function() { return count++ }` не выполняется. - -4. Выполнение продолжается, вызов `makeCounter()` завершается, и результат (небольшая вложенная функция) присваивается глобальной переменной `counter`: - - ![](lexenv-nested-makecounter-4.svg) - - В этой функции есть только одна строчка: `return count++`, которая будет выполнена, когда мы вызовем функцию. - -6. При вызове `counter()` для этого вызова создаётся новое лексическое окружение. Оно пустое, так как в самом `counter` локальных переменных нет. Но `[[Environment]]` `counter` используется, как ссылка на внешнее лексическое окружение `outer`, которое даёт доступ к переменным предшествующего вызова `makeCounter`, где `counter` был создан. +Объект лексического окружения состоит из двух частей: - ![](lexenv-nested-makecounter-5.svg) +1. *Environment Record* -- объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение `this`). - Теперь, когда вызов ищет переменную `count`, он сначала ищет в собственном лексическом окружении (пустое), а затем в лексическом окружении предшествующего вызова `makeCounter()`, где и находит её. +2. Ссылка на *внешнее лексическое окружение* - то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок). - Пожалуйста, обратите внимание, как здесь работает управление памятью. Хотя `makeCounter()` закончил выполнение некоторое время назад, его лексическое окружение остаётся в памяти, потому что есть вложенная функция с `[[Environment]]`, который ссылается на него. +**"Переменная" -- это просто свойство специального внутреннего объекта: `Environment Record`. "Получить или изменить переменную", означает, "получить или изменить свойство этого объекта".** - В большинстве случаев, объект лексического окружения существует до того момента, пока есть функция, которая может его использовать. И только тогда, когда таких не остаётся, окружение уничтожается. +Например, в этом простом коде только одно лексическое окружение: -6. Вызов `counter()` не только возвращает значение `count`, но также увеличивает его. Обратите внимание, что модификация происходит "на месте". Значение `count` изменяется конкретно в том окружении, где оно было найдено. +![лексическое окружение](lexical-environment-global.svg) - ![](lexenv-nested-makecounter-6.svg) +Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом. -7. Следующие вызовы `counter()` сделают то же самое. +На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на `null`. -Теперь ответ на второй вопрос из начала главы должен быть очевиден. +По мере выполнения кода лексическое окружение меняется. -Функция `work()` в коде ниже получает `name` из того места, где была создана, через ссылку на внешнее лексическое окружение: +Вот более длинный код: -![](lexenv-nested-work.svg) +![лексическое окружение](closure-variable-phrase.svg) -Так что, результатом будет `"Pete"`. +Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода: -Но, если бы в `makeWorker()` не было `let name`, тогда бы поиск продолжился дальше и была бы взята глобальная переменная, как мы видим из приведённой выше цепочки. В таком случае, результатом было бы `"John"`. +1. При запуске скрипта лексическое окружение предварительно заполняется всеми объявленными переменными. + - Изначально они находятся в состоянии "Uninitialized". Это особое внутреннее состояние, которое означает, что движок знает о переменной, но на нее нельзя ссылаться, пока она не будет объявлена с помощью `let`. Это почти то же самое, как если бы переменная не существовала. +2. Появляется определение переменной `let phrase`. У неё ещё нет присвоенного значения, поэтому присваивается `undefined`. С этого момента мы можем использовать переменную. +3. Переменной `phrase` присваивается значение. +4. Переменная `phrase` меняет значение. -```smart header="Замыкания" -В программировании есть общий термин: "замыкание", -- которое должен знать каждый разработчик. +Пока что всё выглядит просто, правда? -[Замыкание](https://ru.wikipedia.org/wiki/Замыкание_(программирование)) -- это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями (есть только одно исключение, про которое будет рассказано в ). +- Переменная -- это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом. +- Работа с переменными -- это на самом деле работа со свойствами этого объекта. -То есть, они автоматически запоминают, где были созданы, с помощью скрытого свойства `[[Environment]]` и все они могут получить доступ к внешним переменным. +```smart header="Лексическое окружение - объект спецификации" +"Лексическое окружение" - это объект спецификации: он существует только "теоретически" в спецификации языка для описания того, как все работает. Мы не можем получить этот объект в нашем коде и манипулировать им напрямую. -Когда на собеседовании фронтенд-разработчик получает вопрос: "что такое замыкание?", -- правильным ответом будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и, может быть, несколько слов о технических деталях: свойстве `[[Environment]]` и о том, как работает лексическое окружение. +JavaScript-движки также могут оптимизировать его, отбрасывать неиспользуемые переменные для экономии памяти и выполнять другие внутренние действия, но при этом видимое поведение остается таким, как описано. ``` -## Блоки кода и циклы, IIFE +### Шаг 2. Function Declaration -Предыдущие примеры сосредоточены на функциях. Но лексическое окружение существует для любых блоков кода `{...}`. +Функция - это тоже значение, как и переменная. -Лексическое окружение создаётся при выполнении блока кода и содержит локальные переменные для этого блока. Вот пара примеров. +**Разница заключается в том, что Function Declaration мгновенно инициализируется полностью.** -### If +Когда создается лексическое окружение, Function Declaration сразу же становится функцией, готовой к использованию (в отличие от `let`, который до момента объявления не может быть использован). -В следующем примере переменная `user` существует только в блоке `if`: +Именно поэтому мы можем вызвать функцию, объявленную как Function Declaration, до самого её объявления. - +### Шаг 3. Внутреннее и внешнее лексическое окружение -![](lexenv-if.svg) +Когда запускается функция, в начале ее вызова автоматически создается новое лексическое окружение для хранения локальных переменных и параметров вызова. -Когда выполнение попадает в блок `if`, для этого блока создаётся новое лексическое окружение. - -У него есть ссылка на внешнее окружение, так что `phrase` может быть найдена. Но все переменные и Function Expression, объявленные внутри `if`, остаются в его лексическом окружении и не видны снаружи. +Например, для `say("John")` это выглядит так (выполнение находится на строке, отмеченной стрелкой): -Например, после завершения `if` следующий `alert` не увидит `user`, что вызовет ошибку. +![лексическое окружение](lexical-environment-simple.svg) -### For, while +В процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное): -Для цикла у каждой итерации своё отдельное лексическое окружение. Если переменная объявлена в `for(let ...)`, то она также в нём: +- Внутреннее лексическое окружение соответствует текущему выполнению `say`. -```js run -for (let i = 0; i < 10; i++) { - // У каждого цикла своё собственное лексическое окружение - // {i: value} -} + В нём находится одна переменная `name`, параметр функции. Мы вызываем `say("John")`, так что значение переменной `name` равно `"John"`. +- Внешнее лексическое окружение -- это глобальное лексическое окружение. -alert(i); // Ошибка, нет такой переменной -``` + В нём находятся переменная `phrase` и сама функция. -Обратите внимание: `let i` визуально находится снаружи `{...}`. Но конструкция `for` -- особенная в этом смысле, у каждой итерации цикла своё собственное лексическое окружение с текущим `i` в нём. +У внутреннего лексического окружения есть ссылка на внешнее `outer`. -И так же, как и в `if`, ниже цикла `i` невидима. +**Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.** -### Блоки кода +Если переменная не была найдена, это будет ошибкой в строгом режиме (`use strict`). Без строгого режима, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким же именем. -Мы также можем использовать "простые" блоки кода `{...}`, чтобы изолировать переменные в "локальной области видимости". +Давайте посмотрим, как происходит поиск в нашем примере: -Например, в браузере все скрипты (кроме `type="module"`) разделяют одну общую глобальную область. Так что, если мы создадим глобальную переменную в одном скрипте, она станет доступна и в других. Но это становится источником конфликтов, если два скрипта используют одно и тоже имя переменной и перезаписывают друга друга. +- Для переменной `name`, `alert` внутри `say` сразу же находит ее во внутреннем лексическом окружении. +- Когда `alert` хочет получить доступ к `phrase`, он не находит её локально, поэтому вынужден обратиться к внешнему лексическому окружению и находит `phrase` там. -Это может произойти, если название переменной -- широко распространённое слово, а авторы скрипта не знают друг о друге. +![поиск лексического окружения](lexical-environment-simple-lookup.svg) -Если мы хотим этого избежать, мы можем использовать блок кода для изоляции всего скрипта или какой-то его части: +### Шаг 4. Возврат функции -```js run -{ - // сделать какую-нибудь работу с локальными переменными, которые не должны быть видны снаружи +Давайте вернёмся к примеру с `makeCounter`: - let message = "Hello"; +```js +function makeCounter() { + let count = 0; - alert(message); // Hello + return function() { + return count++; + }; } -alert(message); // Ошибка: переменная message не определена +let counter = makeCounter(); ``` -Из-за того, что у блока есть собственное лексическое окружение, код снаружи него (или в другом скрипте) не видит переменные этого блока. - -### IIFE - -В прошлом в JavaScript не было лексического окружения на уровне блоков кода. +В начале каждого вызова `makeCounter()` создается новый объект лексического окружения, в котором хранятся переменные для конкретного запуска `makeCounter`. -Так что программистам пришлось что-то придумать. И то, что они сделали, называется "immediately-invoked function expressions" (аббревиатура IIFE), что означает функцию, запускаемую сразу после объявления. +Таким образом, мы имеем два вложенных лексических окружения, как в примере выше: -Это не то, что мы должны использовать сегодня, но, так как вы можете встретить это в старых скриптах, полезно понимать принцип работы. - -IIFE выглядит так: - -```js run -(function() { - - let message = "Hello"; - - alert(message); // Hello - -})(); -``` +![](lexenv-nested-makecounter-2.svg) -Здесь создаётся и немедленно вызывается Function Expression. Так что код выполняется сразу же и у него есть свои локальные переменные. +Отличие заключается в том, что во время выполнения `makeCounter()` создается крошечная вложенная функция, состоящая всего из одной строки: `return count++`. Мы ее еще не запускаем, а только создаем. -Function Expression обёрнуто в скобки `(function {...})`, потому что, когда JavaScript встречает `"function"` в основном потоке кода, он воспринимает это как начало Function Declaration. Но у Function Declaration должно быть имя, так что такой код вызовет ошибку: +Все функции помнят лексическое окружение, в котором они были созданы. Технически здесь нет никакой магии: все функции имеют скрытое свойство `[[Environment]]`, которое хранит ссылку на лексическое окружение, в котором была создана функция: -```js run -// Попробуйте объявить и сразу же вызвать функцию -function() { // <-- Error: Unexpected token ( +![](lexenv-nested-makecounter-3.svg) - let message = "Hello"; +Таким образом, `counter.[[Environment]]` имеет ссылку на `{count: 0}` лексического окружения. Так функция запоминает, где она была создана, независимо от того, где она вызывается. Ссылка на `[[Environment]]` устанавливается один раз и навсегда при создании функции. - alert(message); // Hello +Впоследствии, при вызове `counter()`, для этого вызова создается новое лексическое окружение, а его внешняя ссылка на лексическое окружение берется из `counter.[[Environment]]`: -}(); -``` +![](closure-makecounter-nested-call.svg) -Даже если мы скажем: "хорошо, давайте добавим имя", -- это не сработает, потому что JavaScript не позволяет вызывать Function Declaration немедленно. +Теперь, когда код внутри `counter()` ищет переменную `count`, он сначала ищет ее в собственном лексическом окружении (пустом, так как там нет локальных переменных), а затем в лексическом окружении внешнего вызова `makeCounter()`, где находит `count` и изменяет ее. -```js run -// ошибка синтаксиса из-за скобок ниже -function go() { +**Переменная обновляется в том лексическом окружении, в котором она существует.** -}(); // <-- не можете вызывать Function Declaration немедленно -``` -Так что, скобки вокруг функции -- это трюк, который позволяет показать JavaScript, что функция была создана в контексте другого выражения, и, таким образом, это функциональное выражение: ей не нужно имя и её можно вызвать немедленно. +Вот состояние после выполнения: -Кроме скобок, существуют и другие пути показать JavaScript, что мы имеем в виду Function Expression: +![](closure-makecounter-nested-call-2.svg) -```js run -// Пути создания IIFE +Если мы вызовем `counter()` несколько раз, то в одном и том же месте переменная `count` будет увеличена до `2`, `3` и т.д. -(function() { - alert("Скобки вокруг функции"); -}*!*)*/!*(); +```smart header="Замыкания" +В программировании есть общий термин: «замыкание», – который должен знать каждый разработчик. -(function() { - alert("Скобки вокруг всего"); -}()*!*)*/!*; +[Замыкание](https://ru.wikipedia.org/wiki/Замыкание_(программирование)) -- это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями (есть только одно исключение, про которое будет рассказано в ). -*!*!*/!*function() { - alert("Выражение начинается с побитового оператора NOT"); -}(); +То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства `[[Environment]]`, и все они могут получить доступ к внешним переменным. -*!*+*/!*function() { - alert("Выражение начинается с унарного плюса"); -}(); +Когда на собеседовании фронтенд-разработчику задают вопрос: «что такое замыкание?», – правильным ответом будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и, может быть, несколько слов о технических деталях: свойстве `[[Environment]]` и о том, как работает лексическое окружение. ``` -Во всех перечисленных случаях мы объявляем Function Expression и немедленно выполняем его. Ещё раз заметим, что в настоящий момент нет необходимости писать подобный код. - ## Сборка мусора -Обычно лексическое окружение очищается и удаляется после того, как функция выполнилась. Например: - -```js -function f() { - let value1 = 123; - let value2 = 456; -} +Обычно лексическое окружение удаляется из памяти вместе со всеми переменными после завершения вызова функции. Это связано с тем, что на него нет ссылок. Как и любой объект JavaScript, оно хранится в памяти только до тех пор, пока к нему можно обратиться. -f(); -``` +Однако если существует вложенная функция, которая все еще доступна после завершения функции, то она имеет свойство `[[Environment]]`, ссылающееся на лексическое окружение. -Здесь два значения, которые технически являются свойствами лексического окружения. Но после того, как `f()` завершится, это лексическое окружение станет недоступно, поэтому оно удалится из памяти. +В этом случае лексическое окружение остается доступным даже после завершения работы функции. -...Но, если есть вложенная функция, которая всё ещё доступна после выполнения `f`, то у неё есть свойство `[[Environment]]`, которое ссылается на внешнее лексическое окружение, тем самым оставляя его достижимым, "живым": +Например: ```js function f() { let value = 123; - function g() { alert(value); } - -*!* - return g; -*/!* + return function() { + alert(value); + } } -let g = f(); // g доступно и продолжает держать внешнее лексическое окружение в памяти +let g = f(); // g.[[Environment]] хранит ссылку на лексическое окружение +// из соответствующего вызова f() ``` -Обратите внимание, если `f()` вызывается несколько раз и возвращаемые функции сохраняются, тогда все соответствующие объекты лексического окружения продолжат держаться в памяти. Вот три такие функции в коде ниже: +Обратите внимание, что если `f()` вызывается много раз и результирующие функции сохраняются, то все соответствующие объекты лексического окружения также будут сохранены в памяти. В приведенном ниже коде -- все три: ```js function f() { @@ -575,37 +333,36 @@ function f() { return function() { alert(value); }; } -// три функции в массиве, каждая из них ссылается на лексическое окружение +// 3 функции в массиве, каждая из которых ссылается на лексическое окружение // из соответствующего вызова f() let arr = [f(), f(), f()]; ``` -Объект лексического окружения умирает, когда становится недоступным (как и любой другой объект). Другими словами, он существует только до того момента, пока есть хотя бы одна вложенная функция, которая ссылается на него. +Объект лексического окружения исчезает, когда становится недоступным (как и любой другой объект). Другими словами, он существует только до тех пор, пока на него ссылается хотя бы одна вложенная функция. -В следующем коде, после того как `g` станет недоступным, лексическое окружение функции (и, соответственно, `value`) будет удалено из памяти; +В приведенном ниже коде после удаления вложенной функции ее окружающее лексическое окружение (а значит, и `value`) очищается из памяти: ```js function f() { let value = 123; - function g() { alert(value); } - - return g; + return function() { + alert(value); + } } -let g = f(); // while g is alive -// соответствующее лексическое окружение существует +let g = f(); // пока существует функция g, value остается в памяти -g = null; // ...а теперь память очищается +g = null; // ...и теперь память очищена. ``` ### Оптимизация на практике Как мы видели, в теории, пока функция жива, все внешние переменные тоже сохраняются. -Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если легко по коду понять, что внешняя переменная не используется -- она удаляется. +Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если легко по коду понять, что внешняя переменная не используется – она удаляется. -**Одним из важных побочных эффектов в V8 (Chrome, Opera) является то, что такая переменная становится недоступной при отладке.** +**Одним из важных побочных эффектов в V8 (Chrome, Edge, Opera) является то, что такая переменная становится недоступной при отладке.** Попробуйте запустить следующий пример в Chrome с открытой Developer Tools. @@ -626,11 +383,11 @@ let g = f(); g(); ``` -Как вы можете видеть -- такой переменной не существует! В теории, она должна быть доступна, но попала под оптимизацию движка. +Как вы можете видеть – такой переменной не существует! В теории, она должна быть доступна, но попала под оптимизацию движка. -Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них -- мы можем увидеть не ту внешнюю переменную при совпадающих названиях: +Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них – мы можем увидеть не ту внешнюю переменную при совпадающих названиях: -```js run global +```js run let value = "Сюрприз!"; function f() { @@ -647,9 +404,6 @@ let g = f(); g(); ``` -```warn header="До встречи!" -Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Opera, рано или поздно вы с ней встретитесь. +Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Edge/Opera, рано или поздно вы с ней столкнётесь. -Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится. -Вы всегда можете проверить это, запустив пример на этой странице. -``` +Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится. Вы всегда можете проверить это, запустив примеры на этой странице. diff --git a/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg b/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg new file mode 100644 index 0000000000..d18b2b71c0 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg @@ -0,0 +1 @@ +outernullначало выполненияphrase: <uninitialized> say: function... diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg new file mode 100644 index 0000000000..e151fb1ba6 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg @@ -0,0 +1 @@ +count: 1<пусто>nullouterouteroutermakeCounter: function counter: functionизменено здесь diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg new file mode 100644 index 0000000000..75ff7ae7e1 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg @@ -0,0 +1 @@ +count: 0<пусто>nullouterouteroutermakeCounter: function counter: function diff --git a/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg b/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg new file mode 100644 index 0000000000..58bcb57b8e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg @@ -0,0 +1 @@ +phrase: "Bye"phrase: "Hello"phrase: undefinedphrase: <uninitialized>outernullначало выполнения diff --git a/1-js/06-advanced-functions/03-closure/lexenv-if.svg b/1-js/06-advanced-functions/03-closure/lexenv-if.svg index e2b8d6f69f..3d4d6d7ccf 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-if.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-if.svg @@ -1 +1 @@ -phrase: "Hello"outerouternulluser: "John" \ No newline at end of file +phrase: "Hello"outerouternulluser: "John" \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg index 70649ffd59..f15e77a888 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg @@ -1 +1 @@ -makeCounter: function[[Environment]]outernull \ No newline at end of file +makeCounter: function[[Environment]]outernull \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg index 7ca718b95a..3cd8162cc1 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg @@ -1 +1 @@ -makeCounter: functioncounter: undefinedcount: 0outerouternullглобальное LexicalEnvironmentLexicalEnvironment вызова makeCounter() \ No newline at end of file +makeCounter: functioncounter: undefinedcount: 0outerouternullглобальное LexicalEnvironmentLexicalEnvironment вызова makeCounter() \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg index aa0eee4e38..54f1d97ba5 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg @@ -1 +1 @@ -makeCounter: functioncounter: undefinedcount: 0outerouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: undefinedcount: 0outerouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg index 216658f59e..fb60a785fa 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 0outerouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: functioncount: 0outerouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg index 05c137f195..79c440da7a 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 0<пусто>outerouterouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: functioncount: 0<empty>outerouterouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg index b80a01f33c..f5a73aa0cb 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 1outerouternull[[Environment]]изменено здесь \ No newline at end of file +makeCounter: functioncounter: functioncount: 1outerouternull[[Environment]]изменено здесь \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg index ae115aa234..e5b7f83e78 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg @@ -1 +1 @@ -makeWorker: function name: "John"<empty>outerouterouternullname: "Pete" \ No newline at end of file +makeWorker: function name: "John"<empty>outerouterouternullname: "Pete" \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg index 4b87e2311d..72d8f77f6b 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg @@ -1 +1 @@ -phrase: "Bye"phrase: "Hello"phrase: undefined<пусто>outernullначало выполнения \ No newline at end of file +phrase: "Bye"phrase: "Hello"phrase: undefined<empty>outernullначало выполнения \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg index 1316e6987f..e33d72adad 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg @@ -1 +1 @@ -say: function phrase: "Hello"say: functionouternullначало выполнения \ No newline at end of file +say: function phrase: "Hello"say: functionouternullначало выполнения \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg index f974122ff8..9b032bc88b 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg @@ -1 +1 @@ -phrase: "Hello"outernullLexicalEnvironment \ No newline at end of file +phrase: "Hello"outernullЛексическое Окружение \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg index 242f970eb5..79501a5b0c 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg @@ -1 +1 @@ -say: function phrase: "Hello"name: "John"outerouternull \ No newline at end of file +say: function phrase: "Hello"name: "John"outerouternull \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg index 87714b2d75..2778c0919a 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg @@ -1 +1 @@ -say: function phrase: "Hello"name: "John"outerouternullLexicalEnvironment вызова \ No newline at end of file +say: function phrase: "Hello"name: "John"outerouternullЛексическое Окружение вызова \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-search-order.svg b/1-js/06-advanced-functions/03-closure/lexical-search-order.svg index 4b960daa93..d9884ec4f1 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-search-order.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-search-order.svg @@ -1 +1 @@ -123 \ No newline at end of file +123 \ No newline at end of file diff --git a/1-js/06-advanced-functions/04-var/article.md b/1-js/06-advanced-functions/04-var/article.md index 2c914de25e..b0bf9bbd81 100644 --- a/1-js/06-advanced-functions/04-var/article.md +++ b/1-js/06-advanced-functions/04-var/article.md @@ -1,6 +1,12 @@ # Устаревшее ключевое слово "var" +```smart header="Эта статья предназначена для понимания старых скриптов" +Информация, приведенная в этой статье, полезна для понимания старых скриптов. + +Мы не пишем современный код таким образом. +``` + В самой первой главе про [переменные](info:variables) мы ознакомились с тремя способами объявления переменных: 1. `let` @@ -88,6 +94,35 @@ alert(phrase); // Ошибка: phrase не определена (видна в Как мы видим, `var` выходит за пределы блоков `if`, `for` и подобных. Это происходит потому, что на заре развития JavaScript блоки кода не имели лексического окружения. Поэтому можно сказать, что `var` - это пережиток прошлого. +## "var" допускает повторное объявление + +Если в блоке кода дважды объявить одну и ту же переменную `let`, будет ошибка: + +```js run +let user; +let user; // SyntaxError: 'user' has already been declared +``` + +Используя `var`, можно переобъявлять переменную сколько угодно раз. Повторные `var` игнорируются: +```js run +var user = "Пётр"; + +var user; // ничего не делает, переменная объявлена раньше +// ...нет ошибки + +alert(user); // Пётр +``` + +Если дополнительно присвоить значение, то переменная примет новое значение: + +```js run +var user = "Пётр"; + +var user = "Иван"; + +alert(user); // Иван +``` + ## "var" обрабатываются в начале запуска функции Объявления переменных `var` обрабатываются в начале выполнения функции (или запуска скрипта, если переменная является глобальной). @@ -188,12 +223,80 @@ sayHi(); В обоих примерах выше вызов `alert` происходил без ошибки, потому что переменная `phrase` уже существовала. Но её значение ещё не было присвоено, поэтому мы получали `undefined`. +## IIFE + +В прошлом, поскольку существовал только `var`, а он не имел блочной области видимости, программисты придумали способ её эмулировать. Этот способ получил название "Immediately-invoked function expressions" (сокращенно IIFE). + +Это не то, что мы должны использовать сегодня, но, так как вы можете встретить это в старых скриптах, полезно понимать принцип работы. + +IIFE выглядит следующим образом: + +```js run +(function() { + + var message = "Привет"; + + alert(message); // Привет + +})(); +``` + +Здесь создаётся и немедленно вызывается Function Expression. Так что код выполняется сразу же и у него есть свои локальные переменные. + +Function Expression обёрнуто в скобки `(function {...})`, потому что, когда JavaScript встречает `"function"` в основном потоке кода, он воспринимает это как начало Function Declaration. Но у Function Declaration должно быть имя, так что такой код вызовет ошибку: + +```js run +// Пробуем объявить и сразу же вызвать функцию +function() { // <-- SyntaxError: Function statements require a function name + + var message = "Привет"; + + alert(message); // Привет + +}(); +``` + +Даже если мы скажем: "хорошо, давайте добавим имя", -- это не сработает, потому что JavaScript не позволяет вызывать Function Declaration немедленно. + +```js run +// ошибка синтаксиса из-за скобок ниже +function go() { + +}(); // <-- нельзя вызывать Function Declaration немедленно +``` + +Так что скобки вокруг функции -- это трюк, который позволяет объяснить JavaScript, что функция была создана в контексте другого выражения, а значит, что это Function Expression: ей не нужно имя и её можно вызвать немедленно. + +Помимо круглых скобок существуют и другие способы сообщить JavaScript, что мы имеем в виду Function Expression: + +```js run +// Способы создания IIFE + +*!*(*/!*function() { + alert("Круглые скобки вокруг функции"); +}*!*)*/!*(); + +*!*(*/!*function() { + alert("Круглые скобки вокруг всего выражения"); +}()*!*)*/!*; + +*!*!*/!*function() { + alert("Выражение начинается с логического оператора НЕ"); +}(); + +*!*+*/!*function() { + alert("Выражение начинается с унарного плюса"); +}(); +``` + +Во всех перечисленных случаях мы объявляем Function Expression и немедленно запускаем его. Ещё раз отметим: в настоящее время необходимости писать подобный код нет. + ## Итого Существует 2 основных отличия `var` от `let/const`: 1. Переменные `var` не имеют блочной области видимости, они ограничены, как минимум, телом функции. -2. Объявления (инициализация) переменных `var`производится в начале исполнения функции (или скрипта для глобальных переменных). +2. Объявления (инициализация) переменных `var` производится в начале исполнения функции (или скрипта для глобальных переменных). Есть ещё одно небольшое отличие, относящееся к глобальному объекту, мы рассмотрим его в следующей главе. diff --git a/1-js/06-advanced-functions/05-global-object/article.md b/1-js/06-advanced-functions/05-global-object/article.md index a5e9f2b612..b1ff9361f1 100644 --- a/1-js/06-advanced-functions/05-global-object/article.md +++ b/1-js/06-advanced-functions/05-global-object/article.md @@ -5,7 +5,7 @@ В браузере он называется `window`, в Node.js — `global`, в другой среде исполнения может называться иначе. -Недавно `globalThis` был добавлен в язык как стандартизированное имя для глобального объекта, которое должно поддерживаться в любом окружении. В некоторых браузерах, например Edge не на Chromium, `globalThis` ещё не поддерживается, но легко реализуется с помощью полифила. +Недавно `globalThis` был добавлен в язык как стандартизированное имя для глобального объекта, которое должно поддерживаться в любом окружении. Он поддерживается во всех основных браузерах. Далее мы будем использовать `window`, полагая, что наша среда - браузер. Если скрипт может выполняться и в другом окружении, лучше будет `globalThis`. @@ -25,6 +25,8 @@ var gVar = 5; alert(window.gVar); // 5 (становится свойством глобального объекта) ``` +То же самое касается функций, объявленных с помощью синтаксиса Function Declaration (выражения с ключевым словом `function` в основном потоке кода, не Function Expression) + Пожалуйста, не полагайтесь на это. Такое поведение поддерживается для совместимости. В современных проектах, использующих [JavaScript-модули](info:modules), такого не происходит. Если бы мы объявили переменную при помощи `let`, то такого бы не произошло: @@ -69,7 +71,7 @@ if (!window.Promise) { Если нет (скажем, используется старый браузер), мы можем создать полифил: добавить функции, которые не поддерживаются окружением, но существуют в современном стандарте. -```js run +```js if (!window.Promise) { window.Promise = ... // собственная реализация современной возможности языка } @@ -82,7 +84,8 @@ if (!window.Promise) { Это включает в себя как встроенные объекты, например, `Array`, так и характерные для окружения свойства, например, `window.innerHeight` -- высота окна браузера. - Глобальный объект имеет универсальное имя -- `globalThis`. - ...Но чаще на него ссылаются по-старому, используя имя, характерное для данного окружения, такое как `window` (браузер) и `global` (Node.js). Так как `globalThis` появился недавно, он не поддерживается в IE и Edge (не-Chromium версия), но можно использовать полифил. + ...Но чаще на него ссылаются по-старому, используя имя, характерное для данного окружения, такое как `window` (браузер) и `global` (Node.js). + - Следует хранить значения в глобальном объекте, только если они действительно глобальны для нашего проекта. И стараться свести их количество к минимуму. - В браузерах, если только мы не используем [модули](info:modules), глобальные функции и переменные, объявленные с помощью `var`, становятся свойствами глобального объекта. - Для того, чтобы код был проще и в будущем его легче было поддерживать, следует обращаться к свойствам глобального объекта напрямую, как `window.x`. diff --git a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md index 06ec5e45fb..9f00d0d593 100644 --- a/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md +++ b/1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md @@ -4,7 +4,7 @@ importance: 5 # Установка и уменьшение значения счётчика -Измените код `makeCounter()` так, чтобы счётчик мог увеличивать и устанавливать значение: +Измените код `makeCounter()` так, чтобы счётчик мог уменьшать и устанавливать значение: - `counter()` должен возвращать следующее значение (как и раньше). - `counter.set(value)` должен устанавливать счётчику значение `value`. diff --git a/1-js/06-advanced-functions/06-function-object/article.md b/1-js/06-advanced-functions/06-function-object/article.md index 56e7452485..59b009bf66 100644 --- a/1-js/06-advanced-functions/06-function-object/article.md +++ b/1-js/06-advanced-functions/06-function-object/article.md @@ -67,7 +67,7 @@ alert(user.sayBye.name); // sayBye В этом нет никакой магии. Бывает, что корректное имя определить невозможно. В таких случаях свойство name имеет пустое значение. Например: -```js +```js run // функция объявлена внутри массива let arr = [function() {}]; @@ -91,7 +91,7 @@ alert(f2.length); // 2 alert(many.length); // 2 ``` -Как мы видим, троеточие, обозначающее "остаточные параметры", здесь как бы "не считается" +Как мы видим, троеточие, обозначающее "остаточные параметры", здесь как бы "не считается". Свойство `length` иногда используется для [интроспекций](https://ru.wikipedia.org/wiki/Интроспекция_(программирование)) в функциях, которые работают с другими функциями. @@ -348,4 +348,4 @@ welcome(); // Hello, Guest (вложенный вызов работает) Они создают "основную" функцию и добавляют множество "вспомогательных" функций внутрь первой. Например, библиотека [jQuery](https://jquery.com) создаёт функцию с именем `$`. Библиотека [lodash](https://lodash.com) создаёт функцию `_`, а потом добавляет в неё `_.clone`, `_.keyBy` и другие свойства (чтобы узнать о ней побольше см. [документацию](https://lodash.com/docs)). Они делают это, чтобы уменьшить засорение глобального пространства имён посредством того, что одна библиотека предоставляет только одну глобальную переменную, уменьшая вероятность конфликта имён. -Таким образом, функция может не только делать что-то сама по себе, но также и предоставлять полезный функционал через свои свойства. +Таким образом, функция может не только делать что-то сама по себе, но также и предоставлять полезную функциональность через свои свойства. diff --git a/1-js/06-advanced-functions/07-new-function/article.md b/1-js/06-advanced-functions/07-new-function/article.md index 62e966e462..47df9eee1a 100644 --- a/1-js/06-advanced-functions/07-new-function/article.md +++ b/1-js/06-advanced-functions/07-new-function/article.md @@ -1,7 +1,7 @@ # Синтаксис "new Function" -Существует ещё один вариант объявлять функции. Он используется крайне редко, но иногда другого решения не найти. +Существует ещё один вариант объявления функции. Он используется крайне редко, но иногда другого решения не найти. ## Синтаксис diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/article.md b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md index b085cc6128..3bf5997b0f 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/article.md +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md @@ -1,4 +1,4 @@ -# Планирование: setTimeout and setInterval +# Планирование: setTimeout и setInterval Мы можем вызвать функцию не в данный момент, а позже, через заданный интервал времени. Это называется "планирование вызова". @@ -14,7 +14,7 @@ Синтаксис: ```js -let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) +let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...); ``` Параметры: @@ -27,7 +27,7 @@ let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) : Задержка перед запуском в миллисекундах (1000 мс = 1 с). Значение по умолчанию - 0. `arg1`, `arg2`... -: Аргументы, передаваемые в функцию (не поддерживается в IE9-) +: Аргументы, передаваемые в функцию Например, данный код вызывает `sayHi()` спустя одну секунду: @@ -71,7 +71,7 @@ setTimeout(() => alert('Привет'), 1000); Начинающие разработчики иногда ошибаются, добавляя скобки `()` после функции: ```js -// не правильно! +// неправильно! setTimeout(sayHi(), 1000); ``` Это не работает, потому что `setTimeout` ожидает ссылку на функцию. Здесь `sayHi()` запускает выполнение функции, и *результат выполнения* отправляется в `setTimeout`. В нашем случае результатом выполнения `sayHi()` является `undefined` (так как функция ничего не возвращает), поэтому ничего не планируется. @@ -109,7 +109,7 @@ alert(timerId); // тот же идентификатор (не принимае Метод `setInterval` имеет такой же синтаксис как `setTimeout`: ```js -let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) +let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...); ``` Все аргументы имеют такое же значение. Но отличие этого метода от `setTimeout` в том, что функция запускается не один раз, а периодически через указанный интервал времени. @@ -132,11 +132,11 @@ setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); Так что если вы запустите код выше и подождёте с закрытием `alert` несколько секунд, то следующий `alert` будет показан сразу, как только вы закроете предыдущий. Интервал времени между сообщениями `alert` будет короче, чем 2 секунды. ``` -## Рекурсивный setTimeout +## Вложенный setTimeout Есть два способа запускать что-то регулярно. -Один из них `setInterval`. Другим является рекурсивный `setTimeout`. Например: +Один из них `setInterval`. Другим является вложенный `setTimeout`. Например: ```js /** вместо: @@ -153,7 +153,7 @@ let timerId = setTimeout(function tick() { Метод `setTimeout` выше планирует следующий вызов прямо после окончания текущего `(*)`. -Рекурсивный `setTimeout` - более гибкий метод, чем `setInterval`. С его помощью последующий вызов может быть задан по-разному в зависимости от результатов предыдущего. +Вложенный `setTimeout` - более гибкий метод, чем `setInterval`. С его помощью последующий вызов может быть задан по-разному в зависимости от результатов предыдущего. Например, необходимо написать сервис, который отправляет запрос для получения данных на сервер каждые 5 секунд, но если сервер перегружен, то необходимо увеличить интервал запросов до 10, 20, 40 секунд... Вот псевдокод: @@ -176,7 +176,7 @@ let timerId = setTimeout(function request() { А если функции, которые мы планируем, ресурсоёмкие и требуют времени, то мы можем измерить время, затраченное на выполнение, и спланировать следующий вызов раньше или позже. -**Рекурсивный `setTimeout` позволяет задать задержку между выполнениями более точно, чем `setInterval`.** +**Вложенный `setTimeout` позволяет задать задержку между выполнениями более точно, чем `setInterval`.** Сравним два фрагмента кода. Первый использует `setInterval`: @@ -187,7 +187,7 @@ setInterval(function() { }, 100); ``` -Второй использует рекурсивный `setTimeout`: +Второй использует вложенный `setTimeout`: ```js let i = 1; @@ -217,7 +217,7 @@ setTimeout(function run() { ![](settimeout-interval.svg) -**Рекурсивный`setTimeout` гарантирует фиксированную задержку (здесь 100 мс).** +**Вложенный`setTimeout` гарантирует фиксированную задержку (здесь 100 мс).** Это потому, что новый вызов планируется в конце предыдущего. @@ -289,7 +289,7 @@ setTimeout(function run() { - Методы `setInterval(func, delay, ...args)` и `setTimeout(func, delay, ...args)` позволяют выполнять `func` регулярно или только один раз после задержки `delay`, заданной в мс. - Для отмены выполнения необходимо вызвать `clearInterval/clearTimeout` со значением, которое возвращают методы `setInterval/setTimeout`. - Вложенный вызов `setTimeout` является более гибкой альтернативой `setInterval`. Также он позволяет более точно задать интервал между выполнениями. -- Планирование с нулевой задержкой `setTimeout(func,0)` или, что тоже самое, `setTimeout(func)` используется для вызовов, которые должны быть исполнены как можно скорее, после завершения исполнения текущего кода. +- Планирование с нулевой задержкой `setTimeout(func,0)` или, что то же самое, `setTimeout(func)` используется для вызовов, которые должны быть исполнены как можно скорее, после завершения исполнения текущего кода. - Браузер ограничивает 4-мя мс минимальную задержку между пятью и более вложенными вызовами `setTimeout`, а также для `setInterval`, начиная с 5-го вызова. Обратим внимание, что все методы планирования *не гарантируют* точную задержку. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg index 9a214c548f..bce7d6a843 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg @@ -1 +1 @@ -func(1)func(2)func(3)100200300 \ No newline at end of file +func(1)func(2)func(3)100200300 \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg index 13b22a892b..d6d233b2ba 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg @@ -1 +1 @@ -func(1)func(2)func(3)100100 \ No newline at end of file +func(1)func(2)func(3)100100 \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js index 9ef503703b..cdd9851be8 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/_js.view/solution.js @@ -1,11 +1,12 @@ function spy(func) { function wrapper(...args) { + // мы используем ...args вместо arguments для хранения "реального" массива в wrapper.calls wrapper.calls.push(args); - return func.apply(this, arguments); + return func.apply(this, args); } wrapper.calls = []; return wrapper; -} \ No newline at end of file +} diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md index fada16d163..83a16b7914 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/solution.md @@ -1 +1,2 @@ -Здесь мы можем использовать `call.push(args)` для хранения всех аргументов в списке и `f.apply(this, args)` для переадресации вызова. + +Обертка, возвращаемая `spy(f)`, должна хранить все аргументы, и затем использовать `f.apply` для переадресации вызова. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md index 5fbba081e5..9256d08393 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/01-spy-decorator/task.md @@ -27,4 +27,4 @@ for (let args of work.calls) { } ``` -P.S.: Этот декоратор иногда полезен для юнит-тестирования. Его расширенная форма - `sinon.spy` - содержится в библиотеке [Sinon.JS](http://sinonjs.org/). +P.S.: Этот декоратор иногда полезен для юнит-тестирования. Его расширенная форма - `sinon.spy` - содержится в библиотеке [Sinon.JS](https://sinonjs.org/). diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js index 065a77d1f9..3999156b7c 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js @@ -1,15 +1,7 @@ -function debounce(f, ms) { - - let isCooldown = false; - +function debounce(func, ms) { + let timeout; return function() { - if (isCooldown) return; - - f.apply(this, arguments); - - isCooldown = true; - - setTimeout(() => isCooldown = false, ms); + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, arguments), ms); }; - } \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js index 774bc9a6fa..bceaefee24 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js @@ -1,41 +1,47 @@ -describe("debounce", function() { - before(function() { +describe('debounce', function () { + before(function () { this.clock = sinon.useFakeTimers(); }); - after(function() { + after(function () { this.clock.restore(); }); - it("вызывает функцию один раз в 'ms' мс", function() { - let log = ''; + it('для одного вызова - запускается через "ms" миллисекунд', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); - function f(a) { - log += a; - } + debounced('test'); + assert(f.notCalled, 'не вызывается сразу'); + this.clock.tick(1000); + assert(f.calledOnceWith('test'), 'вызывается после 1000ms'); + }); - f = debounce(f, 1000); + it('для 3 вызовов - вызывает последний через "ms" миллисекунд', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); - f(1); // вызвана - f(2); // проигнорирована + debounced('a'); + setTimeout(() => debounced('b'), 200); // проигнорирована + setTimeout(() => debounced('c'), 500); // вызвана + this.clock.tick(1000); - setTimeout(() => f(3), 100); // проигнорирована (слишком рано) - setTimeout(() => f(4), 1100); // вызвана (1000 мс истекли) - setTimeout(() => f(5), 1500); // проигнорирована (менее 1000 мс с последнего вызова) + assert(f.notCalled, 'не вызывается после 1000ms'); - this.clock.tick(5000); - assert.equal(log, "14"); + this.clock.tick(500); + + assert(f.calledOnceWith('c'), 'вызывается после 1500ms'); }); - it("сохраняет контекст вызова", function() { + it('сохраняет контекст вызова', function () { let obj = { f() { assert.equal(this, obj); - } + }, }; obj.f = debounce(obj.f, 1000); - obj.f("test"); + obj.f('test'); + this.clock.tick(5000); }); - -}); +}); \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg new file mode 100644 index 0000000000..8e128ce74d --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg @@ -0,0 +1 @@ +200мс1500мс1000мс0cf(a)f(b)f(c)500мсвремявызовы: после 1000мс \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html new file mode 100644 index 0000000000..aab1a471bb --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html @@ -0,0 +1,24 @@ + + + +Функция handler вызывается на этом поле для ввода: +
+ + +

+ +А на этом поле функция handler вызывается с применением debounce – debounce(handler, 1000): +
+ + +

+ + + \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md index 82c2ecb5fe..5952020f11 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md @@ -1,27 +1,12 @@ ```js demo -function debounce(f, ms) { - - let isCooldown = false; - +function debounce(func, ms) { + let timeout; return function() { - if (isCooldown) return; - - f.apply(this, arguments); - - isCooldown = true; - - setTimeout(() => isCooldown = false, ms); + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, arguments), ms); }; - } -``` - -Вызов `debounce` возвращает обёртку. Возможны два состояния: -- `isCooldown = false` -- готова к выполнению. -- `isCooldown = true` -- ожидание окончания тайм-аута. -В первом вызове `isCoolDown = false`, поэтому вызов продолжается, и состояние изменяется на `true`. - -Пока `isCoolDown` имеет значение `true`, все остальные вызовы игнорируются. +``` -Затем `setTimeout` устанавливает его в `false` после заданной задержки. +Вызов `debounce` возвращает обёртку. При вызове он планирует вызов исходной функции через указанное количество `ms` и отменяет предыдущий такой тайм-аут. \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md index 9c87aa129f..bea131145e 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md @@ -4,20 +4,49 @@ importance: 5 # Декоратор debounce -Результатом декоратора `debounce(f, ms)` должна быть обёртка, которая передаёт вызов `f` не более одного раза в `ms` миллисекунд. -Другими словами, когда мы вызываем `debounce`, это гарантирует, что все остальные вызовы будут игнорироваться в течение `ms`. +Результат декоратора `debounce(f, ms)` – это обёртка, которая откладывает вызовы `f`, пока не пройдёт `ms` миллисекунд бездействия (без вызовов, «cooldown period»), а затем вызывает `f` один раз с последними аргументами. -Например: +Другими словами, `debounce` – это так называемый секретарь, который принимает «телефонные звонки», и ждёт, пока не пройдет `ms` миллисекунд тишины. И только после этого передает «начальнику» информацию о последнем звонке (вызывает непосредственно `f`). + +Например, у нас была функция `f` и мы заменили её на `f = debounce(f, 1000)`. + +Затем, если обёрнутая функция вызывается в 0, 200 и 500 мс, а потом вызовов нет, то фактическая `f` будет вызвана только один раз, в 1500 мс. То есть: по истечению 1000 мс от последнего вызова. + +![](debounce.svg) + +...И она получит аргументы самого последнего вызова, остальные вызовы игнорируются. + +Ниже код этого примера (используется декоратор debounce из библиотеки [Lodash](https://lodash.com/docs/4.17.15#debounce)): ```js no-beautify -let f = debounce(alert, 1000); +let f = _.debounce(alert, 1000); + +f("a"); +setTimeout( () => f("b"), 200); +setTimeout( () => f("c"), 500); + +// Обёрнутая в debounce функция ждёт 1000 мс после последнего вызова, а затем запускает: alert("c") +``` -f(1); // выполняется немедленно -f(2); // проигнорирован +Теперь практический пример. Предположим, пользователь набирает какой-то текст, и мы хотим отправить запрос на сервер, когда ввод этого текста будет завершён. -setTimeout( () => f(3), 100); // проигнорирован (прошло только 100 мс) -setTimeout( () => f(4), 1100); // выполняется -setTimeout( () => f(5), 1500); // проигнорирован (прошло только 400 мс от последнего вызова) +Нет смысла отправлять запрос для каждого набранного символа. Вместо этого мы хотели бы подождать, а затем обработать весь результат. + +В браузере мы можем настроить обработчик событий – функцию, которая вызывается при каждом изменении поля для ввода. Обычно обработчик событий вызывается очень часто, для каждого набранного символа. Но если мы воспользуемся `debounce` на 1000мс, то он будет вызван только один раз, через 1000мс после последнего ввода символа. + +```online + +В этом живом примере обработчик помещает результат в поле ниже, попробуйте: + +[iframe border=1 src="debounce" height=200] + +Видите? На втором поле вызывается функция, обёрнутая в `debounce`, поэтому его содержимое обрабатывается через 1000мс с момента последнего ввода. ``` -На практике `debounce` полезен для функций, которые получают/обновляют данные, и мы знаем, что повторный вызов в течение короткого промежутка времени не даст ничего нового. Так что лучше не тратить на него ресурсы. +Таким образом, `debounce` – это отличный способ обработать последовательность событий: будь то последовательность нажатий клавиш, движений мыши или ещё что-либо. + +Он ждёт заданное время после последнего вызова, а затем запускает свою функцию, которая может обработать результат. + +Задача — реализовать декоратор `debounce`. + +Подсказка: это всего лишь несколько строк, если вдуматься :) \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md index 4d64b9d427..1f1a8c24c1 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/solution.md @@ -34,6 +34,6 @@ function throttle(func, ms) { 1. Во время первого вызова обёртка просто вызывает `func` и устанавливает состояние задержки (`isThrottled = true`). 2. В этом состоянии все вызовы запоминаются в `saveArgs / saveThis`. Обратите внимание, что контекст и аргументы одинаково важны и должны быть запомнены. Они нам нужны для того, чтобы воспроизвести вызов позднее. -3. ... Затем по прошествии `ms` миллисекунд срабатывает `setTimeout`. Состояние задержки сбрасывается (`isThrottled = false`). И если мы проигнорировали вызовы, то "обёртка" выполняется с последними запомненными аргументами и контекстом. +3. Затем по прошествии `ms` миллисекунд срабатывает `setTimeout`. Состояние задержки сбрасывается (`isThrottled = false`). И если мы проигнорировали вызовы, то "обёртка" выполняется с последними запомненными аргументами и контекстом. На третьем шаге выполняется не `func`, а `wrapper`, потому что нам нужно не только выполнить `func`, но и ещё раз установить состояние задержки и таймаут для его сброса. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md index 7f9769a60f..f6f7aab282 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md @@ -4,29 +4,32 @@ importance: 5 # Тормозящий (throttling) декоратор -Создайте "тормозящий" декоратор `throttle(f, ms)`, который возвращает обёртку, передавая вызов в `f` не более одного раза в `ms` миллисекунд. Те вызовы, которые попадают в период "торможения", игнорируются. +Создайте «тормозящий» декоратор `throttle(f, ms)`, который возвращает обёртку. -**Отличие от `debounce` - если проигнорированный вызов является последним во время "задержки", то он выполняется в конце.** +При многократном вызове он передает вызов `f` не чаще одного раза в `ms` миллисекунд. -Давайте рассмотрим реальное применение, чтобы лучше понять это требование и выяснить, откуда оно взято. +По сравнению с декоратором `debounce` поведение совершенно другое: +- `debounce` запускает функцию один раз после периода «бездействия». Подходит для обработки конечного результата. +- `throttle` запускает функцию не чаще, чем указанное время `ms`. Подходит для регулярных обновлений, которые не должны быть слишком частыми. -**Например, мы хотим отслеживать движения мыши.** +Другими словами, `throttle` похож на секретаря, который принимает телефонные звонки, но при этом беспокоит начальника (вызывает непосредственно `f`) не чаще, чем один раз в `ms` миллисекунд. +Давайте рассмотрим реальное применение, чтобы лучше понять это требование и выяснить, откуда оно взято. -В браузере мы можем объявить функцию, которая будет запускаться при каждом движении указателя и получать его местоположение. Во время активного использования мыши эта функция запускается очень часто, это может происходить около 100 раз в секунду (каждые 10 мс). +**Например, мы хотим отслеживать движения мыши.** -**Мы бы хотели обновлять информацию на странице при передвижениях.** +В браузере мы можем реализовать функцию, которая будет запускаться при каждом перемещении указателя и получать его местоположение. Во время активного использования мыши эта функция запускается очень часто, что-то около 100 раз в секунду (каждые 10 мс). **Мы бы хотели обновлять некоторую информацию на странице при передвижении указателя.** -...Но функция обновления `update()` слишком ресурсоёмкая, чтобы делать это при каждом микродвижении. Да и нет смысла делать обновление чаще, чем один раз в 100 мс. +...Но функция обновления `update()` слишком ресурсоёмкая, чтобы делать это при каждом микродвижении. Да и нет смысла делать обновление чаще, чем один раз в 1000 мс. -Поэтому мы обернём вызов в декоратор: будем использовать `throttle(update, 100)` как функцию, которая будет запускаться при каждом перемещении указателя вместо оригинальной `update()`. Декоратор будет вызываться часто, но передавать вызов в `update()` максимум раз в 100 мс. +Поэтому мы обернём вызов в декоратор: будем использовать `throttle(update, 1000)` как функцию, которая будет запускаться при каждом перемещении указателя вместо оригинальной `update()`. Декоратор будет вызываться часто, но передавать вызов в `update()` максимум раз в 1000 мс. Визуально это будет выглядеть вот так: 1. Для первого движения указателя декорированный вариант сразу передаёт вызов в `update`. Это важно, т.к. пользователь сразу видит нашу реакцию на его перемещение. -2. Затем, когда указатель продолжает движение, в течение 100 мс ничего не происходит. Декорированный вариант игнорирует вызовы. -3. По истечению 100 мс происходит ещё один вызов `update` с последними координатами. -4. Затем, наконец, указатель где-то останавливается. Декорированный вариант ждёт, пока не истечёт 100 мс, и затем вызывает `update` с последними координатами. В итоге окончательные координаты указателя тоже обработаны. +2. Затем, когда указатель продолжает движение, в течение 1000 мс ничего не происходит. Декорированный вариант игнорирует вызовы. +3. По истечению 1000 мс происходит ещё один вызов `update` с последними координатами. +4. Затем, наконец, указатель где-то останавливается. Декорированный вариант ждёт, пока не истечёт 1000 мс, и затем вызывает `update` с последними координатами. В итоге окончательные координаты указателя тоже обработаны. Пример кода: diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/article.md b/1-js/06-advanced-functions/09-call-apply-decorators/article.md index f481024e36..6fc33f17f8 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/article.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/article.md @@ -1,4 +1,4 @@ -# Декораторы и переадресация вызова, сall/apply +# Декораторы и переадресация вызова, call/apply JavaScript предоставляет исключительно гибкие возможности по работе с функциями: они могут быть переданы в другие функции, использованы как объекты, и сейчас мы рассмотрим, как *перенаправлять* вызовы между ними и как их декорировать. @@ -45,7 +45,7 @@ alert( "Again: " + slow(2) ); // возвращаем из кеша В коде выше `cachingDecorator` -- это *декоратор*, специальная функция, которая принимает другую функцию и изменяет её поведение. -Идея состоит в том, что мы можем вызвать `cachingDecorator` с любой функцией, в результате чего мы получим кеширующую обёртку. Это здорово, т.к. у нас может быть множество функций, использующих такой функционал, и всё, что нам нужно сделать - это применить к ним `cachingDecorator`. +Идея состоит в том, что мы можем вызвать `cachingDecorator` с любой функцией, в результате чего мы получим кеширующую обёртку. Это здорово, т.к. у нас может быть множество функций, использующих такую функциональность, и всё, что нам нужно сделать - это применить к ним `cachingDecorator`. Отделяя кеширующий код от основного кода, мы также сохраняем чистоту и простоту последнего. @@ -62,7 +62,7 @@ alert( "Again: " + slow(2) ); // возвращаем из кеша - При необходимости мы можем объединить несколько декораторов (речь об этом пойдёт позже). -## Применение "func.call" для передачи контекста. +## Применение "func.call" для передачи контекста Упомянутый выше кеширующий декоратор не подходит для работы с методами объектов. @@ -151,8 +151,8 @@ let user = { name: "John" }; let admin = { name: "Admin" }; // используем 'call' для передачи различных объектов в качестве 'this' -sayHi.call( user ); // this = John -sayHi.call( admin ); // this = Admin +sayHi.call( user ); // John +sayHi.call( admin ); // Admin ``` Здесь мы используем `call` для вызова `say` с заданным контекстом и фразой: diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg index ccf55d88d5..3d8428b273 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg +++ b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg @@ -1 +1 @@ -обёрткавокруг функции \ No newline at end of file +обёрткавокруг функции \ No newline at end of file diff --git a/1-js/06-advanced-functions/10-bind/article.md b/1-js/06-advanced-functions/10-bind/article.md index 9b1c8d4731..1c597cdf12 100644 --- a/1-js/06-advanced-functions/10-bind/article.md +++ b/1-js/06-advanced-functions/10-bind/article.md @@ -191,7 +191,7 @@ for (let key in user) { } ``` -Некоторые JS-библиотеки предоставляют встроенные функции для удобной массовой привязки контекста, например [_.bindAll(obj)](http://lodash.com/docs#bindAll) в lodash. +Некоторые JS-библиотеки предоставляют встроенные функции для удобной массовой привязки контекста, например [_.bindAll(obj)](https://lodash.com/docs#bindAll) в lodash. ```` ## Частичное применение diff --git a/1-js/06-advanced-functions/12-arrow-functions/article.md b/1-js/06-advanced-functions/12-arrow-functions/article.md index 088c58b287..ab10c47396 100644 --- a/1-js/06-advanced-functions/12-arrow-functions/article.md +++ b/1-js/06-advanced-functions/12-arrow-functions/article.md @@ -79,7 +79,7 @@ group.showList(); ## Стрелочные функции не имеют "arguments" -У стрелочных функции также нет переменной `arguments`. +У стрелочных функций также нет переменной `arguments`. Это отлично подходит для декораторов, когда нам нужно пробросить вызов с текущими `this` и `arguments`. diff --git a/1-js/07-object-properties/01-property-descriptors/article.md b/1-js/07-object-properties/01-property-descriptors/article.md index 7eb9378f98..40dbc8f601 100644 --- a/1-js/07-object-properties/01-property-descriptors/article.md +++ b/1-js/07-object-properties/01-property-descriptors/article.md @@ -194,7 +194,7 @@ alert(Object.keys(user)); // name Флаг неконфигурируемого свойства (`configurable:false`) иногда предустановлен для некоторых встроенных объектов и свойств. -Неконфигурируемое свойство не может быть удалено или изменено с помощью `defineProperty`. +Неконфигурируемое свойство не может быть удалено, его атрибуты не могут быть изменены. Например, свойство `Math.PI` - только для чтения, неперечислимое и неконфигурируемое: @@ -214,36 +214,58 @@ alert( JSON.stringify(descriptor, null, 2 ) ); То есть программист не сможет изменить значение `Math.PI` или перезаписать его. ```js run -Math.PI = 3; // Ошибка +Math.PI = 3; // Ошибка, потому что writable: false // delete Math.PI тоже не сработает ``` +Мы также не можем изменить `writable`: -Определение свойства как неконфигурируемого - это дорога в один конец. Мы не сможем отменить это действие, потому что `defineProperty` не работает с неконфигурируемыми свойствами. +```js run +// Ошибка, из-за configurable: false +Object.defineProperty(Math, "PI", { writable: true }); +``` +Мы абсолютно ничего не можем сделать с `Math.PI`. + +Определение свойства как неконфигурируемого - это дорога в один конец. Мы не можем изменить его обратно с помощью `defineProperty`. -В коде ниже мы делаем `user.name` "навечно запечатанной" константой: +**Обратите внимание: `configurable: false` не даст изменить флаги свойства, а также не даст его удалить. При этом можно изменить значение свойства.** + +В коде ниже свойство `user.name` является неконфигурируемым, но мы все ещё можем изменить его значение (т.к. `writable: true`). ```js run -let user = { }; +let user = { + name: "John" +}; + +Object.defineProperty(user, "name", { + configurable: false +}); + +user.name = "Pete"; // работает +delete user.name; // Ошибка +``` +А здесь мы делаем `user.name` "навечно запечатанной" константой, как и встроенный `Math.PI`: +```js run +let user = { + name: "John" +}; Object.defineProperty(user, "name", { - value: "John", writable: false, configurable: false }); -*!* // теперь невозможно изменить user.name или его флаги // всё это не будет работать: -// user.name = "Pete" -// delete user.name -// defineProperty(user, "name", ...) -Object.defineProperty(user, "name", {writable: true}); // Ошибка -*/!* -``` +user.name = "Pete"; +delete user.name; +Object.defineProperty(user, "name", { value: "Pete" }); +``` + +```smart header="Единственное возможное изменение атрибута: writable true -> false" +Есть небольшое исключение, касающееся изменения флагов. -```smart header="Ошибки отображаются только в строгом режиме" -В нестрогом режиме мы не увидим никаких ошибок при записи в свойства "только для чтения" и т.п. Эти операции всё равно не будут выполнены успешно. Действия, нарушающие ограничения флагов, в нестрогом режиме просто молча игнорируются. +Мы можем изменить `writable: true` на `false` для неконфигурируемого свойства, тем самым предотвратив изменение его значения (чтобы добавить ещё один уровень защиты). Однако не наоборот. ``` ## Метод Object.defineProperties @@ -292,7 +314,7 @@ for (let key in user) { ...Но это не копирует флаги. Так что если нам нужен клон "получше", предпочтительнее использовать `Object.defineProperties`. -Другое отличие в том, что `for..in` игнорирует символьные свойства, а `Object.getOwnPropertyDescriptors` возвращает дескрипторы *всех* свойств, включая свойства-символы. +Другое отличие в том, что `for..in` игнорирует символьные и неперечислимые свойства, а `Object.getOwnPropertyDescriptors` возвращает дескрипторы *всех* свойств. ## Глобальное запечатывание объекта diff --git a/1-js/07-object-properties/02-property-accessors/article.md b/1-js/07-object-properties/02-property-accessors/article.md index 5388614b82..5be8d15a1b 100644 --- a/1-js/07-object-properties/02-property-accessors/article.md +++ b/1-js/07-object-properties/02-property-accessors/article.md @@ -55,6 +55,8 @@ alert(user.fullName); // John Smith Снаружи свойство-аксессор выглядит как обычное свойство. В этом и заключается смысл свойств-аксессоров. Мы не *вызываем* `user.fullName` как функцию, а *читаем* как обычное свойство: геттер выполнит всю работу за кулисами. +На данный момент у `fullName` есть только геттер. Если мы попытаемся назначить `user.fullName=`, произойдёт ошибка: + ```js run let user = { get fullName() { @@ -92,12 +94,7 @@ alert(user.name); // Alice alert(user.surname); // Cooper ``` -В итоге мы получили "виртуальное" свойство `fullName`. Его можно прочитать и изменить, но по факту его не существует. - -```smart header="Нет поддержки для `delete`" -При попытке удалить свойство-аксессор оператором `delete` будет ошибка. -``` - +В итоге мы получили "виртуальное" свойство `fullName`. Его можно прочитать и изменить. ## Дескрипторы свойств доступа diff --git a/1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/task.md b/1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/task.md index 5f9791ee3e..624b27c3c6 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/task.md +++ b/1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/task.md @@ -12,6 +12,7 @@ importance: 5 let animal = { jumps: null }; + let rabbit = { __proto__: animal, jumps: true diff --git a/1-js/08-prototypes/01-prototype-inheritance/article.md b/1-js/08-prototypes/01-prototype-inheritance/article.md index d2390c82ef..b02f250839 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/article.md +++ b/1-js/08-prototypes/01-prototype-inheritance/article.md @@ -18,10 +18,11 @@ Одним из них является использование `__proto__`, например так: -```js run +```js let animal = { eats: true }; + let rabbit = { jumps: true }; @@ -31,14 +32,6 @@ rabbit.__proto__ = animal; */!* ``` -```smart header="Свойство `__proto__` — исторически обусловленный геттер/сеттер для `[[Prototype]]`" -Обратите внимание, что `__proto__` — *не то же самое*, что `[[Prototype]]`. Это геттер/сеттер для него. - -Он существует по историческим причинам, в современном языке его заменяют функции `Object.getPrototypeOf/Object.setPrototypeOf`, которые также получают/устанавливают прототип. Мы рассмотрим причины этого и сами функции позже. - -По спецификации `__proto__` должен поддерживаться только браузерами, но по факту все среды, включая серверную, поддерживают его. Далее мы будем в примерах использовать `__proto__`, так как это самый короткий и интуитивно понятный способ установки и чтения прототипа. -``` - Если мы ищем свойство в `rabbit`, а оно отсутствует, JavaScript автоматически берёт его из `animal`. Например: @@ -47,6 +40,7 @@ rabbit.__proto__ = animal; let animal = { eats: true }; + let rabbit = { jumps: true }; @@ -130,12 +124,26 @@ alert(longEar.jumps); // true (из rabbit) ![](proto-animal-rabbit-chain.svg) +Теперь, если мы прочтём что-нибудь из `longEar`, и оно будет отсутствовать, JavaScript будет искать его в `rabbit`, а затем в `animal`. + Есть только два ограничения: 1. Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить `__proto__` по кругу. 2. Значение `__proto__` может быть объектом или `null`. Другие типы игнорируются. -Это вполне очевидно, но всё же: может быть только один `[[Prototype]]`. Объект не может наследовать от двух других объектов. +Это вполне очевидно, но всё же: может быть только один `[[Prototype]]`. Объект не может наследоваться от двух других объектов. + +```smart header="Свойство `__proto__` — исторически обусловленный геттер/сеттер для `[[Prototype]]`" +Это распространённая ошибка начинающих разработчиков - не знать разницы между этими двумя понятиями. + +Обратите внимание, что `__proto__` — *не то же самое*, что внутреннее свойство `[[Prototype]]`. Это геттер/сеттер для `[[Prototype]]`. Позже мы увидим ситуации, когда это имеет значение, а пока давайте просто будем иметь это в виду, поскольку мы строим наше понимание языка JavaScript. + +Свойство `__proto__` немного устарело, оно существует по историческим причинам. Современный JavaScript предполагает, что мы должны использовать функции `Object.getPrototypeOf/Object.setPrototypeOf` вместо того, чтобы получать/устанавливать прототип. Мы также рассмотрим эти функции позже. + +По спецификации `__proto__` должен поддерживаться только браузерами, но по факту все среды, включая серверную, поддерживают его. Так что мы вполне безопасно его используем. + +Далее мы будем в примерах использовать `__proto__`, так как это самый короткий и интуитивно понятный способ установки и чтения прототипа. +``` ## Операция записи не использует прототип @@ -170,7 +178,7 @@ rabbit.walk(); // Rabbit! Bounce-bounce! ![](proto-animal-rabbit-walk-2.svg) -Свойства-аксессоры - исключение, так как запись в него обрабатывается функцией-сеттером. То есть, это, фактически, вызов функции. +Свойства-аксессоры - исключение, так как запись в него обрабатывается функцией-сеттером. То есть это фактически вызов функции. По этой причине `admin.fullName` работает корректно в приведённом ниже коде: @@ -308,11 +316,11 @@ for(let prop in rabbit) { ![](rabbit-animal-object.svg) -Заметим ещё одну деталь. Откуда взялся метод `rabbit.hasOwnProperty`? Мы его явно не определяли. Если посмотреть на цепочку прототипов, то видно, что он берётся из `Object.prototype.hasOwnProperty`. То есть, он унаследован. +Заметим ещё одну деталь. Откуда взялся метод `rabbit.hasOwnProperty`? Мы его явно не определяли. Если посмотреть на цепочку прототипов, то видно, что он берётся из `Object.prototype.hasOwnProperty`. То есть он унаследован. ...Но почему `hasOwnProperty` не появляется в цикле `for..in` в отличие от `eats` и `jumps`? Он ведь перечисляет все унаследованные свойства. -Ответ простой: оно не перечислимо. То есть, у него внутренний флаг `enumerable` стоит `false`, как и у других свойств `Object.prototype`. Поэтому оно и не появляется в цикле. +Ответ простой: оно не перечислимо. То есть у него внутренний флаг `enumerable` стоит `false`, как и у других свойств `Object.prototype`. Поэтому оно и не появляется в цикле. ```smart header="Почти все остальные методы получения ключей/значений игнорируют унаследованные свойства" Почти все остальные методы, получающие ключи/значения, такие как `Object.keys`, `Object.values` и другие - игнорируют унаследованные свойства. @@ -323,7 +331,7 @@ for(let prop in rabbit) { ## Итого - В JavaScript все объекты имеют скрытое свойство `[[Prototype]]`, которое является либо другим объектом, либо `null`. -- Мы можем использовать `obj.__ proto__` для доступа к нему (исторически обусловленный геттер/сеттер, есть другие способы, которые скоро будут рассмотрены). +- Мы можем использовать `obj.__proto__` для доступа к нему (исторически обусловленный геттер/сеттер, есть другие способы, которые скоро будут рассмотрены). - Объект, на который ссылается `[[Prototype]]`, называется "прототипом". - Если мы хотим прочитать свойство `obj` или вызвать метод, которого не существует у `obj`, тогда JavaScript попытается найти его в прототипе. - Операции записи/удаления работают непосредственно с объектом, они не используют прототип (если это обычное свойство, а не сеттер). diff --git a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg index dc53f56636..982f2e4c17 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg @@ -1 +1 @@ -прототип objectobject[[Prototype]] \ No newline at end of file +прототип objectobject[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg index 3e81f262b5..4bf580ae77 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg @@ -1 +1 @@ -eats: true walk: functionanimaljumps: truerabbit[[Prototype]]earLength: 10longEar[[Prototype]] \ No newline at end of file +eats: true walk: functionanimaljumps: truerabbit[[Prototype]]earLength: 10longEar[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg index 3cf5c4c78e..838c78395b 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg @@ -1 +1 @@ -eats: true walk: functionanimalwalk: functionrabbit[[Prototype]] \ No newline at end of file +eats: true walk: functionanimalwalk: functionrabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg index acd4206319..e994331dab 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg @@ -1 +1 @@ -walk: function sleep: functionanimalrabbit[[Prototype]]name: "White Rabbit" isSleeping: true \ No newline at end of file +walk: function sleep: functionanimalrabbit[[Prototype]]name: "Белый кролик" isSleeping: true \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg index ebdef95877..b324710286 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg @@ -1 +1 @@ -eats: true walk: functionanimaljumps: truerabbit[[Prototype]] \ No newline at end of file +eats: true walk: functionanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg index 735e1f2b36..4f3c1bc0ec 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg @@ -1 +1 @@ -eats: trueanimaljumps: truerabbit[[Prototype]] \ No newline at end of file +eats: trueanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg index 433bc613f9..bf0baf013a 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg @@ -1 +1 @@ -name: "John" surname: "Smith" set fullName: functionisAdmin: true name: "Alice" surname: "Cooper"useradmin[[Prototype]] \ No newline at end of file +name: "John" surname: "Smith" set fullName: functionisAdmin: true name: "Alice" surname: "Cooper"useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg index d32585b431..32a9858f83 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg @@ -1 +1 @@ -toString: function hasOwnProperty: function ...Object.prototypeanimal[[Prototype]][[Prototype]][[Prototype]]nulleats: truerabbitjumps: true \ No newline at end of file +toString: function hasOwnProperty: function ...Object.prototypeanimal[[Prototype]][[Prototype]][[Prototype]]nulleats: truerabbitjumps: true \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md b/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md index ca15792760..561330a679 100644 --- a/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md +++ b/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md @@ -39,6 +39,11 @@ alert( user2.name ); // undefined 1. Сначала ищется свойство `constructor` в объекте `user`. Не нашлось. 2. Потом задействуется поиск по цепочке прототипов. Прототип объекта `user` -- это `User.prototype`, и там тоже нет искомого свойства. -3. Значение `User.prototype` -- это пустой объект `{}`, чей прототип -- `Object.prototype`. `Object.prototype.constructor == Object`. Таким образом, свойство `constructor` всё-таки найдено. +3. Идя дальше по цепочке, значение `User.prototype` -- это пустой объект `{}`, чей прототип -- встроенный `Object.prototype`. +4. Наконец, для встроенного `Object.prototype` предусмотрен встроенный `Object.prototype.constructor == Object`. Таким образом, свойство `constructor` всё-таки найдено. -Наконец срабатывает `let user2 = new Object('Pete')`, но конструктор `Object` игнорирует аргументы, он всегда создаёт пустой объект: `let user2 = {}` -- это как раз то, чему равен `user2` в итоге. +В итоге срабатывает `let user2 = new Object('Pete')`. + +Вероятно, это не то, что нам нужно. Мы хотели создать `new User`, а не `new Object`. Это и есть результат отсутствия конструктора. + +(На всякий случай, если вам интересно, вызов `new Object(...)` преобразует свой аргумент в объект. Это теоретическая вещь, на практике никто не вызывает `new Object` со значением, тем более, в основном мы вообще не используем `new Object` для создания объектов). diff --git a/1-js/08-prototypes/02-function-prototype/article.md b/1-js/08-prototypes/02-function-prototype/article.md index fd3ab2f6ce..cba991f886 100644 --- a/1-js/08-prototypes/02-function-prototype/article.md +++ b/1-js/08-prototypes/02-function-prototype/article.md @@ -40,15 +40,15 @@ alert( rabbit.eats ); // true На изображении: `"prototype"` - горизонтальная стрелка, обозначающая обычное свойство для `"F"`, а `[[Prototype]]` -- вертикальная, обозначающая наследование `rabbit` от `animal`. -```smart header="`F.prototype` используется только в момент вызова `new F()`" -`F.prototype` используется только при вызове `new F()` и присваивается в качестве свойства `[[Prototype]]` нового объекта. После этого `F.prototype` и новый объект ничего не связывает. Следует понимать это как "единоразовый подарок" объекту. +```smart header="`F.prototype` используется только в момент вызова `new F`" +`F.prototype` используется только при вызове `new F` и присваивается в качестве свойства `[[Prototype]]` нового объекта. -После создания `F.prototype` может измениться, и новые объекты, созданные с помощью `new F()`, будут иметь другой объект в качестве `[[Prototype]]`, но уже существующие объекты сохранят старый. +Если после создания свойство `F.prototype` изменится (`F.prototype = <другой объект>`), то новые объекты, созданные с помощью `new F`, будут иметь в качестве `[[Prototype]]` другой объект, а уже существующие объекты сохранят старый. ``` ## F.prototype по умолчанию, свойство constructor -У каждой функции по умолчанию уже есть свойство `"prototype"`. +У каждой функции (за исключением стрелочных) по умолчанию уже есть свойство `"prototype"`. По умолчанию `"prototype"` - объект с единственным свойством `constructor`, которое ссылается на функцию-конструктор. @@ -160,7 +160,7 @@ Rabbit.prototype = { Всё достаточно просто. Выделим основные моменты: -- Свойство `F.prototype` (не путать с `[[Prototype]]`) устанавливает`[[Prototype]]` для новых объектов при вызове `new F()`. +- Свойство `F.prototype` (не путать с `[[Prototype]]`) устанавливает `[[Prototype]]` для новых объектов при вызове `new F()`. - Значение `F.prototype` должно быть либо объектом, либо `null`. Другие значения не будут работать. - Свойство `"prototype"` является особым, только когда оно назначено функции-конструктору, которая вызывается оператором `new`. diff --git a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg index 35cdc61f1d..1fced82b39 100644 --- a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg +++ b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg @@ -1 +1 @@ -Rabbitprototypeconstructordefault "prototype" \ No newline at end of file +Rabbitprototypeconstructor"prototype" по умолчанию \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg index 3489ecdd8c..ede4e1227e 100644 --- a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg +++ b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg @@ -1 +1 @@ -eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file +eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg index 3e11f275b0..2424ea648d 100644 --- a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg +++ b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg @@ -1 +1 @@ -default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file +"prototype" по умолчаниюRabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/article.md b/1-js/08-prototypes/03-native-prototypes/article.md index efad27e4e2..7e365b3062 100644 --- a/1-js/08-prototypes/03-native-prototypes/article.md +++ b/1-js/08-prototypes/03-native-prototypes/article.md @@ -2,7 +2,7 @@ Свойство `"prototype"` широко используется внутри самого языка JavaScript. Все встроенные функции-конструкторы используют его. -Сначала мы рассмотрим детали, а затем используем `"prototype"` для добавления встроенным объектам нового функционала. +Сначала мы рассмотрим детали, а затем используем `"prototype"` для добавления встроенным объектам новой функциональности. ## Object.prototype @@ -21,7 +21,7 @@ alert( obj ); // "[object Object]" ? ![](object-prototype.svg) -Когда вызывается `new Object()` (или создаётся объект с помощью литерала `{...}`), свойство `[[Prototype]]` этого объекта устанавливается на `Object.prototype` по правилам, который мы обсуждали в предыдущей главе: +Когда вызывается `new Object()` (или создаётся объект с помощью литерала `{...}`), свойство `[[Prototype]]` этого объекта устанавливается на `Object.prototype` по правилам, которые мы обсуждали в предыдущей главе: ![](object-prototype-1.svg) @@ -33,7 +33,7 @@ alert( obj ); // "[object Object]" ? let obj = {}; alert(obj.__proto__ === Object.prototype); // true -// obj.toString === obj.__proto__.toString == Object.prototype.toString +// obj.toString === obj.__proto__.toString === Object.prototype.toString ``` Обратите внимание, что по цепочке прототипов выше `Object.prototype` больше нет свойства `[[Prototype]]`: @@ -124,7 +124,7 @@ String.prototype.show = function() { ```warn Прототипы глобальны, поэтому очень легко могут возникнуть конфликты. Если две библиотеки добавляют метод `String.prototype.show`, то одна из них перепишет метод другой. -Так что, в общем, изменение встроенных прототипов считается плохой идей. +Так что, в общем, изменение встроенных прототипов считается плохой идеей. ``` **В современном программировании есть только один случай, в котором одобряется изменение встроенных прототипов. Это создание полифилов.** diff --git a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg index 35cdc61f1d..1fced82b39 100644 --- a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg +++ b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg @@ -1 +1 @@ -Rabbitprototypeconstructordefault "prototype" \ No newline at end of file +Rabbitprototypeconstructor"prototype" по умолчанию \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg index 770c908c5e..ebb4f32051 100644 --- a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg @@ -1 +1 @@ -toString: function ...Array.prototypetoString: function ...Object.prototype[[Prototype]][[Prototype]][1, 2, 3] \ No newline at end of file +toString: function ...Array.prototypetoString: function ...Object.prototype[[Prototype]][[Prototype]][1, 2, 3] \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg index 4989df56ba..4d6129e0a0 100644 --- a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg @@ -1 +1 @@ -toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file +toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg index 38c33cae16..9630e68e27 100644 --- a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg @@ -1 +1 @@ -constructor: Object toString: function ...Object.prototypeObjectobj = new Object()[[Prototype]]prototype \ No newline at end of file +constructor: Object toString: function ...Object.prototypeObjectobj = new Object()[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg index 858f8317ee..9ccb342299 100644 --- a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg @@ -1 +1 @@ -obj[[Prototype]]null \ No newline at end of file +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg index 8d3d0beeec..024dd30213 100644 --- a/1-js/08-prototypes/03-native-prototypes/object-prototype.svg +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg @@ -1 +1 @@ -constructor: Object toString: function ...Object.prototypeObjectprototype \ No newline at end of file +constructor: Object toString: function ...Object.prototypeObjectprototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg b/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg index 3489ecdd8c..ede4e1227e 100644 --- a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg +++ b/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg @@ -1 +1 @@ -eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file +eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg index 3e11f275b0..2424ea648d 100644 --- a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg +++ b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg @@ -1 +1 @@ -default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file +"prototype" по умолчаниюRabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/3-compare-calls/task.md b/1-js/08-prototypes/04-prototype-methods/3-compare-calls/task.md index 235b4d11ab..7e9fc4a414 100644 --- a/1-js/08-prototypes/04-prototype-methods/3-compare-calls/task.md +++ b/1-js/08-prototypes/04-prototype-methods/3-compare-calls/task.md @@ -17,7 +17,7 @@ Rabbit.prototype.sayHi = function() { let rabbit = new Rabbit("Rabbit"); ``` -Все эти вызовы делают одно и тоже или нет? +Все эти вызовы делают одно и то же или нет? ```js rabbit.sayHi(); diff --git a/1-js/08-prototypes/04-prototype-methods/article.md b/1-js/08-prototypes/04-prototype-methods/article.md index bd0ee42c16..5403e47e5d 100644 --- a/1-js/08-prototypes/04-prototype-methods/article.md +++ b/1-js/08-prototypes/04-prototype-methods/article.md @@ -7,7 +7,7 @@ Современные же методы это: -- [Object.create(proto, [descriptors])](mdn:js/Object/create) -- создаёт пустой объект со свойством `[[Prototype]]`, указанным как `proto`, и необязательными дескрипторами свойств `descriptors`. +- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- создаёт пустой объект со свойством `[[Prototype]]`, указанным как `proto`, и необязательными дескрипторами свойств `descriptors`. - [Object.getPrototypeOf(obj)](mdn:js/Object/getPrototypeOf) -- возвращает свойство `[[Prototype]]` объекта `obj`. - [Object.setPrototypeOf(obj, proto)](mdn:js/Object/setPrototypeOf) -- устанавливает свойство `[[Prototype]]` объекта `obj` как `proto`. @@ -156,12 +156,12 @@ alert(obj[key]); // "some value" let obj = Object.create(null); */!* -alert(obj); // Error (no toString) +alert(obj); // Ошибка (no toString) ``` ...Но обычно это нормально для ассоциативных массивов. -Обратите внимание, что большая часть методов, связанных с объектами, имеют вид `Object.something(...)`. К примеру, `Object.keys(obj)`. Подобные методы не находятся в прототипе, так что они продолжат работать для таких объектов: +Обратите внимание, что большинство методов, связанных с объектами, имеют вид `Object.something(...)`. К примеру, `Object.keys(obj)`. Подобные методы не находятся в прототипе, так что они продолжат работать для таких объектов: ```js run diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg index 86d09ae0de..cf4d3023f8 100644 --- a/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg @@ -1 +1 @@ -... get __proto__: function set __proto__: functionObject.prototypeObjectobj[[Prototype]]prototype \ No newline at end of file +... get __proto__: function set __proto__: functionObject.prototypeObjectobj[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg index 858f8317ee..9ccb342299 100644 --- a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg @@ -1 +1 @@ -obj[[Prototype]]null \ No newline at end of file +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/09-classes/01-class/article.md b/1-js/09-classes/01-class/article.md index db6ebb6de0..a41d43a601 100644 --- a/1-js/09-classes/01-class/article.md +++ b/1-js/09-classes/01-class/article.md @@ -109,7 +109,7 @@ alert(typeof User); // function alert(User === User.prototype.constructor); // true // Методы находятся в User.prototype, например: -alert(User.prototype.sayHi); // alert(this.name); +alert(User.prototype.sayHi); // sayHi() { alert(this.name); } // в прототипе ровно 2 метода alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi @@ -142,7 +142,7 @@ user.sayHi(); Однако есть важные отличия: -1. Во-первых, функция, созданная с помощью `class`, помечена специальным внутренним свойством `[[FunctionKind]]:"classConstructor"`. Поэтому это не совсем то же самое, что создавать её вручную. +1. Во-первых, функция, созданная с помощью `class`, помечена специальным внутренним свойством `[[IsClassConstructor]]: true`. Поэтому это не совсем то же самое, что создавать её вручную. В отличие от обычных функций, конструктор класса не может быть вызван без `new`: @@ -165,14 +165,14 @@ user.sayHi(); ``` 2. Методы класса являются неперечислимыми. - Определение класса устанавливает флаг `enumerable` в` false` для всех методов в `"prototype"`. + Определение класса устанавливает флаг `enumerable` в `false` для всех методов в `"prototype"`. И это хорошо, так как если мы проходимся циклом `for..in` по объекту, то обычно мы не хотим при этом получать методы класса. 3. Классы всегда используют `use strict`. Весь код внутри класса автоматически находится в строгом режиме. -Также в дополнение к основному, описанному выше, функционалу, синтаксис `class` даёт ряд других интересных возможностей, с которыми мы познакомимся чуть позже. +Также в дополнение к основной, описанной выше, функциональности, синтаксис `class` даёт ряд других интересных возможностей, с которыми мы познакомимся чуть позже. ## Class Expression diff --git a/1-js/09-classes/01-class/class-user.svg b/1-js/09-classes/01-class/class-user.svg index 5ac0146a6c..418d71d187 100644 --- a/1-js/09-classes/01-class/class-user.svg +++ b/1-js/09-classes/01-class/class-user.svg @@ -1 +1 @@ -sayHi: functionUserUser.prototypeprototypeconstructor: User \ No newline at end of file +sayHi: functionUserUser.prototypeprototypeconstructor: User \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js index ca613ca5e5..be2053cfcf 100644 --- a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js @@ -1,7 +1,7 @@ class ExtendedClock extends Clock { constructor(options) { super(options); - let { precision=1000 } = options; + let { precision = 1000 } = options; this.precision = precision; } diff --git a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg index 3412d9825c..203c59447a 100644 --- a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg +++ b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg @@ -1 +1 @@ -constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "White Rabbit"constructorconstructor \ No newline at end of file +constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "Белый кролик"constructorconstructorextends \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/article.md b/1-js/09-classes/02-class-inheritance/article.md index c6ba3f8386..e49e329e70 100644 --- a/1-js/09-classes/02-class-inheritance/article.md +++ b/1-js/09-classes/02-class-inheritance/article.md @@ -1,8 +1,13 @@ + # Наследование классов -Допустим, у нас есть два класса. +Наследование классов - это способ расширения одного класса другим классом. + +Таким образом, мы можем добавить новый функционал к уже существующему. + +## Ключевое слово "extends" -`Animal`: +Допустим, у нас есть класс `Animal`: ```js class Animal { @@ -11,62 +16,31 @@ class Animal { this.name = name; } run(speed) { - this.speed += speed; + this.speed = speed; alert(`${this.name} бежит со скоростью ${this.speed}.`); } stop() { this.speed = 0; - alert(`${this.name} стоит.`); + alert(`${this.name} стоит неподвижно.`); } } let animal = new Animal("Мой питомец"); ``` -![](rabbit-animal-independent-animal.svg) - -...И `Rabbit`: - -```js -class Rabbit { - constructor(name) { - this.name = name; - } - hide() { - alert(`${this.name} прячется!`); - } -} - -let rabbit = new Rabbit("Мой кролик"); -``` - -![](rabbit-animal-independent-rabbit.svg) +Вот как мы можем представить объект `animal` и класс `Animal` графически: -Сейчас они полностью независимы. +![](rabbit-animal-independent-animal.svg) -Но мы хотим, чтобы `Rabbit` расширял `Animal`. Другими словами, кролики должны происходить от животных, т.е. иметь доступ к методам `Animal` и расширять функциональность `Animal` своими методами. +...И мы хотели бы создать ещё один `class Rabbit`. -Для того, чтобы наследовать класс от другого, мы должны использовать ключевое слово `"extends"` и указать название родительского класса перед `{..}`. +Поскольку кролики - это животные, класс `Rabbit` должен быть основан на `Animal`, и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать "общие" животные. -Ниже `Rabbit` наследует от `Animal`: +Синтаксис для расширения другого класса следующий: `class Child extends Parent`. -```js run -class Animal { - constructor(name) { - this.speed = 0; - this.name = name; - } - run(speed) { - this.speed += speed; - alert(`${this.name} бежит со скоростью ${this.speed}.`); - } - stop() { - this.speed = 0; - alert(`${this.name} стоит.`); - } -} +Давайте создадим `class Rabbit`, который наследуется от `Animal`: -// Наследуем от Animal указывая "extends Animal" +```js *!* class Rabbit extends Animal { */!* @@ -80,26 +54,30 @@ let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.hide(); // Белый кролик прячется! ``` -Теперь код `Rabbit` стал короче, так как используется конструктор класса `Animal` по умолчанию и кролик может использовать метод `run` как и все животные. -На самом деле ключевое слово `extends` добавляет ссылку на `[[Prototype]]` из `Rabbit.prototype` в `Animal.prototype`: +Объект класса `Rabbit` имеет доступ как к методам `Rabbit`, таким как `rabbit.hide()`, так и к методам `Animal`, таким как `rabbit.run()`. + +Внутри ключевое слово `extends` работает по старой доброй механике прототипов. Оно устанавливает `Rabbit.prototype.[[Prototype]]` в `Animal.prototype`. Таким образом, если метода не оказалось в `Rabbit.prototype`, JavaScript берет его из `Animal.prototype`. ![](animal-rabbit-extends.svg) -Если метод не найден в `Rabbit.prototype`, JavaScript возьмёт его из `Animal.prototype`. +Например, чтобы найти метод `rabbit.run`, движок проверяет (снизу вверх на картинке): +1. Объект `rabbit` (не имеет `run`). +2. Его прототип, то есть `Rabbit.prototype` (имеет `hide`, но не имеет `run`). +3. Его прототип, то есть (вследствие `extends`) `Animal.prototype`, в котором, наконец, есть метод `run`. -Как мы помним из главы , в JavaScript используется наследование на прототипах для встроенных объектов. Например `Date.prototype.[[Prototype]]` это `Object.prototype`, поэтому у дат есть универсальные методы объекта. +Как мы помним из главы , сам JavaScript использует наследование на прототипах для встроенных объектов. Например, `Date.prototype.[[Prototype]]` является `Object.prototype`, поэтому у дат есть универсальные методы объекта. ````smart header="После `extends` разрешены любые выражения" -Синтаксис создания класса допускает указывать после `extends` не только класс, но любое выражение. +Синтаксис создания класса допускает указывать после `extends` не только класс, но и любое выражение. Пример вызова функции, которая генерирует родительский класс: ```js run function f(phrase) { return class { - sayHi() { alert(phrase) } - } + sayHi() { alert(phrase); } + }; } *!* @@ -115,23 +93,25 @@ new User().sayHi(); // Привет ## Переопределение методов -Давайте пойдём дальше и переопределим метод. Сейчас `Rabbit` наследует от `Animal` метод `stop`, который устанавливает `this.speed = 0`. +Теперь давайте продвинемся дальше и переопределим метод. По умолчанию все методы, не указанные в классе `Rabbit`, берутся непосредственно "как есть" из класса `Animal`. -Если мы определим свой метод `stop` в классе `Rabbit`, то он будет использоваться взамен родительского: +Но если мы укажем в `Rabbit` собственный метод, например `stop()`, то он будет использован вместо него: ```js class Rabbit extends Animal { stop() { - // ...будет использован для rabbit.stop() + // ...теперь это будет использоваться для rabbit.stop() + // вместо stop() из класса Animal } } ``` -...Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе. +Впрочем, обычно мы не хотим полностью заменить родительский метод, а скорее хотим сделать новый на его основе, изменяя или расширяя его функциональность. Мы делаем что-то в нашем методе и вызываем родительский метод до/после или в процессе. У классов есть ключевое слово `"super"` для таких случаев. + - `super.method(...)` вызывает родительский метод. -- `super(...)` вызывает родительский конструктор (работает только внутри нашего конструктора). +- `super(...)` для вызова родительского конструктора (работает только внутри нашего конструктора). Пусть наш кролик автоматически прячется при остановке: @@ -144,7 +124,7 @@ class Animal { } run(speed) { - this.speed += speed; + this.speed = speed; alert(`${this.name} бежит со скоростью ${this.speed}.`); } @@ -180,6 +160,7 @@ rabbit.stop(); // Белый кролик стоит. Белый кролик п Как упоминалось в главе , стрелочные функции не имеют `super`. При обращении к `super` стрелочной функции он берётся из внешней функции: + ```js class Rabbit extends Animal { stop() { @@ -249,11 +230,13 @@ let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not de Упс! При создании кролика - ошибка! Что не так? -Если коротко, то в классах-потомках конструктор обязан вызывать `super(...)`, и (!) делать это перед использованием `this`. +Если коротко, то: + +- **Конструкторы в наследуемых классах должны обязательно вызывать `super(...)`, и (!) делать это перед использованием `this`.** ...Но почему? Что происходит? Это требование кажется довольно странным. -Конечно, всему есть объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит. +Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит. В JavaScript существует различие между "функцией-конструктором наследующего класса" и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством `[[ConstructorKind]]:"derived"`. @@ -297,12 +280,106 @@ alert(rabbit.earLength); // 10 */!* ``` +### Переопределение полей класса: тонкое замечание + +```warn header="Продвинутое замечание" +В этом подразделе предполагается, что у вас уже есть определённый опыт работы с классами, возможно, в других языках программирования. + +Это даёт лучшее представление о языке, а также объясняет поведение, которое может быть источником ошибок (но не очень часто). + +Если вы считаете этот материал слишком трудным для понимания, просто продолжайте читать дальше, а затем вернитесь к нему через некоторое время. +``` + +Мы можем переопределять не только методы, но и поля класса. + +Однако, когда мы получаем доступ к переопределенному полю в родительском конструкторе, это поведение отличается от большинства других языков программирования. + +Рассмотрим этот пример: + +```js run +class Animal { + name = 'animal'; + + constructor() { + alert(this.name); // (*) + } +} + +class Rabbit extends Animal { + name = 'rabbit'; +} + +new Animal(); // animal +*!* +new Rabbit(); // animal +*/!* +``` + +Здесь, класс `Rabbit` расширяет `Animal` и переопределяет поле `name` своим собственным значением. + +В `Rabbit` нет собственного конструктора, поэтому вызывается конструктор `Animal`. + +Что интересно, в обоих случаях: `new Animal()` и `new Rabbit()`, `alert` в строке `(*)` показывает `animal`. + +**Другими словами, родительский конструктор всегда использует своё собственное значение поля, а не переопределённое.** + +Что же в этом странного? + +Если это ещё не ясно, сравните с методами. + +Вот тот же код, но вместо поля `this.name`, мы вызываем метод `this.showName()`: + +```js run +class Animal { + showName() { // вместо this.name = 'animal' + alert('animal'); + } + + constructor() { + this.showName(); // вместо alert(this.name); + } +} + +class Rabbit extends Animal { + showName() { + alert('rabbit'); + } +} + +new Animal(); // animal +*!* +new Rabbit(); // rabbit +*/!* +``` + +Обратите внимание: теперь результат другой. + +И это то, чего мы, естественно, ожидаем. Когда родительский конструктор вызывается в производном классе, он использует переопределённый метод. + +...Но для полей класса это не так. Как уже было сказано, родительский конструктор всегда использует родительское поле. + +Почему же наблюдается разница? + +Что ж, причина заключается в порядке инициализации полей. Поле класса инициализируется: +- Перед конструктором для базового класса (который ничего не расширяет), +- Сразу после `super()` для производного класса. + +В нашем случае `Rabbit` - это производный класс. В нем нет конструктора `constructor()`. Как было сказано ранее, это то же самое, как если бы был пустой конструктор, содержащий только `super(...args)`. + +Итак, `new Rabbit()` вызывает `super()`, таким образом, выполняя родительский конструктор, и (согласно правилу для производных классов) только после этого инициализируются поля его класса. На момент выполнения родительского конструктора ещё нет полей класса `Rabbit`, поэтому используются поля `Animal`. + +Это тонкое различие между полями и методами характерно для JavaScript. + +К счастью, такое поведение проявляется только в том случае, когда переопределенное поле используется в родительском конструкторе. Тогда может быть трудно понять, что происходит, поэтому мы объясняем это здесь. + +Если это становится проблемой, её можно решить, используя методы или геттеры/сеттеры вместо полей. + ## Устройство super, [[HomeObject]] ```warn header="Продвинутая информация" Если вы читаете учебник первый раз - эту секцию можно пропустить. -Она рассказывает о внутреннем устройстве наследования и вызов `super`. +Она рассказывает о внутреннем устройстве наследования и вызовe `super`. ``` Давайте заглянем "под капот" `super`. Здесь есть некоторые интересные моменты. @@ -318,6 +395,7 @@ alert(rabbit.earLength); // 10 Вы можете пропустить эту часть и перейти ниже к подсекции `[[HomeObject]]`, если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться. В примере ниже `rabbit.__proto__ = animal`. Попробуем в `rabbit.eat()` вызвать `animal.eat()`, используя `this.__proto__`: + ```js run let animal = { name: "Animal", @@ -365,7 +443,7 @@ let rabbit = { let longEar = { __proto__: rabbit, eat() { - // ...делаем что-то, связанное с длинными ушами и вызываем родительский (rabbit) метод + // ...делаем что-то, связанное с длинными ушами, и вызываем родительский (rabbit) метод this.__proto__.eat.call(this); // (**) } }; @@ -419,7 +497,6 @@ longEar.eat(); // Error: Maximum call stack size exceeded Давайте посмотрим, как это работает - опять же, используя простые объекты: - ```js run let animal = { name: "Животное", @@ -465,7 +542,7 @@ longEar.eat(); // Длинноух ест. ```js run let animal = { sayHi() { - console.log(`Я животное`); + alert("Я животное"); } }; @@ -479,7 +556,7 @@ let rabbit = { let plant = { sayHi() { - console.log("Я растение"); + alert("Я растение"); } }; @@ -505,8 +582,6 @@ tree.sayHi(); // Я животное (?!?) Вот диаграмма происходящего: -Here's the diagram of what happens: - ![](super-homeobject-wrong.svg) @@ -520,7 +595,7 @@ Here's the diagram of what happens: ```js run let animal = { - eat: function() { // намеренно не пишем так, а не eat() { ... + eat: function() { // намеренно пишем так, а не eat() { ... // ... } }; @@ -551,4 +626,4 @@ rabbit.eat(); // Ошибка вызова super (потому что нет [[ - Поэтому копировать метод, использующий `super`, между разными объектами небезопасно. Также: -- У функций-стрелок нет своего `this` и `super`, поэтому они "прозрачно" встраиваются во внешний контекст. +- У стрелочных функций нет своего `this` и `super`, поэтому они "прозрачно" встраиваются во внешний контекст. diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg index 546aa334f0..5ea9bf29ea 100644 --- a/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg @@ -1 +1 @@ -slice: function ...Array.prototypearrhasOwnProperty: function ...Object.prototype[1, 2, 3][[Prototype]][[Prototype]] \ No newline at end of file +slice: function ...Array.prototypearrhasOwnProperty: function ...Object.prototype[1, 2, 3][[Prototype]][[Prototype]] \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg index 3bdda5a02a..2172088e64 100644 --- a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg @@ -1 +1 @@ -jump: functionRabbit.prototyperabbiteat: functionAnimal.prototypename: "White Rabbit"[[Prototype]][[Prototype]]Rabbit.prototype.__proto__ = Animal.prototype sets thistoString: function hasOwnProperty: function ...Object.prototype[[Prototype]][[Prototype]]null \ No newline at end of file +jump: functionRabbit.prototyperabbiteat: functionAnimal.prototypename: "Белый кролик"[[Prototype]][[Prototype]]Rabbit.prototype.__proto__ = Animal.prototype sets thistoString: function hasOwnProperty: function ...Object.prototype[[Prototype]][[Prototype]]null \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg index 706ca0974b..1b21dd81b5 100644 --- a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg @@ -1 +1 @@ -методы RabbitRabbit.prototyperabbitметоды AnimalAnimal.prototype[[Prototype]][[Prototype]]свойства rabbit \ No newline at end of file +методы RabbitRabbit.prototyperabbitметоды AnimalAnimal.prototype[[Prototype]][[Prototype]]свойства rabbit \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg index bf86db77d9..62b876d3ac 100644 --- a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg @@ -1 +1 @@ - constructor: Animal run: function stop: functionAnimal.prototypeAnimalnew Animal[[Prototype]]prototypename: "My animal" \ No newline at end of file + constructor: Animal run: function stop: functionAnimal.prototypeAnimalnew Animal[[Prototype]]prototypename: "Мой питомец" \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg index 8a5e25037a..ff669d3d84 100644 --- a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg @@ -1 +1 @@ - constructor: Rabbit hide: functionRabbit.prototypeRabbitnew Rabbit[[Prototype]]prototypename: "My rabbit" \ No newline at end of file + constructor: Rabbit hide: functionRabbit.prototypeRabbitnew Rabbit[[Prototype]]prototypename: "Мой кролик" \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg index c9c8fea9e3..f6450ddc49 100644 --- a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg +++ b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg @@ -1 +1 @@ -sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file +sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/this-super-loop.svg b/1-js/09-classes/02-class-inheritance/this-super-loop.svg index 342574da90..4f5f45034a 100644 --- a/1-js/09-classes/02-class-inheritance/this-super-loop.svg +++ b/1-js/09-classes/02-class-inheritance/this-super-loop.svg @@ -1 +1 @@ -rabbitlongEarrabbitlongEar \ No newline at end of file +rabbitlongEarrabbitlongEar \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/rabbit-extends-object.svg similarity index 95% rename from 1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg rename to 1-js/09-classes/03-static-properties-methods/1-class-extend-object/rabbit-extends-object.svg index 0a1f4382c2..34d783b4de 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg +++ b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/rabbit-extends-object.svg @@ -1 +1 @@ -call: function bind: function ...Function.prototypeconstructorObjectRabbit[[Prototype]][[Prototype]]constructorcall: function bind: function ...Function.prototypeRabbit[[Prototype]]constructorclass Rabbitclass Rabbit extends Object \ No newline at end of file +call: function bind: function ...Function.prototypeconstructorObjectRabbit[[Prototype]][[Prototype]]constructorcall: function bind: function ...Function.prototypeRabbit[[Prototype]]constructorclass Rabbitclass Rabbit extends Object \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/solution.md similarity index 80% rename from 1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md rename to 1-js/09-classes/03-static-properties-methods/1-class-extend-object/solution.md index e60afe16b7..3c1a42f600 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md +++ b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/solution.md @@ -28,7 +28,7 @@ alert( rabbit.hasOwnProperty('name') ); // true 1. Между `"prototype"` функций-конструкторов (для методов) 2. Между самими функциями-конструкторами (для статических методов). -В нашем случае, для `class Rabbit extends Object` это значит: +В случае с `class Rabbit extends Object` это значит: ```js run class Rabbit extends Object {} @@ -44,11 +44,11 @@ class Rabbit extends Object {} *!* // обычно мы вызываем Object.getOwnPropertyNames -alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b +alert( Rabbit.getOwnPropertyNames({a: 1, b: 2}) ); // a,b */!* ``` -Но если явно не наследуем от объекта, то для `Rabbit.__ proto__` не установлено значение `Object`. +Но если явно не наследуем от объекта, то для `Rabbit.__proto__` не установлено значение `Object`. Пример: @@ -61,13 +61,13 @@ alert( Rabbit.__proto__ === Function.prototype ); // как у каждой фу *!* // ошибка - нет такой функции у Rabbit -alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error +alert( Rabbit.getOwnPropertyNames({a: 1, b: 2}) ); // Ошибка */!* ``` Таким образом, в этом случае у `Rabbit` нет доступа к статическим методам `Object`. -Кстати, у `Function.prototype` есть "общие" методы, такие как `call`, `bind` и т. д. Они в конечном итоге доступны в обоих случаях, потому что для встроенного конструктора `Object` `Object.__proto__ === Function.prototype`. +Кстати, у `Function.prototype` также есть "общие" методы, такие как `call`, `bind` и т. д. Они в конечном итоге доступны в обоих случаях, потому что для встроенного конструктора `Object` `Object.__proto__ === Function.prototype`. Пример на картинке: diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/task.md similarity index 93% rename from 1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md rename to 1-js/09-classes/03-static-properties-methods/1-class-extend-object/task.md index 620f9f4792..55586e055a 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md +++ b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/task.md @@ -1,4 +1,4 @@ -importance: 5 +importance: 3 --- @@ -38,5 +38,5 @@ class Rabbit extends Object { let rabbit = new Rabbit("Кроль"); -alert( rabbit.hasOwnProperty('name') ); // true +alert( rabbit.hasOwnProperty('name') ); // Ошибка ``` diff --git a/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg index fab401df66..ee1fc54748 100644 --- a/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg +++ b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg @@ -1 +1 @@ -constructor: Animal run: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitrabbit[[Prototype]][[Prototype]][[Prototype]]prototypeprototypecomparename: "White Rabbit" \ No newline at end of file +constructor: Animal run: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitrabbit[[Prototype]][[Prototype]][[Prototype]]prototypeprototypecomparename: "Белый кролик" \ No newline at end of file diff --git a/1-js/09-classes/03-static-properties-methods/article.md b/1-js/09-classes/03-static-properties-methods/article.md index 01c58b6788..68b7ce6395 100644 --- a/1-js/09-classes/03-static-properties-methods/article.md +++ b/1-js/09-classes/03-static-properties-methods/article.md @@ -1,9 +1,9 @@ # Статические свойства и методы -Мы также можем присвоить метод самой функции-классу, а не её `"prototype"`. Такие методы называются *статическими*. +Мы также можем присвоить метод самому классу. Такие методы называются *статическими*. -В классе такие методы обозначаются ключевым словом `static`, например: +В объявление класса они добавляются с помощью ключевого слова `static`, например: ```js run class User { @@ -20,7 +20,7 @@ User.staticMethod(); // true Это фактически то же самое, что присвоить метод напрямую как свойство функции: ```js -class User() { } +class User { } User.staticMethod = function() { alert(this === User); @@ -29,9 +29,13 @@ User.staticMethod = function() { Значением `this` при вызове `User.staticMethod()` является сам конструктор класса `User` (правило "объект до точки"). -Обычно статические методы используются для реализации функций, принадлежащих классу, но не к каким-то конкретным его объектам. +Обычно статические методы используются для реализации функций, которые будут принадлежать классу в целом, но не какому-либо его конкретному объекту. -Например, есть объекты статей `Article`, и нужна функция для их сравнения. Естественное решение - сделать для этого метод `Article.compare`: +Звучит не очень понятно? Сейчас все встанет на свои места. + +Например, есть объекты статей `Article`, и нужна функция для их сравнения. + +Естественное решение – сделать для этого статический метод `Article.compare`: ```js run class Article { @@ -61,9 +65,11 @@ articles.sort(Article.compare); alert( articles[0].title ); // CSS ``` -Здесь метод `Article.compare` стоит "над" статьями, как способ их сравнения. Это метод не отдельной статьи, а всего класса. +Здесь метод `Article.compare` стоит "над" статьями, как средство для их сравнения. Это метод не отдельной статьи, а всего класса. + +Другим примером может быть так называемый "фабричный" метод. -Другим примером может быть так называемый "фабричный" метод. Представим, что нам нужно создавать статьи различными способами: +Скажем, нам нужно несколько способов создания статьи: 1. Создание через заданные параметры (`title`, `date` и т. д.). 2. Создание пустой статьи с сегодняшней датой. @@ -99,13 +105,22 @@ alert( article.title ); // Сегодняшний дайджест ```js // предположим, что Article - это специальный класс для управления статьями -// статический метод для удаления статьи: +// статический метод для удаления статьи по id: Article.remove({id: 12345}); ``` -## Статические свойства +````warn header="Статические методы недоступны для отдельных объектов" +Статические методы могут вызываться для классов, но не для отдельных объектов. + +Например. такой код не будет работать: -[recent browser=Chrome] +```js +// ... +article.createTodays(); /// Error: article.createTodays is not a function +``` +```` + +## Статические свойства Статические свойства также возможны, они выглядят как свойства класса, но с `static` в начале: @@ -123,7 +138,7 @@ alert( Article.publisher ); // Илья Кантор Article.publisher = "Илья Кантор"; ``` -## Наследование статических свойств и методов +## Наследование статических свойств и методов [#statics-and-inheritance] Статические свойства и методы наследуются. @@ -177,7 +192,7 @@ rabbits[0].run(); // Чёрный кролик бежит со скорость Так что `Rabbit extends Animal` создаёт две ссылки на прототип: -1. Функция `Rabbit` прототипно наследует от `Animal` function. +1. Функция `Rabbit` прототипно наследует от функции `Animal`. 2. `Rabbit.prototype` прототипно наследует от `Animal.prototype`. В результате наследование работает как для обычных, так и для статических методов. @@ -192,7 +207,7 @@ class Rabbit extends Animal {} alert(Rabbit.__proto__ === Animal); // true // для обычных методов -alert(Rabbit.prototype.__proto__ === Animal.prototype); +alert(Rabbit.prototype.__proto__ === Animal.prototype); // true ``` ## Итого diff --git a/1-js/09-classes/04-private-protected-properties-methods/article.md b/1-js/09-classes/04-private-protected-properties-methods/article.md index 55f03bfca0..9add309bc9 100644 --- a/1-js/09-classes/04-private-protected-properties-methods/article.md +++ b/1-js/09-classes/04-private-protected-properties-methods/article.md @@ -170,7 +170,7 @@ class CoffeeMachine { new CoffeeMachine().setWaterAmount(100); ``` -Это выглядит немного длиннее, но функции более гибкие. Они могут принимать несколько аргументов (даже если они нам сейчас не нужны). Итак, на будущее, если нам надо что-то отрефакторить, функции более безопасный выбор. +Это выглядит немного длиннее, но функции более гибкие. Они могут принимать несколько аргументов (даже если они нам сейчас не нужны). Итак, на будущее, если нам надо что-то отрефакторить, функции -- более безопасный выбор. С другой стороны, синтаксис get/set короче, решать вам. ```` @@ -178,7 +178,7 @@ new CoffeeMachine().setWaterAmount(100); ```smart header="Защищённые поля наследуются" Если мы унаследуем `class MegaMachine extends CoffeeMachine`, ничто не помешает нам обращаться к `this._waterAmount` или `this._power` из методов нового класса. -Таким образом защищённые методы, конечно же, наследуются. В отличие от приватных полей, в чём мы убедимся ниже. +Таким образом, защищённые поля, конечно же, наследуются. В отличие от приватных полей, в чём мы убедимся ниже. ``` ## Приватное свойство "#waterLimit" @@ -208,7 +208,7 @@ class CoffeeMachine { let coffeeMachine = new CoffeeMachine(); *!* -// снаружи нет доступа к приватным методам класса +// снаружи нет доступа к приватным методам класса coffeeMachine.#checkWater(); // Error coffeeMachine.#waterLimit = 1000; // Error */!* @@ -241,7 +241,7 @@ machine.waterAmount = 100; alert(machine.#waterAmount); // Error ``` -В отличие от защищённых, функционал приватных полей обеспечивается самим языком. Это хорошо. +В отличие от защищённых, функциональность приватных полей обеспечивается самим языком. Это хорошо. Но если мы унаследуем от `CoffeeMachine`, то мы не получим прямого доступа к `#waterAmount`. Мы будем вынуждены полагаться на геттер/сеттер `waterAmount`: @@ -260,7 +260,7 @@ class MegaCoffeeMachine extends CoffeeMachine { ````warn Приватные поля особенные. -Как мы помним, обычно мы можем получить доступ к полям объекта с помощью this[name]: +Как мы помним, обычно мы можем получить доступ к полям объекта с помощью `this[name]`: ```js class User { diff --git a/1-js/09-classes/05-extend-natives/article.md b/1-js/09-classes/05-extend-natives/article.md index 41ba26ee95..a8840522f4 100644 --- a/1-js/09-classes/05-extend-natives/article.md +++ b/1-js/09-classes/05-extend-natives/article.md @@ -74,7 +74,7 @@ alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function Как мы уже знаем, встроенные классы расширяют друг друга. -Обычно, когда один класс наследует другому, то наследуются и статические методы. Это было подробно разъяснено в главе [](info:static-properties-methods#statics-and-inheritance). +Обычно, когда один класс наследует другой, то наследуются и статические методы. Это было подробно разъяснено в главе [](info:static-properties-methods#statics-and-inheritance). Но встроенные классы - исключение. Они не наследуют статические методы друг друга. diff --git a/1-js/09-classes/05-extend-natives/object-date-inheritance.svg b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg index a0165bccbc..be47d7fd96 100644 --- a/1-js/09-classes/05-extend-natives/object-date-inheritance.svg +++ b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg @@ -1 +1 @@ -constructor: Object toString: function hasOwnProperty: function ...Object.prototypeconstructor: Date toString: function getDate: function ...Date.prototypeObjectDatenew Date()[[Prototype]][[Prototype]]prototypeprototypedefineProperty keys ...now parse ...1 Jan 2019 \ No newline at end of file +constructor: Object toString: function hasOwnProperty: function ...Object.prototypeconstructor: Date toString: function getDate: function ...Date.prototypeObjectDatenew Date()[[Prototype]][[Prototype]]prototypeprototypedefineProperty keys ...now parse ...1 Jan 2019 \ No newline at end of file diff --git a/1-js/09-classes/06-instanceof/article.md b/1-js/09-classes/06-instanceof/article.md index ca4654da6c..a9dc6317a4 100644 --- a/1-js/09-classes/06-instanceof/article.md +++ b/1-js/09-classes/06-instanceof/article.md @@ -1,6 +1,6 @@ # Проверка класса: "instanceof" -Оператор `instanceof` позволяет проверить, к какому классу принадлежит объект, с учётом наследования. +Оператор `instanceof` позволяет проверить, принадлежит ли объект указанному классу, с учётом наследования. Такая проверка может потребоваться во многих случаях. Здесь мы используем её для создания *полиморфной* функции, которая интерпретирует аргументы по-разному в зависимости от их типа. @@ -92,7 +92,7 @@ alert( arr instanceof Object ); // true alert(rabbit instanceof Animal); // true */!* - // rabbit.__proto__ === Rabbit.prototype + // rabbit.__proto__ === Animal.prototype (нет совпадения) *!* // rabbit.__proto__.__proto__ === Animal.prototype (совпадение!) */!* @@ -102,7 +102,7 @@ alert( arr instanceof Object ); // true ![](instanceof.svg) -Кстати, есть метод [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), которые возвращает `true`, если объект `objA` есть где-то в прототипной цепочке объекта `objB`. Так что `obj instanceof Class` можно перефразировать как `Class.prototype.isPrototypeOf(obj)`. +Кстати, есть метод [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), который возвращает `true`, если объект `objA` есть где-то в прототипной цепочке объекта `objB`. Так что `obj instanceof Class` можно перефразировать как `Class.prototype.isPrototypeOf(obj)`. Забавно, но сам конструктор `Class` не участвует в процессе проверки! Важна только цепочка прототипов `Class.prototype`. @@ -125,7 +125,7 @@ alert( rabbit instanceof Rabbit ); // false ## Бонус: Object.prototype.toString возвращает тип -Мы уже знаем, что обычные объекты преобразуется к строке как `[object Object]`: +Мы уже знаем, что обычные объекты преобразуются к строке как `[object Object]`: ```js run let obj = {}; diff --git a/1-js/09-classes/06-instanceof/instanceof.svg b/1-js/09-classes/06-instanceof/instanceof.svg index 920be68b72..d63b03a8a2 100644 --- a/1-js/09-classes/06-instanceof/instanceof.svg +++ b/1-js/09-classes/06-instanceof/instanceof.svg @@ -1 +1 @@ -Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file +Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file diff --git a/1-js/09-classes/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md index cffb75add0..e92a1aae8a 100644 --- a/1-js/09-classes/07-mixins/article.md +++ b/1-js/09-classes/07-mixins/article.md @@ -4,13 +4,13 @@ Иногда это может ограничивать нас. Например, у нас есть класс `StreetSweeper` и класс `Bicycle`, а мы хотим создать их смесь: `StreetSweepingBicycle`. -Или у нас есть класс `User`, который реализует пользователей, и класс `EventEmitter`, реализующий события. Мы хотели бы добавить функционал класса `EventEmitter` к `User`, чтобы пользователи могли легко генерировать события. +Или у нас есть класс `User`, который реализует пользователей, и класс `EventEmitter`, реализующий события. Мы хотели бы добавить функциональность класса `EventEmitter` к `User`, чтобы пользователи могли легко генерировать события. Для таких случаев существуют "примеси". По определению из Википедии, [примесь](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%81%D1%8C_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)) - это класс, методы которого предназначены для использования в других классах, причём без наследования от примеси. -Другими словами, *примесь* определяет методы, которые реализуют определённое поведение. Мы не используем примесь саму по себе, а используем её, чтобы добавить функционал другим классам. +Другими словами, *примесь* определяет методы, которые реализуют определённое поведение. Мы не используем примесь саму по себе, а используем её, чтобы добавить функциональность другим классам. ## Пример примеси @@ -57,7 +57,7 @@ class User extends Person { Object.assign(User.prototype, sayHiMixin); ``` -Примеси могут наследовать друг другу. +Примеси могут наследовать друг друга. В примере ниже `sayHiMixin` наследует от `sayMixin`: @@ -69,7 +69,7 @@ let sayMixin = { }; let sayHiMixin = { - __proto__: sayMixin, // (или мы можем использовать Object.create для задания прототипа) + __proto__: sayMixin, // (или мы можем использовать Object.setPrototypeOf для задания прототипа) sayHi() { *!* @@ -97,19 +97,19 @@ new User("Вася").sayHi(); // Привет, Вася! Обратим внимание, что при вызове родительского метода `super.say()` из `sayHiMixin` (строки, помеченные `(*)`) этот метод ищется в прототипе самой примеси, а не класса. -Вот диаграмма (см правую часть): +Вот диаграмма (см. правую часть): ![](mixin-inheritance.svg) Это связано с тем, что методы `sayHi` и `sayBye` были изначально созданы в объекте `sayHiMixin`. Несмотря на то, что они скопированы, их внутреннее свойство `[[HomeObject]]` ссылается на `sayHiMixin`, как показано на картинке выше. -Так как `super` ищет родительские методы в `[[HomeObject]].[[Prototype]]`, это означает `sayHiMixin.[[Prototype]]`, а не `User.[[Prototype]]`. +Так как `super` ищет родительские методы в `[[HomeObject]].[[Prototype]]`, это означает, что он ищет `sayHiMixin.[[Prototype]]`. ## EventMixin -Многие объекты в браузерной разработке (и не только) обладают важной способностью - они могут генерировать события. События - отличный способ передачи информации всем, кто в ней заинтересован. Давайте создадим примесь, которая позволит легко добавлять функционал по работе с событиями любым классам/объектам. +Многие объекты в браузерной разработке (и не только) обладают важной способностью - они могут генерировать события. События - отличный способ передачи информации всем, кто в ней заинтересован. Давайте создадим примесь, которая позволит легко добавлять функциональность по работе с событиями любым классам/объектам. -- Примесь добавит метод `.trigger(name, [data])` для генерации события. Аргумент `name` - это имя события, за которым могут следовать другие аргументы с данными для события. +- Примесь добавит метод `.trigger(name, [...data])` для генерации события. Аргумент `name` - это имя события, за которым могут следовать дополнительные аргументы с данными для события. - Также будет добавлен метод `.on(name, handler)`, который назначает обработчик для события с заданным именем. Обработчик будет вызван, когда произойдёт событие с указанным именем `name`, и получит данные из `.trigger`. - ...и метод `.off(name, handler)`, который удаляет обработчик указанного события. @@ -138,7 +138,7 @@ let eventMixin = { * menu.off('select', handler) */ off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; + let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { @@ -152,7 +152,7 @@ let eventMixin = { * this.trigger('select', data1, data2); */ trigger(eventName, ...args) { - if (!this._eventHandlers || !this._eventHandlers[eventName]) { + if (!this._eventHandlers?.[eventName]) { return; // обработчиков для этого события нет } @@ -164,11 +164,13 @@ let eventMixin = { Итак, у нас есть 3 метода: -1. `.on(eventName, handler)` -- назначает функцию `handler`, чтобы обработать событие с заданным именем. Обработчики хранятся в свойстве `_eventHandlers`, представляющим собой объект, в котором имя события является ключом, а массив обработчиков - значением. +1. `.on(eventName, handler)` -- назначает функцию `handler`, чтобы обработать событие с заданным именем. -2. `.off(eventName, handler)` -- убирает функцию из списка обработчиков. + Технически существует свойство `_eventHandlers`, в котором хранится массив обработчиков для каждого имени события, и оно просто добавляет это событие в список. -3. `.trigger(eventName, ...args)` -- генерирует событие: все назначенные обработчики из `_eventHandlers[eventName]` вызываются, и `...args` передаются им в качестве аргументов. +3. `.off(eventName, handler)` -- убирает функцию из списка обработчиков. + +4. `.trigger(eventName, ...args)` -- генерирует событие: все назначенные обработчики из `_eventHandlers[eventName]` вызываются, и `...args` передаются им в качестве аргументов. Использование: @@ -184,7 +186,7 @@ Object.assign(Menu.prototype, eventMixin); let menu = new Menu(); -// Добавить обработчик, который будет вызван при событии "select": +// Добавим обработчик, который будет вызван при событии "select": *!* menu.on("select", value => alert(`Выбранное значение: ${value}`)); */!* @@ -203,6 +205,6 @@ menu.choose("123"); // Выбранное значение: 123 Некоторые другие языки допускают множественное наследование. JavaScript не поддерживает множественное наследование, но с помощью примесей мы можем реализовать нечто похожее, скопировав методы в прототип. -Мы можем использовать примеси для расширения функционала классов, например, для обработки событий, как мы сделали это выше. +Мы можем использовать примеси для расширения функциональности классов, например, для обработки событий, как мы сделали это выше. С примесями могут возникнуть конфликты, если они перезаписывают существующие методы класса. Стоит помнить об этом и быть внимательнее при выборе имён для методов примеси, чтобы их избежать. diff --git a/1-js/09-classes/07-mixins/mixin-inheritance.svg b/1-js/09-classes/07-mixins/mixin-inheritance.svg index 2e7ba8b394..1fdc223936 100644 --- a/1-js/09-classes/07-mixins/mixin-inheritance.svg +++ b/1-js/09-classes/07-mixins/mixin-inheritance.svg @@ -1 +1 @@ -sayHi: function sayBye: functionsayHiMixinsay: functionsayMixin[[Prototype]]constructor: User sayHi: function sayBye: functionUser.prototype[[Prototype]]name: ...user[[HomeObject] \ No newline at end of file +sayHi: function sayBye: functionsayHiMixinsay: functionsayMixin[[Prototype]]constructor: User sayHi: function sayBye: functionUser.prototype[[Prototype]]name: ...user[[HomeObject] \ No newline at end of file diff --git a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md index f1ef88dea9..427d829f42 100644 --- a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md +++ b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md @@ -35,6 +35,6 @@ importance: 5 */!* ``` -Нам определённо нужна очиска после работы, неважно возникли ошибки или нет. +Нам определённо нужна очистка после работы, неважно возникли ошибки или нет. Есть ли здесь преимущество в использовании `finally` или оба фрагмента кода одинаковы? Если такое преимущество есть, то дайте пример, когда оно проявляется. diff --git a/1-js/10-error-handling/1-try-catch/article.md b/1-js/10-error-handling/1-try-catch/article.md index 8ad1363959..5a9e089544 100644 --- a/1-js/10-error-handling/1-try-catch/article.md +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -81,7 +81,7 @@ try { try { {{{{{{{{{{{{ } catch(e) { - alert("Движок не может понять этот код, он не корректен"); + alert("Движок не может понять этот код, он некорректен"); } ``` @@ -283,22 +283,22 @@ let error = new ReferenceError(message); Например: ```js run -let error = new Error(" Ого, ошибка! o_O"); +let error = new Error("Ого, ошибка! o_O"); alert(error.name); // Error -alert(error.message); // Ого, ошибка! o_O +alert(error.message); // Ого, ошибка! o_O ``` Давайте посмотрим, какую ошибку генерирует `JSON.parse`: ```js run try { - JSON.parse("{ bad json o_O }"); + JSON.parse("{ некорректный json o_O }"); } catch(e) { *!* alert(e.name); // SyntaxError */!* - alert(e.message); // Unexpected token o in JSON at position 2 + alert(e.message); // Expected property name or '}' in JSON at position 2 (line 1 column 3) } ``` @@ -626,9 +626,9 @@ window.onerror = function(message, url, line, col, error) { ``` -Роль глобального обработчика `window.onerror` обычно заключается не в восстановлении выполнения скрипта -- это скорее всего невозможно в случае программной ошибки, а в отправке сообщение об ошибке разработчикам. +Роль глобального обработчика `window.onerror` обычно заключается не в восстановлении выполнения скрипта -- это скорее всего невозможно в случае программной ошибки, а в отправке сообщения об ошибке разработчикам. -Существуют также веб-сервисы, которые предоставляют логирование ошибок для таких случаев, такие как или . +Существуют также веб-сервисы, которые предоставляют логирование ошибок для таких случаев, такие как . Они работают так: diff --git a/1-js/10-error-handling/1-try-catch/try-catch-flow.svg b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg index 8384c6ca27..8e6ca4d79c 100644 --- a/1-js/10-error-handling/1-try-catch/try-catch-flow.svg +++ b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg @@ -1 +1 @@ -НачалоНет ошибокВ коде произошла ошибкаИгнорировать блок catchИгнорировать остаток tryВыполнить блок catchtry { }// код... \ No newline at end of file +НачалоНет ошибокВ коде произошла ошибкаИгнорировать блок catchИгнорировать остаток tryВыполнить блок catchtry { }// код... \ No newline at end of file diff --git a/1-js/10-error-handling/2-custom-errors/article.md b/1-js/10-error-handling/2-custom-errors/article.md index 2cbc1b2d29..fbc3b9dc35 100644 --- a/1-js/10-error-handling/2-custom-errors/article.md +++ b/1-js/10-error-handling/2-custom-errors/article.md @@ -40,7 +40,7 @@ class Error { Теперь давайте унаследуем от него `ValidationError` и попробуем новый класс в действии: -```js run untrusted +```js run *!* class ValidationError extends Error { */!* @@ -119,7 +119,7 @@ try { // вместо (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ... -``` +``` Версия с `instanceof` гораздо лучше, потому что в будущем мы собираемся расширить `ValidationError`, сделав его подтипы, такие как `PropertyRequiredError`. И проверка `instanceof` продолжит работать для новых наследованных классов. Так что это на будущее. diff --git a/1-js/11-async/01-callbacks/01-animate-circle-callback/task.md b/1-js/11-async/01-callbacks/01-animate-circle-callback/task.md deleted file mode 100644 index c50d632368..0000000000 --- a/1-js/11-async/01-callbacks/01-animate-circle-callback/task.md +++ /dev/null @@ -1,25 +0,0 @@ - -# Анимация круга с помощью колбэка - -В задаче находится код для анимации появления круга. - -Давайте представим, что теперь нам нужен не просто круг, а круг с сообщением внутри. И сообщение должно появляться *после* анимации (когда круг достигнет своих размеров), иначе это будет некрасиво. - -В том решении функция `showCircle(cx, cy, radius)` рисовала круг, но способа узнать, что всё нарисовано, не было. - -Поэтому добавим в параметры колбэк: `showCircle(cx, cy, radius, callback)`, который выполним, когда анимация будет завершена. Функция `callback` будет добавлять в наш круг `

` элемент с сообщением. - -Посмотрите пример: - -```js -showCircle(150, 150, 100, div => { - div.classList.add('message-ball'); - div.append("Hello, world!"); -}); -``` - -Демо: - -[iframe src="solution" height=260] - -Возьмите за основу решение задачи . diff --git a/1-js/11-async/01-callbacks/article.md b/1-js/11-async/01-callbacks/article.md index 5f293d24d1..87f7e85465 100644 --- a/1-js/11-async/01-callbacks/article.md +++ b/1-js/11-async/01-callbacks/article.md @@ -1,7 +1,14 @@ - # Введение: колбэки +```warn header="В примерах мы будем использовать браузерные методы" +Для демонстрации использования колбэков, промисов и других абстрактных понятий мы будем использовать некоторые браузерные методы: в частности, загрузку скриптов и выполнение простых манипуляций с документом. + +Если вы не знакомы с этими методами, и их использование в примерах вызывает у вас недоумение, возможно, вам стоит прочитать несколько глав из [следующей части](/document) учебника. + +Тем не менее, мы все равно попытаемся максимально доходчиво всё разъяснить. Ничего особо сложного в плане браузера не будет. +``` + Многие действия в JavaScript *асинхронные*. Например, рассмотрим функцию `loadScript(src)`: @@ -54,26 +61,26 @@ newFunction(); // такой функции не существует! function loadScript(src, *!*callback*/!*) { let script = document.createElement('script'); script.src = src; - *!* script.onload = () => callback(script); */!* - document.head.append(script); } ``` +Событие `onload` описано в статье , оно в основном выполняет функцию после загрузки и выполнения скрипта. + Теперь, если мы хотим вызвать функцию из скрипта, нужно делать это в колбэке: ```js loadScript('/my/script.js', function() { - // эта функция вызовется после того, когда загрузится скрипт + // эта функция вызовется после того, как загрузится скрипт newFunction(); // теперь всё работает ... }); ``` -Смысл такой: вторым аргументом передаётся функция (обычно анонимная), которая выполняется по завершению действия. +Смысл такой: вторым аргументом передаётся функция (обычно анонимная), которая выполняется по завершении действия. Возьмём для примера реальный скрипт с библиотекой функций: diff --git a/1-js/11-async/01-callbacks/callback-hell.svg b/1-js/11-async/01-callbacks/callback-hell.svg index 907f62c2a6..bd53e44322 100644 --- a/1-js/11-async/01-callbacks/callback-hell.svg +++ b/1-js/11-async/01-callbacks/callback-hell.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/03-animate-circle-promise/solution.view/index.html b/1-js/11-async/02-promise-basics/03-animate-circle-promise/solution.view/index.html index f0d39c34c6..ae1d06567e 100644 --- a/1-js/11-async/02-promise-basics/03-animate-circle-promise/solution.view/index.html +++ b/1-js/11-async/02-promise-basics/03-animate-circle-promise/solution.view/index.html @@ -10,7 +10,7 @@ text-align: center; } .circle { - transition-property: width, height, margin-left, margin-top; + transition-property: width, height; transition-duration: 2s; position: fixed; transform: translateX(-50%) translateY(-50%); diff --git a/1-js/11-async/02-promise-basics/article.md b/1-js/11-async/02-promise-basics/article.md index 4b333aad62..a22e4ed6d0 100644 --- a/1-js/11-async/02-promise-basics/article.md +++ b/1-js/11-async/02-promise-basics/article.md @@ -1,10 +1,11 @@ + # Промисы Представьте, что вы известный певец, которого фанаты постоянно донимают расспросами о предстоящем сингле. Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в который они могут записаться. Они могут оставить там свой e-mail, чтобы получить песню, как только она выйдет. И даже больше: если что-то пойдёт не так, например, в студии будет пожар и песню выпустить не выйдет, они также получат уведомление об этом. -Все счастливы! Вы счастливы, потому что вас больше не донимают фанаты, а фанаты могут больше не беспокоиться, что пропустят новый сингл. +Все счастливы! Вы счастливы, потому что вас больше не донимают фанаты, а фанаты больше не беспокоятся, что пропустят новый сингл. Это аналогия из реальной жизни для ситуаций, с которыми мы часто сталкиваемся в программировании: @@ -47,7 +48,7 @@ let promise = new Promise(function(resolve, reject) { Ниже пример конструктора `Promise` и простого исполнителя с кодом, дающим результат с задержкой (через `setTimeout`): -```js run +```js let promise = new Promise(function(resolve, reject) { // эта функция выполнится автоматически, при вызове new Promise @@ -126,9 +127,9 @@ let promise = new Promise(function(resolve, reject) { Свойства `state` и `result` - это внутренние свойства объекта `Promise` и мы не имеем к ним прямого доступа. Для обработки результата следует использовать методы `.then`/`.catch`/`.finally`, про них речь пойдёт дальше. ``` -## Потребители: then, catch, finally +## Потребители: then, catch -Объект `Promise` служит связующим звеном между исполнителем ("создающим" кодом или "певцом") и функциями-потребителями ("фанатами"), которые получат либо результат, либо ошибку. Функции-потребители могут быть зарегистрированы (подписаны) с помощью методов `.then`, `.catch` и `.finally`. +Объект `Promise` служит связующим звеном между исполнителем ("создающим" кодом или "певцом") и функциями-потребителями ("фанатами"), которые получат либо результат, либо ошибку. Функции-потребители могут быть зарегистрированы (подписаны) с помощью методов `.then` и `.catch`. ### then @@ -195,7 +196,7 @@ promise.then(alert); // выведет "done!" спустя одну секун ### catch -Если мы хотели бы только обработать ошибку, то можно использовать `null` в качестве первого аргумента: `.then(null, errorHandlingFunction)`. Или можно воспользоваться методом `.catch(errorHandlingFunction)`, который сделает тоже самое: +Если мы хотели бы только обработать ошибку, то можно использовать `null` в качестве первого аргумента: `.then(null, errorHandlingFunction)`. Или можно воспользоваться методом `.catch(errorHandlingFunction)`, который сделает то же самое: ```js run let promise = new Promise((resolve, reject) => { @@ -203,66 +204,91 @@ let promise = new Promise((resolve, reject) => { }); *!* -// .catch(f) это тоже самое, что promise.then(null, f) +// .catch(f) это то же самое, что promise.then(null, f) promise.catch(alert); // выведет "Error: Ошибка!" спустя одну секунду */!* ``` Вызов `.catch(f)` - это сокращённый, "укороченный" вариант `.then(null, f)`. -### finally +## Очистка: finally По аналогии с блоком `finally` из обычного `try {...} catch {...}`, у промисов также есть метод `finally`. Вызов `.finally(f)` похож на `.then(f, f)`, в том смысле, что `f` выполнится в любом случае, когда промис завершится: успешно или с ошибкой. -`finally` хорошо подходит для очистки, например остановки индикатора загрузки, его ведь нужно остановить вне зависимости от результата. +Идея `finally` состоит в том, чтобы настроить обработчик для выполнения очистки/доведения после завершения предыдущих операций. + +Например, остановка индикаторов загрузки, закрытие больше не нужных соединений и т.д. -Например: +Думайте об этом как о завершении вечеринки. Независимо от того, была ли вечеринка хорошей или плохой, сколько на ней было друзей, нам все равно нужно (или, по крайней мере, мы должны) сделать уборку после нее. + +Код может выглядеть следующим образом: ```js new Promise((resolve, reject) => { - /* сделать что-то, что займёт время, и после вызвать resolve/reject */ + /* сделать что-то, что займёт время, и после вызвать resolve или может reject */ }) *!* // выполнится, когда промис завершится, независимо от того, успешно или нет .finally(() => остановить индикатор загрузки) + // таким образом, индикатор загрузки всегда останавливается, прежде чем мы продолжим */!* .then(result => показать результат, err => показать ошибку) ``` -Но это не совсем псевдоним `then(f,f)`, как можно было подумать. Существует несколько важных отличий: +Обратите внимание, что `finally(f)` - это не совсем псевдоним `then(f,f)`, как можно было подумать. + +Есть важные различия: 1. Обработчик, вызываемый из `finally`, не имеет аргументов. В `finally` мы не знаем, как был завершён промис. И это нормально, потому что обычно наша задача - выполнить "общие" завершающие процедуры. + + Пожалуйста, взгляните на приведенный выше пример: как вы можете видеть, обработчик `finally` не имеет аргументов, а результат промиса обрабатывается в следующем обработчике. + 2. Обработчик `finally` "пропускает" результат или ошибку дальше, к последующим обработчикам. Например, здесь результат проходит через `finally` к `then`: + ```js run new Promise((resolve, reject) => { - setTimeout(() => resolve("result"), 2000) + setTimeout(() => resolve("value"), 2000); }) - .finally(() => alert("Промис завершён")) - .then(result => alert(result)); // <-- .then обработает результат + .finally(() => alert("Промис завершён")) // срабатывает первым + .then(result => alert(result)); // <-- .then показывает "value" ``` - + + Как вы можете видеть, значение возвращаемое первым промисом, передается через `finally` к следующему `then`. + + Это очень удобно, потому что `finally` не предназначен для обработки результата промиса. Как уже было сказано, это место для проведения общей очистки, независимо от того, каков был результат. + А здесь ошибка из промиса проходит через `finally` к `catch`: ```js run new Promise((resolve, reject) => { throw new Error("error"); }) - .finally(() => alert("Промис завершён")) - .catch(err => alert(err)); // <-- .catch обработает объект ошибки + .finally(() => alert("Промис завершён")) // срабатывает первым + .catch(err => alert(err)); // <-- .catch показывает ошибку ``` - Это очень удобно, потому что `finally` не предназначен для обработки результата промиса. Так что он просто пропускает его через себя дальше. +3. Обработчик `finally` также не должен ничего возвращать. Если это так, то возвращаемое значение молча игнорируется. + + Единственным исключением из этого правила является случай, когда обработчик `finally` выдает ошибку. Затем эта ошибка передается следующему обработчику вместо любого предыдущего результата. + +Подведем итог: - Мы более подробно поговорим о создании цепочек промисов и передаче результатов между обработчиками в следующей главе. +- Обработчик `finally` не получает результат предыдущего обработчика (у него нет аргументов). Вместо этого этот результат передается следующему подходящему обработчику. +- Если обработчик `finally` возвращает что-то, это игнорируется. +- Когда `finally` выдает ошибку, выполнение переходит к ближайшему обработчику ошибок. -3. Последнее, но не менее значимое: вызов `.finally(f)` удобнее, чем `.then(f, f)` - не надо дублировать функции f. +Эти функции полезны и заставляют все работать правильно, если мы используем `finally` так, как предполагается: для общих процедур очистки. ````smart header="На завершённых промисах обработчики запускаются сразу" -Если промис в состоянии ожидания, обработчики в `.then/catch/finally` будут ждать его. Однако, если промис уже завершён, то обработчики выполнятся сразу: +Если промис в состоянии ожидания, обработчики в `.then/catch/finally` будут ждать его. + +Иногда может случиться так, что промис уже выполнен, когда мы добавляем к нему обработчик. + +В таком случае эти обработчики просто запускаются немедленно: ```js run // при создании промиса он сразу переводится в состояние "успешно завершён" @@ -272,10 +298,10 @@ promise.then(alert); // готово! (выведется сразу) ``` ```` -Теперь рассмотрим несколько практических примеров того, как промисы могут облегчить нам написание асинхронного кода. - ## Пример: loadScript [#loadscript] +Теперь рассмотрим несколько практических примеров того, как промисы могут облегчить нам написание асинхронного кода. + У нас есть функция `loadScript` для загрузки скрипта из предыдущей главы. Давайте вспомним, как выглядел вариант с колбэками: @@ -294,7 +320,7 @@ function loadScript(src, callback) { Теперь перепишем её, используя `Promise`. -Новой функции `loadScript` будет не нужен аргумент `callback`. Вместо этого она будет создавать и возвращать объект `Promise`, который будет переходить в состояние "успешно завершён", когда загрузка закончится. Внешний код может добавлять обработчики ("подписчиков"), используя `.then`: +Новой функции `loadScript` более не нужен аргумент `callback`. Вместо этого она будет создавать и возвращать объект `Promise`, который перейдет в состояние "успешно завершён", когда загрузка закончится. Внешний код может добавлять обработчики ("подписчиков"), используя `.then`: ```js run function loadScript(src) { diff --git a/1-js/11-async/02-promise-basics/promise-reject-1.svg b/1-js/11-async/02-promise-basics/promise-reject-1.svg index 5a94d80525..777e477394 100644 --- a/1-js/11-async/02-promise-basics/promise-reject-1.svg +++ b/1-js/11-async/02-promise-basics/promise-reject-1.svg @@ -1 +1 @@ -new Promise(executor)state: "pending" result: undefinedreject(error)state: "rejected" result: error \ No newline at end of file +new Promise(executor)state: "pending" result: undefinedreject(error)state: "rejected" result: error \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/promise-resolve-1.svg b/1-js/11-async/02-promise-basics/promise-resolve-1.svg index f70eec8ccd..f1f34eaee1 100644 --- a/1-js/11-async/02-promise-basics/promise-resolve-1.svg +++ b/1-js/11-async/02-promise-basics/promise-resolve-1.svg @@ -1 +1 @@ -new Promise(executor)state: "pending" result: undefinedresolve("done")state: "fulfilled" result: "done" \ No newline at end of file +new Promise(executor)state: "pending" result: undefinedresolve("done")state: "fulfilled" result: "done" \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/promise-resolve-reject.svg b/1-js/11-async/02-promise-basics/promise-resolve-reject.svg index 657da3ff65..80593a3a4b 100644 --- a/1-js/11-async/02-promise-basics/promise-resolve-reject.svg +++ b/1-js/11-async/02-promise-basics/promise-resolve-reject.svg @@ -1 +1 @@ -new Promise(executor)state: "pending" result: undefinedresolve(value)reject(error)state: "fulfilled" result: valuestate: "rejected" result: error \ No newline at end of file +new Promise(executor)state: "pending" result: undefinedresolve(value)reject(error)state: "fulfilled" result: valuestate: "rejected" result: error \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/article.md b/1-js/11-async/03-promise-chaining/article.md index ec21105850..7fc2eb886a 100644 --- a/1-js/11-async/03-promise-chaining/article.md +++ b/1-js/11-async/03-promise-chaining/article.md @@ -86,7 +86,7 @@ promise.then(function(result) { Обработчик `handler`, переданный в `.then(handler)`, может вернуть промис. -В этом случае дальнейшие обработчики ожидают, пока оно выполнится, и затем получают его результат. +В этом случае дальнейшие обработчики ожидают, пока он выполнится, и затем получают его результат. Например: diff --git a/1-js/11-async/03-promise-chaining/promise-handler-variants.svg b/1-js/11-async/03-promise-chaining/promise-handler-variants.svg index 0ad3d3f86b..cf5a2c7a69 100644 --- a/1-js/11-async/03-promise-chaining/promise-handler-variants.svg +++ b/1-js/11-async/03-promise-chaining/promise-handler-variants.svg @@ -1 +1 @@ -возвратом значениявозвратом промисаошибкойstate: "fulfilled" result: valuestate: "rejected" result: error...с результатом нового промиса...state: "pending" result: undefinedвызов .then(handler) всегда возвращает промис:если handler заканчивается…этот промис завершается с: \ No newline at end of file +возвратом значениявозвратом промисаошибкойstate: "fulfilled" result: valuestate: "rejected" result: error...с результатом нового промиса...state: "pending" result: undefinedвызов .then(handler) всегда возвращает промис:если handler заканчивается…этот промис завершается с: \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-then-chain.svg b/1-js/11-async/03-promise-chaining/promise-then-chain.svg index 631ad64a7a..fb60142fbd 100644 --- a/1-js/11-async/03-promise-chaining/promise-then-chain.svg +++ b/1-js/11-async/03-promise-chaining/promise-then-chain.svg @@ -1 +1 @@ -.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file +.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-then-many.svg b/1-js/11-async/03-promise-chaining/promise-then-many.svg index a50359e5f8..8fea7beaaf 100644 --- a/1-js/11-async/03-promise-chaining/promise-then-many.svg +++ b/1-js/11-async/03-promise-chaining/promise-then-many.svg @@ -1 +1 @@ -.thennew Promiseresolve(1).then.then \ No newline at end of file +.thennew Promiseresolve(1).then.then \ No newline at end of file diff --git a/1-js/11-async/04-promise-error-handling/article.md b/1-js/11-async/04-promise-error-handling/article.md index f52216290c..fe0488bdde 100644 --- a/1-js/11-async/04-promise-error-handling/article.md +++ b/1-js/11-async/04-promise-error-handling/article.md @@ -199,6 +199,7 @@ new Promise(function() { ## Итого - `.catch` перехватывает все виды ошибок в промисах: будь то вызов `reject()` или ошибка, брошенная в обработчике при помощи `throw`. +- `.then` также перехватывает ошибки таким же образом, если задан второй аргумент (который является обработчиком ошибок). - Необходимо размещать `.catch` там, где мы хотим обработать ошибки и знаем, как это сделать. Обработчик может проанализировать ошибку (могут быть полезны пользовательские классы ошибок) и пробросить её, если ничего не знает о ней (возможно, это программная ошибка). - Можно и совсем не использовать `.catch`, если нет нормального способа восстановиться после ошибки. - В любом случае нам следует использовать обработчик события `unhandledrejection` (для браузеров и аналог для других окружений), чтобы отслеживать необработанные ошибки и информировать о них пользователя (и, возможно, наш сервер), благодаря чему наше приложение никогда не будет "просто умирать". diff --git a/1-js/11-async/04-promise-error-handling/promise-then-chain.svg b/1-js/11-async/04-promise-error-handling/promise-then-chain.svg index 631ad64a7a..fb60142fbd 100644 --- a/1-js/11-async/04-promise-error-handling/promise-then-chain.svg +++ b/1-js/11-async/04-promise-error-handling/promise-then-chain.svg @@ -1 +1 @@ -.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file +.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file diff --git a/1-js/11-async/04-promise-error-handling/promise-then-many.svg b/1-js/11-async/04-promise-error-handling/promise-then-many.svg index a50359e5f8..8fea7beaaf 100644 --- a/1-js/11-async/04-promise-error-handling/promise-then-many.svg +++ b/1-js/11-async/04-promise-error-handling/promise-then-many.svg @@ -1 +1 @@ -.thennew Promiseresolve(1).then.then \ No newline at end of file +.thennew Promiseresolve(1).then.then \ No newline at end of file diff --git a/1-js/11-async/05-promise-api/article.md b/1-js/11-async/05-promise-api/article.md index b9136cff01..d50fc9ed44 100644 --- a/1-js/11-async/05-promise-api/article.md +++ b/1-js/11-async/05-promise-api/article.md @@ -1,6 +1,6 @@ # Promise API -В классе `Promise` есть 5 статических методов. Давайте познакомимся с ними. +В классе `Promise` есть 6 статических методов. Давайте познакомимся с ними. ## Promise.all @@ -13,7 +13,7 @@ Синтаксис: ```js -let promise = Promise.all([...промисы...]); +let promise = Promise.all(iterable); ``` Метод `Promise.all` принимает массив промисов (может принимать любой перебираемый объект, но обычно используется массив) и возвращает новый промис. @@ -33,7 +33,7 @@ Promise.all([ Обратите внимание, что порядок элементов массива в точности соответствует порядку исходных промисов. Даже если первый промис будет выполняться дольше всех, его результат всё равно будет первым в массиве. -Часто применяемый трюк - пропустить массив данных через map-функцию, которая для каждого элемента создаст задачу-промис, и затем обернёт получившийся массив в `Promise.all`. +Часто применяемый трюк - пропустить массив данных через map-функцию, которая для каждого элемента создаст задачу-промис, и затем обернуть получившийся массив в `Promise.all`. Например, если у нас есть массив ссылок, то мы можем загрузить их вот так: @@ -98,7 +98,7 @@ Promise.all([ Например, если сделано несколько вызовов `fetch`, как в примере выше, и один не прошёл, то остальные будут всё ещё выполняться, но `Promise.all` за ними уже не смотрит. Скорее всего, они так или иначе завершатся, но их результаты будут проигнорированы. -`Promise.all` ничего не делает для их отмены, так как в промисах вообще нет концепии "отмены". В главе мы рассмотрим `AbortController`, который помогает с этим, но он не является частью Promise API. +`Promise.all` ничего не делает для их отмены, так как в промисах вообще нет концепции "отмены". В главе мы рассмотрим `AbortController`, который помогает с этим, но он не является частью Promise API. ``` ````smart header="`Promise.all(iterable)` разрешает передавать не-промисы в `iterable` (перебираемом объекте)" @@ -123,6 +123,12 @@ Promise.all([ [recent browser="new"] +Синтаксис: + +```js +let promise = Promise.allSettled(iterable); +``` + `Promise.all` завершается с ошибкой, если она возникает в любом из переданных промисов. Это подходит для ситуаций "всё или ничего", когда нам нужны *все* результаты для продолжения: ```js @@ -181,10 +187,10 @@ Promise.allSettled(urls.map(url => fetch(url))) if(!Promise.allSettled) { Promise.allSettled = function(promises) { return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({ - state: 'fulfilled', + status: 'fulfilled', value: value }), error => ({ - state: 'rejected', + status: 'rejected', reason: error })))); }; @@ -199,7 +205,7 @@ if(!Promise.allSettled) { ## Promise.race -Метод очень похож на `Promise.all`, но ждёт только первый промис, из которого берёт результат (или ошибку). +Метод очень похож на `Promise.all`, но ждёт только первый *выполненный* промис, из которого берёт результат (или ошибку). Синтаксис: @@ -219,12 +225,53 @@ Promise.race([ Быстрее всех выполнился первый промис, он и дал результат. После этого остальные промисы игнорируются. +## Promise.any + +Метод очень похож на `Promise.race`, но ждёт только первый *успешно выполненный* промис, из которого берёт результат. + +Если ни один из переданных промисов не завершится успешно, тогда возвращённый объект Promise будет отклонён с помощью `AggregateError` – специального объекта ошибок, который хранит все ошибки промисов в своём свойстве `errors`. + +Синтаксис: + +```js +let promise = Promise.any(iterable); +``` + +Например, здесь, результатом будет `1`: + +```js run +Promise.any([ + 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 +``` + +Первый промис в этом примере был самым быстрым, но он был отклонён, поэтому результатом стал второй. После того, как первый успешно выполненный промис "выиграет гонку", все дальнейшие результаты будут проигнорированы. + +Вот пример, в котором все промисы отклоняются: + +```js run +Promise.any([ + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ошибка!")), 1000)), + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ещё одна ошибка!")), 2000)) +]).catch(error => { + console.log(error.constructor.name); // AggregateError + console.log(error.errors[0]); // Error: Ошибка! + console.log(error.errors[1]); // Error: Ещё одна ошибка! +}); +``` + +Как вы можете видеть, объекты ошибок для отклонённых промисов доступны в свойстве `errors` объекта `AggregateError`. + ## Promise.resolve/reject -Методы `Promise.resolve` и `Promise.reject` редко используются в современном коде, так как синтаксис `async/await` (мы рассмотрим его [чуть позже](info:async-await)) делает его, в общем-то, не нужным. +Методы `Promise.resolve` и `Promise.reject` редко используются в современном коде, так как синтаксис `async/await` (мы рассмотрим его [чуть позже](info:async-await)) делает их, в общем-то, не нужными. Мы рассмотрим их здесь для полноты картины, а также для тех, кто по каким-то причинам не может использовать `async/await`. +### Promise.resolve + - `Promise.resolve(value)` создаёт успешно выполненный промис с результатом `value`. То же самое, что: @@ -272,14 +319,15 @@ let promise = new Promise((resolve, reject) => reject(error)); ## Итого -Мы ознакомились с пятью статическими методами класса `Promise`: +Мы ознакомились с шестью статическими методами класса `Promise`: 1. `Promise.all(promises)` -- ожидает выполнения всех промисов и возвращает массив с результатами. Если любой из указанных промисов вернёт ошибку, то результатом работы `Promise.all` будет эта ошибка, результаты остальных промисов будут игнорироваться. 2. `Promise.allSettled(promises)` (добавлен недавно) -- ждёт, пока все промисы завершатся и возвращает их результаты в виде массива с объектами, у каждого объекта два свойства: - - `state`: `"fulfilled"`, если выполнен успешно или `"rejected"`, если ошибка, + - `status`: `"fulfilled"`, если выполнен успешно или `"rejected"`, если ошибка, - `value` - результат, если успешно или `reason` - ошибка, если нет. -3. `Promise.race(promises)` -- ожидает первый выполненный промис, который становится его результатом, остальные игнорируются. -4. `Promise.resolve(value)` -- возвращает успешно выполнившийся промис с результатом `value`. -5. `Promise.reject(error)` -- возвращает промис с ошибкой `error`. +3. `Promise.race(promises)` -- ожидает первый *выполненный* промис, который становится его результатом, остальные игнорируются. +4. `Promise.any(promises)` (добавлен недавно) -- ожидает первый *успешно выполненный* промис, который становится его результатом, остальные игнорируются. Если все переданные промисы отклонены, [`AggregateError`](mdn:js/AggregateError) становится ошибкой `Promise.any`. +5. `Promise.resolve(value)` -- возвращает успешно выполнившийся промис с результатом `value`. +6. `Promise.reject(error)` -- возвращает промис с ошибкой `error`. Из всех перечисленных методов, самый часто используемый - это, пожалуй, `Promise.all`. diff --git a/1-js/11-async/06-promisify/article.md b/1-js/11-async/06-promisify/article.md index 69d8bf0331..a5481112a7 100644 --- a/1-js/11-async/06-promisify/article.md +++ b/1-js/11-async/06-promisify/article.md @@ -6,7 +6,7 @@ Например, у нас есть `loadScript(src, callback)` из главы . -```js run +```js function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; @@ -53,7 +53,7 @@ function promisify(f) { return new Promise((resolve, reject) => { function callback(err, result) { // наш специальный колбэк для f if (err) { - return reject(err); + reject(err); } else { resolve(result); } @@ -84,7 +84,7 @@ function promisify(f, manyArgs = false) { return new Promise((resolve, reject) => { function *!*callback(err, ...results*/!*) { // наш специальный колбэк для f if (err) { - return reject(err); + reject(err); } else { // делаем resolve для всех results колбэка, если задано manyArgs *!*resolve(manyArgs ? results : results[0]);*/!* @@ -108,7 +108,7 @@ f(...).then(arrayOfResults => ..., err => ...) Также существуют модули с более гибкой промисификацией, например, [es6-promisify](https://github.com/digitaldesignlabs/es6-promisify) или встроенная функция `util.promisify` в Node.js. ```smart -Промисификация - это отличный подход, особенно, если вы будете использовать `async/await` (см. следующую главу), но она не является тотальной заменой любых колбэков. +Промисификация - это отличный подход, особенно, если вы будете использовать `async/await` (см. следующую главу об ) но она не является тотальной заменой любых колбэков. Помните, промис может иметь только один результат, но колбэк технически может вызываться сколько угодно раз. diff --git a/1-js/11-async/07-microtask-queue/article.md b/1-js/11-async/07-microtask-queue/article.md index dd0c91a5fc..3a255f8905 100644 --- a/1-js/11-async/07-microtask-queue/article.md +++ b/1-js/11-async/07-microtask-queue/article.md @@ -107,7 +107,7 @@ window.addEventListener('unhandledrejection', event => alert(event.reason)); ## Итого -Обработка промисов всегда асинхронная, т.к. все действия промисов проходят через внутреннюю очередь "promise jobs", так называемую "очередь микрозадач (microtask queue)" (термин v8). +Обработка промисов всегда асинхронная, т.к. все действия промисов проходят через внутреннюю очередь "promise jobs", так называемую "очередь микрозадач (microtask queue)" (термин V8). Таким образом, обработчики `.then/catch/finally` вызываются после выполнения текущего кода. diff --git a/1-js/11-async/07-microtask-queue/promiseQueue.svg b/1-js/11-async/07-microtask-queue/promiseQueue.svg index c1a087bb4f..d9df04d704 100644 --- a/1-js/11-async/07-microtask-queue/promiseQueue.svg +++ b/1-js/11-async/07-microtask-queue/promiseQueue.svg @@ -1 +1 @@ -promise . then ( handler ); ... alert ( "code finished" );handler в очередизапускается handler из очередивыполнение скрипта завершено \ No newline at end of file +promise . then ( handler ); ... alert ( "код завершён" );handler в очередизапускается handler из очередивыполнение скрипта завершено \ No newline at end of file diff --git a/1-js/11-async/08-async-await/article.md b/1-js/11-async/08-async-await/article.md index 1b6cba745c..5236c75027 100644 --- a/1-js/11-async/08-async-await/article.md +++ b/1-js/11-async/08-async-await/article.md @@ -133,7 +133,7 @@ let user = await response.json(); Можно обернуть этот код в анонимную `async`–функцию, тогда всё заработает: -```js run +```js (async () => { let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); @@ -206,7 +206,7 @@ async function f() { } ``` -Делает тоже самое, что и такой: +Делает то же самое, что и такой: ```js async function f() { @@ -216,7 +216,7 @@ async function f() { } ``` -Но есть отличие, на практике промис может завершиться с ошибкой не сразу, а через некоторое время. В этом случае будет задержка, а затем `await` выбросит исключение. +Но есть отличие: на практике промис может завершиться с ошибкой не сразу, а через некоторое время. В этом случае будет задержка, а затем `await` выбросит исключение. Такие ошибки можно ловить, используя `try..catch`, как с обычным `throw`: diff --git a/1-js/12-generators-iterators/1-generators/article.md b/1-js/12-generators-iterators/1-generators/article.md index 36beb2eb6f..2277ec4316 100644 --- a/1-js/12-generators-iterators/1-generators/article.md +++ b/1-js/12-generators-iterators/1-generators/article.md @@ -223,7 +223,7 @@ alert( [...range] ); // 1,2,3,4,5 ```smart header="Генераторы могут генерировать бесконечно" В примерах выше мы генерировали конечные последовательности, но мы также можем сделать генератор, который будет возвращать значения бесконечно. Например, бесконечная последовательность псевдослучайных чисел. -Конечно, нам потребуется `break` (или `return`) в цикле `for..of` по такому генератору, иначе цикл будет продолжаться бесконечно и скрипт "зависнет". +Конечно, нам потребуется `break` (или `return`) в цикле `for..of` по такому генератору, иначе цикл будет продолжаться бесконечно, и скрипт "зависнет". ``` ## Композиция генераторов @@ -239,7 +239,7 @@ function* generateSequence(start, end) { ``` Мы хотели бы использовать её при генерации более сложной последовательности: -- сначала цифры `0..9` (с кодами символов 48..57), +- сначала цифры `0..9` (с кодами символов 48..57) - за которыми следуют буквы в верхнем регистре `A..Z` (коды символов 65..90) - за которыми следуют буквы алфавита `a..z` (коды символов 97..122) diff --git a/1-js/12-generators-iterators/1-generators/genYield2-2.svg b/1-js/12-generators-iterators/1-generators/genYield2-2.svg index d35f11a54a..e100650905 100644 --- a/1-js/12-generators-iterators/1-generators/genYield2-2.svg +++ b/1-js/12-generators-iterators/1-generators/genYield2-2.svg @@ -1 +1 @@ -"2 + 2 = ?""3 * 3 = ?". next ( 4 ). next ( 9 )ГенераторВызывающий код \ No newline at end of file +"2 + 2 = ?""3 * 3 = ?". next ( 4 ). next ( 9 )ГенераторВызывающий код \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/genYield2.svg b/1-js/12-generators-iterators/1-generators/genYield2.svg index 224dbb7e59..9776156cd2 100644 --- a/1-js/12-generators-iterators/1-generators/genYield2.svg +++ b/1-js/12-generators-iterators/1-generators/genYield2.svg @@ -1 +1 @@ -question = "2 + 2 = ?"ГенераторВызывающий код.next(4) \ No newline at end of file +question = "2 + 2 = ?"ГенераторВызывающий код.next(4) \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-1.svg b/1-js/12-generators-iterators/1-generators/generateSequence-1.svg index d994d673dd..138df324cc 100644 --- a/1-js/12-generators-iterators/1-generators/generateSequence-1.svg +++ b/1-js/12-generators-iterators/1-generators/generateSequence-1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-2.svg b/1-js/12-generators-iterators/1-generators/generateSequence-2.svg index 05399563a6..7478543a4b 100644 --- a/1-js/12-generators-iterators/1-generators/generateSequence-2.svg +++ b/1-js/12-generators-iterators/1-generators/generateSequence-2.svg @@ -1 +1 @@ -{value: 1, done: false} \ No newline at end of file +{value: 1, done: false} \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-3.svg b/1-js/12-generators-iterators/1-generators/generateSequence-3.svg index a3c6c8274f..d32b114f98 100644 --- a/1-js/12-generators-iterators/1-generators/generateSequence-3.svg +++ b/1-js/12-generators-iterators/1-generators/generateSequence-3.svg @@ -1 +1 @@ -{value: 2, done: false} \ No newline at end of file +{value: 2, done: false} \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-4.svg b/1-js/12-generators-iterators/1-generators/generateSequence-4.svg index c9e5c52fd6..23049fcd21 100644 --- a/1-js/12-generators-iterators/1-generators/generateSequence-4.svg +++ b/1-js/12-generators-iterators/1-generators/generateSequence-4.svg @@ -1 +1 @@ -{value: 3, done: true} \ No newline at end of file +{value: 3, done: true} \ No newline at end of file diff --git a/1-js/13-modules/01-modules-intro/article.md b/1-js/13-modules/01-modules-intro/article.md index b9a12f8015..944eff33c3 100644 --- a/1-js/13-modules/01-modules-intro/article.md +++ b/1-js/13-modules/01-modules-intro/article.md @@ -9,8 +9,8 @@ Например: -- [AMD](https://ru.wikipedia.org/wiki/Asynchronous_module_definition) -- одна из самых старых модульных систем, изначально реализована библиотекой [require.js](http://requirejs.org/). -- [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) -- модульная система, созданная для сервера Node.js. +- [AMD](https://ru.wikipedia.org/wiki/Asynchronous_module_definition) -- одна из самых старых модульных систем, изначально реализована библиотекой [require.js](https://requirejs.org/). +- [CommonJS](https://wiki.commonjs.org/wiki/Modules/1.1) -- модульная система, созданная для сервера Node.js. - [UMD](https://github.com/umdjs/umd) -- ещё одна модульная система, предлагается как универсальная, совместима с AMD и CommonJS. Теперь все они постепенно становятся частью истории, хотя их и можно найти в старых скриптах. @@ -57,6 +57,10 @@ sayHi('John'); // Hello, John! Браузер автоматически загрузит и запустит импортированный модуль (и те, которые он импортирует, если надо), а затем запустит скрипт. +```warn header="Модули не работают локально. Только через HTTP(s)" +Если вы попытаетесь открыть веб-страницу локально, через протокол `file://`, вы обнаружите, что директивы `import/export` не работают. Для тестирования модулей используйте локальный веб-сервер, например, [static-server](https://www.npmjs.com/package/static-server#getting-started) или используйте возможности "живого сервера" вашего редактора, например, расширение [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) для VS Code. +``` + ## Основные возможности модулей Чем отличаются модули от "обычных" скриптов? @@ -81,9 +85,9 @@ sayHi('John'); // Hello, John! [codetabs src="scopes" height="140" current="index.html"] -Модули должны экспортировать функционал, предназначенный для использования извне. А другие модули могут его импортировать. +Модули должны экспортировать функциональность, предназначенную для использования извне. А другие модули могут её импортировать. -Так что нам надо импортировать `user.js` в `hello.js` и взять из него нужный функционал, вместо того чтобы полагаться на глобальные переменные. +Так что нам надо импортировать `user.js` в `hello.js` и взять из него нужную функциональность, вместо того чтобы полагаться на глобальные переменные. Правильный вариант: @@ -377,7 +381,7 @@ import {sayHi} from 'sayHi'; // Ошибка, "голый" модуль - Дублирующиеся внешние скрипты игнорируются. 2. У модулей есть своя область видимости, обмениваться функциональностью можно через `import/export`. 3. В модулях всегда включена директива `use strict`. -4. Код в модулях выполняется только один раз. Экспортируемая функциональность создётся один раз и передаётся всем импортёрам. +4. Код в модулях выполняется только один раз. Экспортируемая функциональность создаётся один раз и передаётся всем импортёрам. Когда мы используем модули, каждый модуль реализует свою функциональность и экспортирует её. Затем мы используем `import`, чтобы напрямую импортировать её туда, куда необходимо. Браузер загружает и анализирует скрипты автоматически. diff --git a/1-js/13-modules/02-import-export/article.md b/1-js/13-modules/02-import-export/article.md index 9f8cbd9f70..a1d5f586b5 100644 --- a/1-js/13-modules/02-import-export/article.md +++ b/1-js/13-modules/02-import-export/article.md @@ -12,7 +12,7 @@ ```js // экспорт массива -*!*export*/!* let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +*!*export*/!* let months = ['Jan', 'Feb', 'Mar', 'Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // экспорт константы *!*export*/!* const MODULES_BECAME_STANDARD_YEAR = 2015; @@ -26,7 +26,7 @@ ``` ````smart header="Не ставится точка с запятой после экспорта класса/функции" -Обратите внимание, что `export` перед классом или функцией не делает их [функциональным выражением](info:function-expressions-arrows). Это всё также объявление функции, хотя и экспортируемое. +Обратите внимание, что `export` перед классом или функцией не делает их [функциональным выражением](info:function-expressions). Это всё также объявление функции, хотя и экспортируемое. Большинство руководств по стилю кода в JavaScript не рекомендуют ставить точку с запятой после объявлений функций или классов. @@ -93,7 +93,7 @@ say.sayBye('John'); Для этого есть несколько причин. -1. Современные инструменты сборки ([webpack](http://webpack.github.io) и другие) собирают модули вместе и оптимизируют их, ускоряя загрузку и удаляя неиспользуемый код. +1. Современные инструменты сборки ([webpack](https://webpack.js.org) и другие) собирают модули вместе и оптимизируют их, ускоряя загрузку и удаляя неиспользуемый код. Предположим, мы добавили в наш проект стороннюю библиотеку `say.js` с множеством функций: ```js @@ -162,7 +162,7 @@ say.*!*bye*/!*('John'); // Bye, John! Естественно, требуется много файлов, если для всего делать отдельный модуль, но это не проблема. Так даже удобнее: навигация по проекту становится проще, особенно, если у файлов хорошие имена, и они структурированы по папкам. -Модули предоставляют специальный синтаксис `export default` ("эспорт по умолчанию") для второго подхода. +Модули предоставляют специальный синтаксис `export default` ("экспорт по умолчанию") для второго подхода. Ставим `export default` перед тем, что нужно экспортировать: @@ -321,7 +321,7 @@ export {default as User} from './user.js'; // реэкспортировать d Зачем это нужно? Рассмотрим практический пример использования. -Представим, что мы пишем "пакет": папку со множеством модулей, из которой часть функционала экспортируется наружу (инструменты вроде NPM позволяют нам публиковать и распространять такие пакеты), а многие модули - просто вспомогательные, для внутреннего использования в других модулях пакета. +Представим, что мы пишем "пакет": папку со множеством модулей, из которой часть функциональности экспортируется наружу (инструменты вроде NPM позволяют нам публиковать и распространять такие пакеты), а многие модули - просто вспомогательные, для внутреннего использования в других модулях пакета. Структура файлов может быть такой: ``` @@ -337,7 +337,7 @@ auth/ ... ``` -Мы бы хотели сделать функционал нашего пакета доступным через единую точку входа: "главный файл" `auth/index.js`. Чтобы можно было использовать его следующим образом: +Мы бы хотели сделать функциональность нашего пакета доступной через единую точку входа: "главный файл" `auth/index.js`. Чтобы можно было использовать её следующим образом: ```js import {login, logout} from 'auth/index.js' @@ -345,7 +345,7 @@ import {login, logout} from 'auth/index.js' Идея в том, что внешние разработчики, которые будут использовать наш пакет, не должны разбираться с его внутренней структурой, рыться в файлах внутри нашего пакета. Всё, что нужно, мы экспортируем в `auth/index.js`, а остальное скрываем от любопытных взглядов. -Так как нужный функционал может быть разбросан по модулям нашего пакета, мы можем импортировать их в `auth/index.js` и тут же экспортировать наружу. +Так как нужная функциональность может быть разбросана по модулям нашего пакета, мы можем импортировать их в `auth/index.js` и тут же экспортировать наружу. ```js // 📁 auth/index.js @@ -421,7 +421,7 @@ export default class User { - Именованные экспорты из модуля: - `import {x [as y], ...} from "module"` -- Экспорт по умолчанию: +- Импорт по умолчанию: - `import x from "module"` - `import {default as x} from "module"` - Всё сразу: diff --git a/1-js/13-modules/03-modules-dynamic-imports/article.md b/1-js/13-modules/03-modules-dynamic-imports/article.md index d073370007..3ebcb63972 100644 --- a/1-js/13-modules/03-modules-dynamic-imports/article.md +++ b/1-js/13-modules/03-modules-dynamic-imports/article.md @@ -23,7 +23,7 @@ if(...) { } ``` -Всё это следствие того, что цель директив `import/export` -- задать костяк структуры кода. Благодаря ним она может быть проанализирована, модули могут быть собраны в один файл специальными инструментами, а неиспользуемые экспорты удалены. Это возможно только благодаря тому, что всё статично. +Всё это следствие того, что цель директив `import/export` -- задать костяк структуры кода. Благодаря им она может быть проанализирована, модули могут быть собраны в один файл специальными инструментами, а неиспользуемые экспорты удалены. Это возможно только благодаря тому, что всё статично. Но как мы можем импортировать модуль динамически, по запросу? diff --git a/1-js/99-js-misc/01-proxy/02-array-negative/task.md b/1-js/99-js-misc/01-proxy/02-array-negative/task.md index 58a34e26b7..2ef8566275 100644 --- a/1-js/99-js-misc/01-proxy/02-array-negative/task.md +++ b/1-js/99-js-misc/01-proxy/02-array-negative/task.md @@ -29,5 +29,5 @@ array = new Proxy(array, { alert( array[-1] ); // 3 alert( array[-2] ); // 2 -// весь остальной функционал массивов должен остаться без изменений +// вся остальная функциональность массивов должна остаться без изменений ``` diff --git a/1-js/99-js-misc/01-proxy/03-observable/solution.md b/1-js/99-js-misc/01-proxy/03-observable/solution.md index 1943b8aeb0..07bcbb64fe 100644 --- a/1-js/99-js-misc/01-proxy/03-observable/solution.md +++ b/1-js/99-js-misc/01-proxy/03-observable/solution.md @@ -1,7 +1,7 @@ Решение состоит из двух частей: 1. При вызове `.observe(handler)` нам нужно где-то сохранить обработчик, чтобы вызвать его позже. Можно хранить обработчики прямо в объекте, создав в нём для этого свой символьный ключ. -2. Нам нужен прокси с ловушкой `set`, чтобы вызывать обратчики при изменении свойств. +2. Нам нужен прокси с ловушкой `set`, чтобы вызывать обработчики при изменении свойств. ```js run let handlers = Symbol('handlers'); diff --git a/1-js/99-js-misc/01-proxy/article.md b/1-js/99-js-misc/01-proxy/article.md index e06c0452b8..1c1e77c3e6 100644 --- a/1-js/99-js-misc/01-proxy/article.md +++ b/1-js/99-js-misc/01-proxy/article.md @@ -8,7 +8,7 @@ Синтаксис: ```js -let proxy = new Proxy(target, handler) +let proxy = new Proxy(target, handler); ``` - `target` -- это объект, для которого нужно сделать прокси, может быть чем угодно, включая функции. @@ -224,7 +224,7 @@ numbers.push("тест"); // TypeError (ловушка set на прокси в alert("Интерпретатор никогда не доходит до этой строки (из-за ошибки в строке выше)"); ``` -Обратите внимание, что встроенный функционал массива по-прежнему работает! Значения добавляются методом `push`. Свойство `length` при этом увеличивается. Наш прокси ничего не ломает. +Обратите внимание, что встроенная функциональность массива по-прежнему работает! Значения добавляются методом `push`. Свойство `length` при этом увеличивается. Наш прокси ничего не ломает. Нам не нужно переопределять методы массива `push` и `unshift` и другие, чтобы добавлять туда проверку на тип, так как внутри себя они используют операцию `[[Set]]`, которая перехватывается прокси. @@ -454,7 +454,6 @@ user = { Впрочем, приватные свойства имеют свои недостатки. В частности, они не наследуются. ``` - ## "В диапазоне" с ловушкой "has" Давайте посмотрим ещё примеры. @@ -610,7 +609,7 @@ sayHi("Вася"); // Привет, Вася! (через 3 секунды) |-----------------|----------------|-------------| | `obj[prop]` | `Reflect.get(obj, prop)` | `[[Get]]` | | `obj[prop] = value` | `Reflect.set(obj, prop, value)` | `[[Set]]` | -| `delete obj[prop]` | `Reflect.deleteProperty(obj, prop)` | `[[HasProperty]]` | +| `delete obj[prop]` | `Reflect.deleteProperty(obj, prop)` | `[[Delete]]` | | `new F(value)` | `Reflect.construct(F, value)` | `[[Construct]]` | | ... | ... | ... | @@ -667,7 +666,7 @@ user.name = "Петя"; // выводит "SET name=Петя" ### Прокси для геттера -Рассмотрим конкретный пример, демонстрирующий, чем лучше `Reflect.get`, и заодно разберёмся, зачем в `get/set` нужен четвёртый аргумент `receiver`, мы его ранее не использовали. +Рассмотрим конкретный пример, демонстрирующий, чем лучше `Reflect.get`, и заодно разберёмся, зачем в `get/set` нужен третий аргумент `receiver`, мы его ранее не использовали. Допустим, у нас есть объект `user` со свойством `_name` и геттером для него. @@ -891,7 +890,7 @@ user = new Proxy(user, { alert(user.getName()); // Гость ``` -Однако, такое решение имеет ряд недостатков, о которых уже говорилось: методу передаётся оригинальный объект, который может быть передан куда-то ещё, и это может поломать весь функционал проксирования. +Однако, такое решение имеет ряд недостатков, о которых уже говорилось: методу передаётся оригинальный объект, который может быть передан куда-то ещё, и это может поломать всю функциональность проксирования. ### Прокси != оригинальный объект diff --git a/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg b/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg index ae70cac68f..8f75484e9c 100644 --- a/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg +++ b/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg @@ -1 +1 @@ -_name: "Гость" name: getter_name: "Админ"user (прокси)исходный useradmin[[Prototype]] \ No newline at end of file +_name: "Guest" name: getter_name: "Admin"user (прокси)исходный useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/99-js-misc/01-proxy/proxy-inherit.svg b/1-js/99-js-misc/01-proxy/proxy-inherit.svg index 847827ea3d..9a5706c55e 100644 --- a/1-js/99-js-misc/01-proxy/proxy-inherit.svg +++ b/1-js/99-js-misc/01-proxy/proxy-inherit.svg @@ -1 +1 @@ -_name: "Гость" name: getteruser (прокси)исходный user \ No newline at end of file +_name: "Guest" name: getteruser (прокси)исходный user \ No newline at end of file diff --git a/1-js/99-js-misc/01-proxy/proxy.svg b/1-js/99-js-misc/01-proxy/proxy.svg index 8aa14bdb82..157e350f44 100644 --- a/1-js/99-js-misc/01-proxy/proxy.svg +++ b/1-js/99-js-misc/01-proxy/proxy.svg @@ -1 +1 @@ -test: 5proxytargetget proxy.test5 \ No newline at end of file +test: 5proxytargetget proxy.test5 \ No newline at end of file diff --git a/1-js/99-js-misc/03-currying-partials/article.md b/1-js/99-js-misc/03-currying-partials/article.md index 607e18c3ed..7e6eddcf33 100644 --- a/1-js/99-js-misc/03-currying-partials/article.md +++ b/1-js/99-js-misc/03-currying-partials/article.md @@ -31,9 +31,9 @@ function sum(a, b) { return a + b; } -let carriedSum = curry(sum); +let curriedSum = curry(sum); -alert( carriedSum(1)(2) ); // 3 +alert( curriedSum(1)(2) ); // 3 ``` Как вы видите, реализация довольна проста: это две обёртки. @@ -49,10 +49,10 @@ function sum(a, b) { return a + b; } -let carriedSum = _.curry(sum); // используем _.carry из lodash +let curriedSum = _.curry(sum); // используем _.curry из lodash -alert( carriedSum(1, 2) ); // 3, можно вызывать как обычно -alert( carriedSum(1)(2) ); // 3, а можно частично +alert( curriedSum(1, 2) ); // 3, можно вызывать как обычно +alert( curriedSum(1)(2) ); // 3, а можно частично ``` ## Каррирование? Зачем? diff --git a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md b/1-js/99-js-misc/04-reference-type/2-check-syntax/solution.md similarity index 95% rename from 1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md rename to 1-js/99-js-misc/04-reference-type/2-check-syntax/solution.md index 17da58944f..dc1acff5ee 100644 --- a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md +++ b/1-js/99-js-misc/04-reference-type/2-check-syntax/solution.md @@ -1,43 +1,37 @@ -**Ошибка**! - -Попробуйте запустить: - -```js run -let user = { - name: "Джон", - go: function() { alert(this.name) } -} - -(user.go)() // ошибка! -``` - -Сообщение об ошибке в большинстве браузеров не даёт понимания, что же пошло не так. - -**Ошибка появляется, потому что точка с запятой пропущена после `user = {...}`.** - -JavaScript не вставляет автоматически точку с запятой перед круглой скобкой `(user.go)()`, поэтому читает этот код так: - -```js no-beautify -let user = { go:... }(user.go)() -``` - -Теперь мы тоже можем увидеть, что такое объединённое выражение синтаксически является вызовом объекта `{ go: ... }` как функции с аргументом `(user.go)`. И это происходит в той же строчке с объявлением переменной `let user`, т.е. объект `user` ещё даже не определён, поэтому получается ошибка. - -Если мы вставим точку с запятой - всё заработает: - -```js run -let user = { - name: "Джон", - go: function() { alert(this.name) } -}*!*;*/!* - -(user.go)() // Джон -``` - -Обратите внимание, что круглые скобки вокруг `(user.go)` ничего не значат. Обычно они определяют последовательность операций (оператор группировки), но здесь вызов метода через точку `.` срабатывает первым в любом случае, поэтому группировка ни на что не влияет. Только точка с запятой имеет значение. - - - - - - +**Ошибка**! + +Попробуйте запустить: + +```js run +let user = { + name: "John", + go: function() { alert(this.name) } +} + +(user.go)() // ошибка! +``` + +Сообщение об ошибке в большинстве браузеров не даёт понимания, что же пошло не так. + +**Ошибка появляется, потому что точка с запятой пропущена после `user = {...}`.** + +JavaScript не вставляет автоматически точку с запятой перед круглой скобкой `(user.go)()`, поэтому читает этот код так: + +```js no-beautify +let user = { go:... }(user.go)() +``` + +Теперь мы тоже можем увидеть, что такое объединённое выражение синтаксически является вызовом объекта `{ go: ... }` как функции с аргументом `(user.go)`. И это происходит в той же строчке с объявлением переменной `let user`, т.е. объект `user` ещё даже не определён, поэтому получается ошибка. + +Если мы вставим точку с запятой - всё заработает: + +```js run +let user = { + name: "John", + go: function() { alert(this.name) } +}*!*;*/!* + +(user.go)() // John +``` + +Обратите внимание, что круглые скобки вокруг `(user.go)` ничего не значат. Обычно они определяют последовательность операций (оператор группировки), но здесь вызов метода через точку `.` срабатывает первым в любом случае, поэтому группировка ни на что не влияет. Только точка с запятой имеет значение. diff --git a/1-js/04-object-basics/04-object-methods/2-check-syntax/task.md b/1-js/99-js-misc/04-reference-type/2-check-syntax/task.md similarity index 89% rename from 1-js/04-object-basics/04-object-methods/2-check-syntax/task.md rename to 1-js/99-js-misc/04-reference-type/2-check-syntax/task.md index 0b5b7681a7..e362e2df0b 100644 --- a/1-js/04-object-basics/04-object-methods/2-check-syntax/task.md +++ b/1-js/99-js-misc/04-reference-type/2-check-syntax/task.md @@ -1,19 +1,19 @@ importance: 2 - + --- - + # Проверка синтаксиса - + Каким будет результат выполнения этого кода? - - + + ```js no-beautify let user = { - name: "Джон", + name: "John", go: function() { alert(this.name) } } - + (user.go)() ``` - + P.S. Здесь есть подвох :) diff --git a/1-js/04-object-basics/04-object-methods/3-why-this/solution.md b/1-js/99-js-misc/04-reference-type/3-why-this/solution.md similarity index 99% rename from 1-js/04-object-basics/04-object-methods/3-why-this/solution.md rename to 1-js/99-js-misc/04-reference-type/3-why-this/solution.md index b801084225..e5d7e3ebbd 100644 --- a/1-js/04-object-basics/04-object-methods/3-why-this/solution.md +++ b/1-js/99-js-misc/04-reference-type/3-why-this/solution.md @@ -2,9 +2,7 @@ Вот как это объясняется. 1. Это обычный вызов метода объекта через точку `.`, и `this` ссылается на объект перед точкой. - 2. Здесь то же самое. Круглые скобки (оператор группировки) тут не изменяют порядок выполнения операций - доступ к методу через точку в любом случае срабатывает первым. - 3. Здесь мы имеем более сложный вызов `(expression).method()`. Такой вызов работает, как если бы он был разделён на 2 строчки: ```js no-beautify @@ -13,10 +11,8 @@ ``` Здесь `f()` выполняется как функция, без передачи значения `this`. - 4. Тут похожая ситуация на случай `(3)` - идёт потеря значения `this`. Чтобы объяснить поведение в примерах `(3)` и `(4)`, нам нужно помнить, что доступ к свойству (через точку или квадратные скобки) возвращает специальное значение ссылочного типа (Reference Type). За исключением вызова метода, любая другая операция (подобно операции присваивания `=` или сравнения через логические операторы, например `||`) превращает это значение в обычное, которое не несёт информации, позволяющей установить `this`. - diff --git a/1-js/04-object-basics/04-object-methods/3-why-this/task.md b/1-js/99-js-misc/04-reference-type/3-why-this/task.md similarity index 87% rename from 1-js/04-object-basics/04-object-methods/3-why-this/task.md rename to 1-js/99-js-misc/04-reference-type/3-why-this/task.md index 5d61a25004..9956f66f95 100644 --- a/1-js/04-object-basics/04-object-methods/3-why-this/task.md +++ b/1-js/99-js-misc/04-reference-type/3-why-this/task.md @@ -4,7 +4,7 @@ importance: 3 # Объясните значение "this" -В представленном ниже коде мы намерены вызвать `user.go()` метод 4 раза подряд. +В представленном ниже коде мы намерены вызвать `obj.go()` метод 4 раза подряд. Но вызовы `(1)` и `(2)` работают иначе, чем `(3)` и `(4)`. Почему? @@ -23,4 +23,3 @@ obj.go(); // (1) [object Object] (obj.go || obj.stop)(); // (4) undefined ``` - diff --git a/1-js/99-js-misc/04-reference-type/article.md b/1-js/99-js-misc/04-reference-type/article.md new file mode 100644 index 0000000000..05bbf824ae --- /dev/null +++ b/1-js/99-js-misc/04-reference-type/article.md @@ -0,0 +1,106 @@ +# Ссылочный тип + +```warn header="Продвинутая возможность языка" +Эта статья охватывает продвинутую тему, чтобы лучше понять некоторые нестандартные случаи. + +Она несильно важна. Многие опытные разработчики прекрасно живут, даже не подозревая об этом. Читайте дальше, если хотите узнать, как все работает под капотом. +``` + +Некоторые хитрые способы вызова метода приводят к потере значения `this`, например: + +```js run +let user = { + name: "Джон", + hi() { alert(this.name); }, + bye() { alert("Пока"); } +}; + +user.hi(); // Джон (простой вызов метода работает хорошо) + +*!* +// теперь давайте попробуем вызывать user.hi или user.bye +// в зависимости от имени пользователя user.name +(user.name == "Джон" ? user.hi : user.bye)(); // Ошибка! +*/!* +``` + +В последней строчке кода используется условный оператор `?`, который определяет, какой будет вызван метод (`user.hi` или `user.bye`) в зависимости от выполнения условия. В данном случае будет выбран `user.hi`. + +Затем метод тут же вызывается с помощью скобок `()`. Но вызов не работает как положено! + +Вы можете видеть, что при вызове будет ошибка, потому что значением `"this"` внутри функции становится `undefined` (полагаем, что у нас строгий режим). + +Так работает (доступ к методу объекта через точку): +```js +user.hi(); +``` + +Так уже не работает (вызываемый метод вычисляется): +```js +(user.name == "John" ? user.hi : user.bye)(); // Ошибка! +``` + +Почему? Если мы хотим понять, почему так происходит, давайте разберёмся (заглянем под капот), как работает вызов методов (`obj.method()`). + +## Ссылочный тип: объяснение + +Присмотревшись поближе, в выражении `obj.method()` можно заметить две операции: + +1. Сначала оператор точка `'.'` возвращает свойство объекта - его метод (`obj.method`). +2. Затем скобки `()` вызывают этот метод (исполняется код метода). + +Итак, каким же образом информация о `this` передаётся из первой части во вторую? + +Если мы поместим эти операции в отдельные строки, то значение `this`, естественно, будет потеряно: + +```js run +let user = { + name: "John", + hi() { alert(this.name); } +}; + +*!* +// разделим получение метода объекта и его вызов в разных строках +let hi = user.hi; +hi(); // Ошибка, потому что значением this является undefined +*/!* +``` + +Здесь `hi = user.hi` сохраняет функцию в переменной, и далее в последней строке она вызывается полностью сама по себе, без объекта, так что нет `this`. + +**Для работы вызовов типа `user.hi()`, JavaScript использует трюк - точка `'.'` возвращает не саму функцию, а специальное значение "ссылочного типа", называемого [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).** + +Этот ссылочный тип (Reference Type) является внутренним типом. Мы не можем явно использовать его, но он используется внутри языка. + +Значение ссылочного типа - это "триплет": комбинация из трёх значений `(base, name, strict)`, где: + +- `base` - это объект. +- `name` - это имя свойства объекта. +- `strict` - это режим исполнения. Является true, если действует строгий режим (`use strict`). + +Результатом доступа к свойству `user.hi` является не функция, а значение ссылочного типа. Для `user.hi` в строгом режиме оно будет таким: + +```js +// значение ссылочного типа (Reference Type) +(user, "hi", true) +``` + +Когда скобки `()` применяются к значению ссылочного типа (происходит вызов), то они получают полную информацию об объекте и его методе, и могут поставить правильный `this` (`user` в данном случае, по `base`). + +Ссылочный тип - исключительно внутренний, промежуточный, используемый, чтобы передать информацию от точки `.` до вызывающих скобок `()`. + +При любой другой операции, например, присваивании `hi = user.hi`, ссылочный тип заменяется на собственно значение `user.hi` (функцию), и дальше работа уже идёт только с ней. Поэтому дальнейший вызов происходит уже без `this`. + +Таким образом, значение `this` передаётся правильно, только если функция вызывается напрямую с использованием синтаксиса точки `obj.method()` или квадратных скобок `obj['method']()` (они делают то же самое). Существуют различные способы решения этой проблемы: одним из таких является [func.bind()](/bind#solution-2-bind). + +## Итого + +Ссылочный тип - это внутренний тип языка. + +Чтение свойства, например, с точкой `.` в `obj.method()` возвращает не точное значение свойства, а специальное значение "ссылочного типа", в котором хранится как значение свойства, так и объект, из которого оно было взято. + +Это нужно для последующего вызова метода `()`, чтобы получить объект и установить для него `this`. + +Для всех остальных операций ссылочный тип автоматически становится значением свойства (в нашем случае функцией). + +Вся механика скрыта от наших глаз. Это имеет значение только в особых случаях, например, когда метод динамически извлекается из объекта с использованием выражения. diff --git a/99-archive/089-bitwise-operators/1-bitwise-operator-value/solution.md b/1-js/99-js-misc/05-bitwise-operators/1-bitwise-operator-value/solution.md old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/1-bitwise-operator-value/solution.md rename to 1-js/99-js-misc/05-bitwise-operators/1-bitwise-operator-value/solution.md diff --git a/99-archive/089-bitwise-operators/1-bitwise-operator-value/task.md b/1-js/99-js-misc/05-bitwise-operators/1-bitwise-operator-value/task.md old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/1-bitwise-operator-value/task.md rename to 1-js/99-js-misc/05-bitwise-operators/1-bitwise-operator-value/task.md diff --git a/99-archive/089-bitwise-operators/2-check-integer/solution.md b/1-js/99-js-misc/05-bitwise-operators/2-check-integer/solution.md old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/2-check-integer/solution.md rename to 1-js/99-js-misc/05-bitwise-operators/2-check-integer/solution.md diff --git a/99-archive/089-bitwise-operators/2-check-integer/task.md b/1-js/99-js-misc/05-bitwise-operators/2-check-integer/task.md old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/2-check-integer/task.md rename to 1-js/99-js-misc/05-bitwise-operators/2-check-integer/task.md diff --git a/99-archive/089-bitwise-operators/3-bitwise-symmetry/solution.md b/1-js/99-js-misc/05-bitwise-operators/3-bitwise-symmetry/solution.md old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/3-bitwise-symmetry/solution.md rename to 1-js/99-js-misc/05-bitwise-operators/3-bitwise-symmetry/solution.md diff --git a/99-archive/089-bitwise-operators/3-bitwise-symmetry/task.md b/1-js/99-js-misc/05-bitwise-operators/3-bitwise-symmetry/task.md old mode 100755 new mode 100644 similarity index 82% rename from 99-archive/089-bitwise-operators/3-bitwise-symmetry/task.md rename to 1-js/99-js-misc/05-bitwise-operators/3-bitwise-symmetry/task.md index a33d505c06..50e7813e7e --- a/99-archive/089-bitwise-operators/3-bitwise-symmetry/task.md +++ b/1-js/99-js-misc/05-bitwise-operators/3-bitwise-symmetry/task.md @@ -10,4 +10,4 @@ importance: 5 - `(a & b) == (b & a)` - `(a | b) == (b | a)` -Иными словами, при перемене мест -- всегда ли результат остаётся тем же? +Иными словами, при перемене мест -- всегда ли результат останется тем же? diff --git a/99-archive/089-bitwise-operators/4-bit-rounding/solution.md b/1-js/99-js-misc/05-bitwise-operators/4-bit-rounding/solution.md old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/4-bit-rounding/solution.md rename to 1-js/99-js-misc/05-bitwise-operators/4-bit-rounding/solution.md diff --git a/99-archive/089-bitwise-operators/4-bit-rounding/task.md b/1-js/99-js-misc/05-bitwise-operators/4-bit-rounding/task.md old mode 100755 new mode 100644 similarity index 65% rename from 99-archive/089-bitwise-operators/4-bit-rounding/task.md rename to 1-js/99-js-misc/05-bitwise-operators/4-bit-rounding/task.md index f650fb8774..2aa66c0f96 --- a/99-archive/089-bitwise-operators/4-bit-rounding/task.md +++ b/1-js/99-js-misc/05-bitwise-operators/4-bit-rounding/task.md @@ -4,7 +4,7 @@ importance: 5 # Почему результат разный? -Почему результат второго `alert'а` такой странный? +Почему результат второго `alert` такой странный? ```js run alert( 123456789 ^ 0 ); // 123456789 diff --git a/99-archive/089-bitwise-operators/article.md b/1-js/99-js-misc/05-bitwise-operators/article.md similarity index 95% rename from 99-archive/089-bitwise-operators/article.md rename to 1-js/99-js-misc/05-bitwise-operators/article.md index 13d0a49567..ee23388ecc 100644 --- a/99-archive/089-bitwise-operators/article.md +++ b/1-js/99-js-misc/05-bitwise-operators/article.md @@ -1,8 +1,3 @@ -archive: - ref: null - ---- - # Побитовые операторы Побитовые операторы интерпретируют операнды как последовательность из 32 битов (нулей и единиц). Они производят операции, используя двоичное представление числа, и возвращают новую последовательность из 32 бит (число) в качестве результата. @@ -17,7 +12,7 @@ archive: Разберём, как устроены числа внутри подробнее, это необходимо знать для битовых операций с ними. -- Что такое [двоичная система счисления](http://ru.wikipedia.org/wiki/%C4%E2%EE%E8%F7%ED%E0%FF_%F1%E8%F1%F2%E5%EC%E0_%F1%F7%E8%F1%EB%E5%ED%E8%FF), вам, надеюсь, уже известно. При разборе побитовых операций мы будем обсуждать именно двоичное представление чисел, из 32 бит. +- Что такое [двоичная система счисления](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%81%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F), вам, надеюсь, уже известно. При разборе побитовых операций мы будем обсуждать именно двоичное представление чисел, из 32 бит. - *Старший бит слева* -- это научное название для самого обычного порядка записи цифр (от большего разряда к меньшему). При этом, если больший разряд отсутствует, то соответствующий бит равен нулю. Примеры представления чисел в двоичной системе: @@ -125,11 +120,11 @@ archive: Например: ```js run -var access = parseInt("11000", 2); // получаем число из строки +let access = parseInt("11000", 2); // получаем число из строки alert( access ); // 24, число с таким 2-ным представлением -var access2 = access.toString(2); // обратно двоичную строку из числа +let access2 = access.toString(2); // обратно двоичную строку из числа alert( access2 ); // 11000 ``` @@ -267,7 +262,7 @@ alert( access2 ); // 11000 Ключ: 1220461917 в двоичном виде: 01001000101111101100010101011101 -Результат операции 1220461917 ^ key: +Результат операции 1220461908 ^ key: 00000000000000000000000000001001 Результат в 10-ной системе (исходное сообщение): 9 @@ -350,7 +345,7 @@ alert(10000000000 * 2); // 20000000000, обычное умножение Этот оператор сдвигает биты вправо, отбрасывая лишние. При этом слева добавляется *копия* крайнего-левого бита. -Знак числа (представленный крайним-левым битом) при этом не меняется, так как новый крайний-левый бит имеет то же значение, что и исходном числе. +Знак числа (представленный крайним-левым битом) при этом не меняется, так как новый крайний-левый бит имеет то же значение, что и в исходном числе. Поэтому он назван "переносящим знак". @@ -395,7 +390,7 @@ alert( 100 >> 3 ); // 12, деление на 2 три раза, целая ча Знаковый бит становится равным 0, поэтому результат всегда положителен. -**Для неотрицательных чисел правый сдвиг с заполнением нулями `>>>` и правый сдвиг с переносом знака `>>` дадут одинаковый результат, т.к в обоих случаях слева добавятся нули.** +**Для неотрицательных чисел правый сдвиг с заполнением нулями `>>>` и правый сдвиг с переносом знака `>>` дадут одинаковый результат, т.к. в обоих случаях слева добавятся нули.** Для отрицательных чисел -- результат работы разный. Например, `-9 >>> 2` даст `1073741821`, в отличие от `-9 >> 2` (даёт `-3`): @@ -432,7 +427,7 @@ alert( 100 >> 3 ); // 12, деление на 2 три раза, целая ча - `Редактор` - `Админ` -Каждой роли соответствует ряд доступов к статьям и функционалу сайта. +Каждой роли соответствует ряд доступов к статьям и функциональности сайта. Например, `Гость` может лишь просматривать статьи сайта, а `Редактор` -- ещё и редактировать их, и тому подобное. @@ -541,19 +536,19 @@ alert( 100 >> 3 ); // 12, деление на 2 три раза, целая ча Как правило, доступы задаются в виде констант: ```js no-beautify -var ACCESS_ADMIN = 1; // 00001 -var ACCESS_GOODS_EDIT = 2; // 00010 -var ACCESS_GOODS_VIEW = 4; // 00100 -var ACCESS_ARTICLE_EDIT = 8; // 01000 -var ACCESS_ARTICLE_VIEW = 16; // 10000 +const ACCESS_ADMIN = 1; // 00001 +const ACCESS_GOODS_EDIT = 2; // 00010 +const ACCESS_GOODS_VIEW = 4; // 00100 +const ACCESS_ARTICLE_EDIT = 8; // 01000 +const ACCESS_ARTICLE_VIEW = 16; // 10000 ``` Из этих констант получить нужную комбинацию доступов можно при помощи операции `|`. ```js -var guest = ACCESS_ARTICLE_VIEW | ACCESS_GOODS_VIEW; // 10100 -var editor = guest | ACCESS_ARTICLE_EDIT | ACCESS_GOODS_EDIT; // 11110 -var admin = editor | ACCESS_ADMIN; // 11111 +const guest = ACCESS_ARTICLE_VIEW | ACCESS_GOODS_VIEW; // 10100 +const editor = guest | ACCESS_ARTICLE_EDIT | ACCESS_GOODS_EDIT; // 11110 +const admin = editor | ACCESS_ADMIN; // 11111 ``` Теперь, чтобы понять, есть ли в доступе `editor` нужный доступ, например управление правами -- достаточно применить к нему побитовый оператор И (`&`) с соответствующей константой. @@ -572,7 +567,7 @@ alert(editor & ACCESS_ARTICLE_EDIT); // 8, доступ есть Например, проверим, есть ли права на просмотр ИЛИ изменение товаров. Соответствующие права задаются битом `1` на втором и третьем месте с конца, что даёт число `00110` (=`6` в 10-ной системе). ```js -var check = ACCESS_GOODS_VIEW | ACCESS_GOODS_EDIT; // 6, 00110 +const check = ACCESS_GOODS_VIEW | ACCESS_GOODS_EDIT; // 6, 00110 alert( admin & check ); // не 0, значит есть доступ к просмотру ИЛИ изменению ``` @@ -630,7 +625,7 @@ alert( 1.1 + 1.2 ^ 0 ); // 2, сложение выполнится раньше Как видно из последнего равенства, `~n == 0` только если `n == -1`. Поэтому можно легко проверить равенство `n == -1`: ```js run -var n = 5; +let n = 5; if (~n) { // сработает, т.к. ~n = -(5+1) = -6 alert( "n не -1" ); // выведет! @@ -638,7 +633,7 @@ if (~n) { // сработает, т.к. ~n = -(5+1) = -6 ``` ```js run -var n = -1; +let n = -1; if (~n) { // не сработает, т.к. ~n = -(-1+1) = 0 alert( "...ничего не выведет..." ); @@ -648,7 +643,7 @@ if (~n) { // не сработает, т.к. ~n = -(-1+1) = 0 Проверка на `-1` пригождается, например, при поиске символа в строке. Вызов `str.indexOf("подстрока")` возвращает позицию подстроки в `str`, или `-1` если не нашёл. ```js run -var str = "Проверка"; +let str = "Проверка"; if (~str.indexOf("верка")) { // Сочетание "if (~...indexOf)" читается как "если найдено" alert( 'найдено!' ); @@ -685,5 +680,5 @@ alert( 11 >> 2 ); // 2, целочисленное деление (менее з - Округления числа: `(12.34^0) = 12`. - Проверки на равенство `-1`: `if (~n) { n не -1 }`. -- Упаковки нескольких битововых значений ("флагов") в одно значение. Это экономит память и позволяет проверять наличие комбинации флагов одним оператором `&`. +- Упаковки нескольких битовых значений ("флагов") в одно значение. Это экономит память и позволяет проверять наличие комбинации флагов одним оператором `&`. - Других ситуаций, когда нужны битовые маски. diff --git a/99-archive/089-bitwise-operators/head.html b/1-js/99-js-misc/05-bitwise-operators/head.html old mode 100755 new mode 100644 similarity index 100% rename from 99-archive/089-bitwise-operators/head.html rename to 1-js/99-js-misc/05-bitwise-operators/head.html diff --git a/1-js/99-js-misc/06-bigint/article.md b/1-js/99-js-misc/06-bigint/article.md new file mode 100644 index 0000000000..5f92b46238 --- /dev/null +++ b/1-js/99-js-misc/06-bigint/article.md @@ -0,0 +1,129 @@ +# BigInt + +[recent caniuse="bigint"] + +`BigInt` - это специальный числовой тип, который предоставляет возможность работать с целыми числами произвольной длины. + +Чтобы создать значение типа `BigInt`, необходимо добавить `n` в конец числового литерала или вызвать функцию `BigInt`, которая создаст число типа `BigInt` из переданного аргумента. Аргументом может быть число, строка и др. + +```js +const bigint = 1234567890123456789012345678901234567890n; + +const sameBigint = BigInt("1234567890123456789012345678901234567890"); + +const bigintFromNumber = BigInt(10); // то же самое, что и 10n +``` + +## Математические операторы + +`BigInt` можно использовать как обычные числа, к примеру: + +```js run +alert(1n + 2n); // 3 + +alert(5n / 2n); // 2 +``` + +Обратите внимание: операция деления `5/2` возвращает округлённый результат, без дробной части. Все операции с числами типа `bigint` возвращают `bigint`. + +В математических операциях мы не можем смешивать `bigint` и обычные числа: + +```js run +alert(1n + 2); // Error: Cannot mix BigInt and other types +``` + +Мы должны явно их конвертировать: используя либо `BigInt()`, либо `Number()`, например: + +```js run +let bigint = 1n; +let number = 2; + +// конвертируем number в bigint +alert(bigint + BigInt(number)); // 3 + +// конвертируем `bigint` в number +alert(Number(bigint) + number); // 3 +``` + +Конвертирование `bigint` в число всегда происходит неявно и без генерации ошибок, но если значение `bigint` слишком велико и не подходит под тип `number`, то дополнительные биты будут отброшены, так что следует быть осторожными с такими преобразованиями. + +````smart header="К `BigInt` числам нельзя применить унарный оператор `+`" +Унарный оператор `+value` является хорошо известным способом конвертировать произвольное значение `value` в число. + +Данный оператор не поддерживается при работе с `BigInt` числами: +```js run +let bigint = 1n; + +alert( +bigint ); // Ошибка! +``` + +Мы должны использовать `Number()` для преобразования `bigint` к `number`. +```` + +## Операции сравнения + +Операции сравнения, такие как `<`, `>`, работают с `bigint` и обычными числами как обычно: + +```js run +alert( 2n > 1n ); // true + +alert( 2n > 1 ); // true +``` + +Пожалуйста, обратите внимание, что обычные и `bigint` числа принадлежат к разным типам, они могут быть равны только при нестрогом сравнении `==`: + +```js run +alert( 1 == 1n ); // true + +alert( 1 === 1n ); // false +``` + +## Логические операции + +В `if` или любом другом логическом операторе `bigint` число ведёт себя как обычное число. + +К примеру, в `if` `bigint` `0n` преобразуется в `false`, другие значения преобразуются в `true`: + +```js run +if (0n) { + // никогда не выполнится +} +``` + +Логические операторы `||`, `&&` и другие также работают с `bigint` числами как с обычными числами: + +```js run +alert( 1n || 2 ); // 1 + +alert( 0n || 2 ); // 2 +``` + +## Полифилы + +Создание полифила для `BigInt` - достаточно непростая задача. Причина в том, что многие операторы в JavaScript, такие как `+`, `-` и др., ведут себя по-разному с `bigint` по сравнению с обычными числами. + +К примеру, деление `bigint` числа всегда возвращает `bigint` (округлённое при необходимости). + +Чтобы эмулировать такое поведение, полифил должен будет проанализировать код и заменить все такие операторы на свои вызовы. Такая реализация будет тяжеловесной, не очень хорошей с точки зрения производительности. + +Вот почему на данный момент нет хорошо реализованного полифила. + +Существует обратное решение, предложеное разработчиками библиотеки [JSBI](https://github.com/GoogleChromeLabs/jsbi). + +Эта библиотека реализует большие числа, используя собственные методы. Мы можем использовать их вместо встроенных `BigInt`: + +| Операция | Встроенный `BigInt` | JSBI | +|-----------|-----------------|------| +| Создание из `number` | `a = BigInt(789)` | `a = JSBI.BigInt(789)` | +| Сложение | `c = a + b` | `c = JSBI.add(a, b)` | +| Вычитание | `c = a - b` | `c = JSBI.subtract(a, b)` | +| ... | ... | ... | + +...А затем использовать полифил (плагин Babel) для замены вызовов JSBI на встроенные `Bigint` для браузеров, которые их поддерживают. + +Другими словами, данный подход предлагает использовать JSBI вместо встроенных `BigInt`. JSBI внутри себя работает с числами как с `BigInt`, эмулирует их с соблюдением всех требований спецификации. Таким образом, мы можем выполнять JSBI-код в интерпретаторах, которые не поддерживают `Bigint`, а для тех, которые поддерживают - полифил преобразует вызовы в обычные `Bigint`. + +## Ссылки + +- MDN: [BigInt](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/BigInt). +- Спецификация: [BigInt](https://tc39.es/ecma262/#sec-bigint-objects). diff --git a/1-js/99-js-misc/07-unicode/article.md b/1-js/99-js-misc/07-unicode/article.md new file mode 100644 index 0000000000..3ae322dc42 --- /dev/null +++ b/1-js/99-js-misc/07-unicode/article.md @@ -0,0 +1,173 @@ + +# Юникод, внутреннее устройство строк + +```warn header="Глубокое погружение в тему" +Этот раздел более подробно описывает, как устроены строки. Такие знания пригодятся, если вы намерены работать с эмодзи, редкими математическими символами, иероглифами, и т.д. +``` + +Как мы уже знаем, строки в JavaScript основаны на [Юникоде](https://ru.wikipedia.org/wiki/Юникод): каждый символ представляет из себя последовательность байтов из 1-4 байтов. + +JavaScript позволяет нам вставить символ в строку, указав его шестнадцатеричный Юникод с помощью одной из этих трех нотаций: + +- `\xXX` + + Вместо `XX` должны быть указаны две шестнадцатеричные цифры со значением от `00` до `FF`. В этом случае `\xXX` -- это символ, Юникод которого равен `XX`. + + Поскольку нотация `\xXX` поддерживает только две шестнадцатеричные цифры, ее можно использовать только для первых 256 символов Юникода. + + Эти 256 символов включают в себя латинский алфавит, большинство основных синтаксических символов и некоторые другие. Например, `"\x7A"` - это то же самое, что `"z"` (Юникод `U+007A`). + + ```js run + alert( "\x7A" ); // z + alert( "\xA9" ); // ©, символ авторского права + ``` + +- `\uXXXX` + + Вместо `XXXX` должны быть указаны ровно 4 шестнадцатеричные цифры со значением от `0000` до `FFFF`. В этом случае `\uXXXX` - это символ, Юникод которого равен `XXXX`. + + Символы со значениями Юникода, превышающими `U+FFFF`, также могут быть представлены с помощью этой нотации, но в таком случае нам придется использовать так называемую суррогатную пару (о ней мы поговорим позже в этой главе). + + ```js run + alert( "\u00A9" ); // ©, то же самое, что \xA9, используя 4-значную шестнадцатеричную нотацию + alert( "\u044F" ); // я, буква кириллического алфавита + alert( "\u2191" ); // ↑, символ стрелки вверх + ``` + +- `\u{X…XXXXXX}` + + Вместо `X…XXXXXX` должно быть шестнадцатеричное значение от 1 до 6 байт от `0` до `10FFFF` (максимальная точка кода, определенная стандартом Юникод). Эта нотация позволяет нам легко представлять все существующие символы Юникода. + + ```js run + alert( "\u{20331}" ); // 佫, редкий китайский иероглиф (длинный Юникод) + alert( "\u{1F60D}" ); // 😍, символ улыбающегося лица (ещё один длинный Юникод) + ``` + +## Суррогатные пары + +Все часто используемые символы имеют 2-байтовые коды (4 шестнадцатеричные цифры). В большинстве европейских языков буквы, цифры и основные унифицированные идеографические наборы CJK (CJK -- от китайской, японской и корейской систем письма) имеют 2-байтовое представление. + +Изначально JavaScript был основан на кодировке UTF-16, которая предусматривала только 2 байта на один символ. Однако 2 байта допускают только 65536 комбинаций, и этого недостаточно для всех возможных символов Юникода. + +Поэтому редкие символы, требующие более 2 байт, кодируются парой 2-байтовых символов, которые называются "суррогатной парой". + +Побочным эффектом является то, что длина таких символов равна `2`: + +```js run +alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X +alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY +alert( '𩷶'.length ); // 2, редкий китайский иероглиф +``` + +Это происходит потому, что суррогатные пары не существовали в то время, когда был создан JavaScript, и поэтому они не обрабатываются языком корректно. + +На самом деле в каждой из приведенных строк у нас по одному символу, но свойство `length` показывает длину `2`. + +Получить такой символ также может быть непросто, поскольку большинство языковых функций рассматривают суррогатные пары как два символа. + +Например, здесь мы видим два странных символа в выводе: + +```js run +alert( '𝒳'[0] ); // показывает странные символы... +alert( '𝒳'[1] ); // ...части суррогатной пары +``` + +Части суррогатной пары не имеют никакого значения друг без друга. + +Технически, суррогатные пары также можно определить по их кодам: если символ имеет код в интервале `0xd800...0xdbff`, то он является первой частью суррогатной пары. Следующий символ (вторая часть) должен иметь код в интервале `0xdc00...0xdfff`. Эти интервалы зарезервированы стандартом исключительно для суррогатных пар. + +Поэтому для работы с суррогатными парами в JavaScript были добавлены методы `String.fromCodePoint` и `str.codePointAt`. + +По сути, они аналогичны [String.fromCharCode](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode) и [str.charCodeAt](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt), но они правильно обрабатывают суррогатные пары. + +Здесь можно увидеть разницу: + +```js run +// charCodeAt не учитывает суррогатные пары, поэтому он выдает коды для 1-й части 𝒳: + +alert( '𝒳'.charCodeAt(0).toString(16) ); // d835 + +// codePointAt учитывает суррогатные пары +alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, считывает обе части суррогатной пары +``` + +При этом, если брать с позиции 1 (а это здесь скорее неверно), то они оба возвращают только 2-ю часть пары: + +```js run +alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3 +alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3 +// бессмысленная 2-я половина пары +``` + +Другие способы работы с суррогатными парами вы найдете в главе . Возможно, для этого тоже существуют специальные библиотеки, но они не настолько известные, чтобы предлагать их в учебнике. + +````warn header="Разделение строки в случайном месте может быть опасным!" +Разделив строку в случайном месте, например, с помощью `str.slice(0, 4)`, мы не можем гарантировать валидность полученного значения. Например: + +```js run +alert( 'hi 😂'.slice(0, 4) ); // hi [?] +``` + +Здесь мы видим мусорный символ (первая половина суррогатной пары 😂) в выводе. + +Просто имейте это в виду, если вы намерены надежно работать с суррогатными парами. Может быть, это не очень большая проблема, но, по крайней мере, вы должны понимать, что происходит. +```` + +## Диакритические знаки и нормализация + +Во многих языках есть символы, состоящие из основного символа и знака над/под ним. + +Например, буква `a` может быть основой для этих символов: `àáâäãåā`. + +Большинство распространенных "составных" символов имеют свой собственный код в таблице Юникода. Но не все, потому что существует слишком большое количество возможных комбинаций. + +Для поддержки любых комбинаций стандарт Юникод позволяет нам использовать несколько Юникодных символов: основной символ, за которым следует один или много символов-"меток", которые "украшают" его. + +Например, если за `S` следует специальный символ "точка сверху" (код `\u0307`), то он отобразится как Ṡ. + +```js run +alert( 'S\u0307' ); // Ṡ +``` + +Если нам нужен дополнительный знак над буквой (или под ней) -- нет проблем, просто добавляем соответствующий символ. + +Например, если мы добавим символ "точка снизу" (код `\u0323`), то получим "S с точками сверху и снизу": `Ṩ`. + +Вот, как это будет выглядеть: + +```js run +alert( 'S\u0307\u0323' ); // Ṩ +``` + +Это обеспечивает большую гибкость, но при этом возникает определенная проблема: два символа могут визуально выглядеть одинаково, но при этом они будут представлены разными комбинациями Юникода. + +Например: + +```js run +let s1 = 'S\u0307\u0323'; // Ṩ, S + точка сверху + точка снизу +let s2 = 'S\u0323\u0307'; // Ṩ, S + точка снизу + точка сверху + +alert( `s1: ${s1}, s2: ${s2}` ); + +alert( s1 == s2 ); // false, хотя символы выглядят одинаково (?!) +``` + +Для решения этой проблемы предусмотрен алгоритм "Юникодной нормализации", приводящий каждую строку к единому "нормальному" виду. + +Его реализует метод [str.normalize()](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/normalize). + +```js run +alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true +``` + +Забавно, но в нашем случае `normalize()` "схлопывает" последовательность из трёх символов в один: `\u1e68` — S с двумя точками. + +```js run +alert( "S\u0307\u0323".normalize().length ); // 1 + +alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true +``` + +В действительности это не всегда так. Причина в том, что символ `Ṩ` является "достаточно распространенным", поэтому создатели стандарта Юникод включили его в основную таблицу и присвоили ему код. + +Если вы хотите узнать больше о правилах и вариантах нормализации -- они описаны в дополнении к стандарту Юникод: [Unicode Normalization Forms](https://www.unicode.org/reports/tr15/), но для большинства практических целей достаточно информации из этого раздела. diff --git a/1-js/99-js-misc/90-intl/1-collate-array-sort/solution.md b/1-js/99-js-misc/08-intl/1-collate-array-sort/solution.md similarity index 89% rename from 1-js/99-js-misc/90-intl/1-collate-array-sort/solution.md rename to 1-js/99-js-misc/08-intl/1-collate-array-sort/solution.md index 41851d30e9..0594dd8c4a 100755 --- a/1-js/99-js-misc/90-intl/1-collate-array-sort/solution.md +++ b/1-js/99-js-misc/08-intl/1-collate-array-sort/solution.md @@ -5,9 +5,7 @@ let animals = ["тигр", "ёж", "енот", "ехидна", "АИСТ", "ЯК *!* let collator = new Intl.Collator(); -animals.sort(function(a, b) { - return collator.compare(a, b); -}); +animals.sort((a, b) => collator.compare(a, b)); */!* alert( animals ); // АИСТ,ёж,енот,ехидна,тигр,ЯК diff --git a/1-js/99-js-misc/90-intl/1-collate-array-sort/task.md b/1-js/99-js-misc/08-intl/1-collate-array-sort/task.md similarity index 59% rename from 1-js/99-js-misc/90-intl/1-collate-array-sort/task.md rename to 1-js/99-js-misc/08-intl/1-collate-array-sort/task.md index a0eb4a63b2..1259405529 100755 --- a/1-js/99-js-misc/90-intl/1-collate-array-sort/task.md +++ b/1-js/99-js-misc/08-intl/1-collate-array-sort/task.md @@ -16,4 +16,4 @@ alert( animals ); // АИСТ,ёж,енот,ехидна,тигр,ЯК В этом примере порядок сортировки не должен зависеть от регистра. -Что касается буквы `"ё"`, то мы следуем [обычным правилам сортировки буквы ё](http://ru.wikipedia.org/wiki/%D0%81#.D0.A1.D0.BE.D1.80.D1.82.D0.B8.D1.80.D0.BE.D0.B2.D0.BA.D0.B0), по которым "е" и "ё" считаются одной и той же буквой, за исключением случая, когда два слова отличаются только в позиции буквы "е" / "ё" -- тогда слово с "е" ставится первым. +Что касается буквы `"ё"`, то мы следуем [обычным правилам сортировки буквы ё](https://ru.wikipedia.org/wiki/%D0%81#.D0.A1.D0.BE.D1.80.D1.82.D0.B8.D1.80.D0.BE.D0.B2.D0.BA.D0.B0), по которым "е" и "ё" считаются одной и той же буквой, за исключением случая, когда два слова отличаются только в позиции буквы "е" / "ё" -- тогда слово с "е" ставится первым. diff --git a/1-js/99-js-misc/90-intl/article.md b/1-js/99-js-misc/08-intl/article.md similarity index 86% rename from 1-js/99-js-misc/90-intl/article.md rename to 1-js/99-js-misc/08-intl/article.md index 004e0812b4..b4bb313487 100644 --- a/1-js/99-js-misc/90-intl/article.md +++ b/1-js/99-js-misc/08-intl/article.md @@ -13,7 +13,7 @@ Числа : В одних странах выводятся цифрами, в других -- иероглифами, длинные числа разделяются где-то пробелом, где-то запятой. -Все современные браузеры, кроме IE10- (но есть библиотеки и для него) поддерживают стандарт [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), предназначенный решить эти проблемы навсегда. +Все современные браузеры, кроме IE10 (но есть библиотеки и для него) поддерживают стандарт [ECMA 402](https://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf), предназначенный решить эти проблемы навсегда. ## Основные объекты @@ -43,9 +43,9 @@ 3. `en-US` -- английский язык, используемый в США (`US`). 4. `zh-Hans-CN` -- китайский язык (`zh`), записываемый упрощённой иероглифической письменностью (`Hans`), используемый в Китае. -Также через суффикс `-u-*` можно указать расширения локалей, например `"th-TH-u-nu-thai"` -- тайский язык (`th`), используемый в Тайланде (`TH`), с записью чисел тайскими буквами (๐, ๑, ๒, ๓, ๔, ๕, ๖, ๗, ๘, ๙) . +Также через суффикс `-u-*` можно указать расширения локалей, например `"th-TH-u-nu-thai"` -- тайский язык (`th`), используемый в Таиланде (`TH`), с записью чисел тайскими буквами (๐, ๑, ๒, ๓, ๔, ๕, ๖, ๗, ๘, ๙) . -Стандарт, который описывает локали -- [RFC 5464](http://tools.ietf.org/html/rfc5646), языки описаны в [IANA language registry](http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). +Стандарт, который описывает локали -- [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646), языки описаны в [IANA language registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). Все методы принимают локаль в виде строки или массива, содержащего несколько локалей в порядке предпочтения. @@ -72,7 +72,7 @@ ```js // создание -var collator = new Intl.Collator([locales, [options]]) +let collator = new Intl.Collator([locales, [options]]) ``` Параметры: @@ -93,14 +93,14 @@ var collator = new Intl.Collator([locales, [options]]) - `ignorePunctuation` -- игнорировать знаки пунктуации: `true/false`, по умолчанию `false`. - `numeric` -- использовать ли численное сравнение: `true/false`, если `true`, то будет `12 > 2`, иначе `12 < 2`. - - `caseFirst` -- в сортировке должны идти первыми прописные или строчные буквы, варианты: `"upper"` (прописные), `lower` (строчные) или `false` (стандартное для локали, также является значением по умолчанию). Не поддерживается IE11-. + - `caseFirst` -- в сортировке должны идти первыми прописные или строчные буквы, варианты: `"upper"` (прописные), `"lower"` (строчные) или `"false"` (стандартное для локали, также является значением по умолчанию). Не поддерживается IE11. В подавляющем большинстве случаев подходят стандартные параметры, то есть `options` указывать не нужно. Использование: ```js -var result = collator.compare(str1, str2); +let result = collator.compare(str1, str2); ``` Результат `compare` имеет значение `1` (больше), `0` (равно) или `-1` (меньше). @@ -108,7 +108,7 @@ var result = collator.compare(str1, str2); Например: ```js run -var collator = new Intl.Collator(); +let collator = new Intl.Collator(); alert( "ёжик" > "яблоко" ); // true (ёжик больше, что неверно) alert( collator.compare("ёжик", "яблоко") ); // -1 (ёжик меньше, верно) @@ -117,13 +117,13 @@ alert( collator.compare("ёжик", "яблоко") ); // -1 (ёжик мень Выше были использованы полностью стандартные настройки. Они различают регистр символа, но это различие можно убрать, если настроить чувствительность `sensitivity`: ```js run -var collator = new Intl.Collator(); -alert( collator.compare("ЁжиК", "ёжик") ); // 1, разные +let collator1 = new Intl.Collator(); +alert( collator1.compare("ЁжиК", "ёжик") ); // 1, разные -var collator = new Intl.Collator(undefined, { +let collator2 = new Intl.Collator(undefined, { sensitivity: "accent" }); -alert( collator.compare("ЁжиК", "ёжик") ); // 0, одинаковые +alert( collator2.compare("ЁжиК", "ёжик") ); // 0, одинаковые ``` ## Даты, Intl.DateTimeFormat [#intl-datetimeformat] @@ -132,7 +132,7 @@ alert( collator.compare("ЁжиК", "ёжик") ); // 0, одинаковые ```js // создание -var formatter = new Intl.DateTimeFormat([locales, [options]]) +let formatter = new Intl.DateTimeFormat([locales, [options]]) ``` Первый аргумент -- такой же, как и в `Collator`, а в объекте `options` мы можем определить, какие именно части даты показывать (часы, месяц, год...) и в каком формате. @@ -152,10 +152,10 @@ var formatter = new Intl.DateTimeFormat([locales, [options]]) localeMatcher Алгоритм подбора локали - lookup,best fit + lookup, best fit - best fit + best fit @@ -221,8 +221,7 @@ var formatter = new Intl.DateTimeFormat([locales, [options]]) - second - + second Секунды 2-digit, numeric @@ -245,32 +244,32 @@ var formatter = new Intl.DateTimeFormat([locales, [options]]) - month, day - hour, minute, second -Если указанный формат не поддерживается, то настройка `formatMatcher` задаёт алгоритм подбора наиболее близкого формата: `basic` -- по [стандартным правилам](http://www.ecma-international.org/ecma-402/1.0/#BasicFormatMatcher) и `best fit` -- по умолчанию, на усмотрение окружения (браузера). +Если указанный формат не поддерживается, то настройка `formatMatcher` задаёт алгоритм подбора наиболее близкого формата: `basic` -- по [стандартным правилам](https://www.ecma-international.org/ecma-402/1.0/#BasicFormatMatcher) и `best fit` -- по умолчанию, на усмотрение окружения (браузера). Использование: ```js -var dateString = formatter.format(date); +let dateString = formatter.format(date); ``` Например: ```js run -var date = new Date(2014, 11, 31, 12, 30, 0); +let date = new Date(2014, 11, 31, 12, 30, 0); -var formatter = new Intl.DateTimeFormat("ru"); -alert( formatter.format(date) ); // 31.12.2014 +let formatter1 = new Intl.DateTimeFormat("ru"); +alert( formatter1.format(date) ); // 31.12.2014 -var formatter = new Intl.DateTimeFormat("en-US"); -alert( formatter.format(date) ); // 12/31/2014 +let formatter2 = new Intl.DateTimeFormat("en-US"); +alert( formatter2.format(date) ); // 12/31/2014 ``` Длинная дата, с настройками: ```js run -var date = new Date(2014, 11, 31, 12, 30, 0); +let date = new Date(2014, 11, 31, 12, 30, 0); -var formatter = new Intl.DateTimeFormat("ru", { +let formatter = new Intl.DateTimeFormat("ru", { weekday: "long", year: "numeric", month: "long", @@ -283,9 +282,9 @@ alert( formatter.format(date) ); // среда, 31 декабря 2014 г. Только время: ```js run -var date = new Date(2014, 11, 31, 12, 30, 0); +let date = new Date(2014, 11, 31, 12, 30, 0); -var formatter = new Intl.DateTimeFormat("ru", { +let formatter = new Intl.DateTimeFormat("ru", { hour: "numeric", minute: "numeric", second: "numeric" @@ -294,14 +293,14 @@ var formatter = new Intl.DateTimeFormat("ru", { alert( formatter.format(date) ); // 12:30:00 ``` -## Числа: Intl.NumberFormat +## Числа, Intl.NumberFormat Форматтер `Intl.NumberFormat` умеет красиво форматировать не только числа, но и валюту, а также проценты. Синтаксис: ```js -var formatter = new Intl.NumberFormat([locales[, options]]); +let formatter = new Intl.NumberFormat([locales[, options]]); formatter.format(number); // форматирование ``` @@ -332,7 +331,7 @@ formatter.format(number); // форматирование currency Алфавитный код валюты - См. [Список кодов валюты](http://www.currency-iso.org/en/home/tables/table-a1.html), например USD + См. Список кодов валюты, например USD @@ -351,7 +350,7 @@ formatter.format(number); // форматирование minimumIntegerDigits Минимальное количество цифр целой части - от `1` до `21` + от 1 до 21 21 @@ -378,21 +377,21 @@ formatter.format(number); // форматирование maximumSignificantDigits Максимальное количество значимых цифр от minimumSignificantDigits до 21 - minimumSignificantDigits + 21 Пример без опций: ```js run -var formatter = new Intl.NumberFormat("ru"); +let formatter = new Intl.NumberFormat("ru"); alert( formatter.format(1234567890.123) ); // 1 234 567 890,123 ``` С ограничением значимых цифр (важны только первые 3): ```js run -var formatter = new Intl.NumberFormat("ru", { +let formatter = new Intl.NumberFormat("ru", { maximumSignificantDigits: 3 }); alert( formatter.format(1234567890.123) ); // 1 230 000 000 @@ -400,8 +399,8 @@ alert( formatter.format(1234567890.123) ); // 1 230 000 000 С опциями для валюты: -```js -var formatter = new Intl.NumberFormat("ru", { +```js run +let formatter = new Intl.NumberFormat("ru", { style: "currency", currency: "GBP" }); @@ -411,8 +410,8 @@ alert( formatter.format(1234.5) ); // 1 234,5 £ С двумя цифрами после запятой: -```js -var formatter = new Intl.NumberFormat("ru", { +```js run +let formatter = new Intl.NumberFormat("ru", { style: "currency", currency: "GBP", minimumFractionDigits: 2 @@ -429,7 +428,7 @@ alert( formatter.format(1234.5) ); // 1 234,50 £ : Сравнивает строку с другой, с учётом локали, например: ```js run - var str = "ёжик"; + let str = "ёжик"; alert( str.localeCompare("яблоко", "ru") ); // -1 ``` @@ -438,7 +437,7 @@ alert( formatter.format(1234.5) ); // 1 234,50 £ : Форматирует дату в соответствии с локалью, например: ```js run no-beautify - var date = new Date(2014, 11, 31, 12, 00); + let date = new Date(2014, 11, 31, 12, 0); alert( date.toLocaleString("ru", { year: 'numeric', month: 'long' }) ); // Декабрь 2014 ``` @@ -456,4 +455,4 @@ alert( formatter.format(1234.5) ); // 1 234,50 £ ## Старые IE -В IE10- рекомендуется использовать полифил, например библиотеку . +В IE10 рекомендуется использовать полифил, например библиотеку . diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/article.md b/1-js/99-js-misc/09-weakref-finalizationregistry/article.md new file mode 100644 index 0000000000..6a7d9ba40f --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/article.md @@ -0,0 +1,431 @@ + +# WeakRef и FinalizationRegistry + +```warn header="\"Скрытые\" возможности языка" +В этой статье рассматривается очень узконаправленная тема, с которой большинство разработчиков на практике сталкиваются чрезвычайно редко (а могут и вообще не знать о её существовании). + +Мы рекомендуем пропустить эту главу, если вы только начали изучение JavaScript. +``` + +Вспоминая основную концепцию *принципа достижимости* из главы , мы можем отметить, что движок JavaScript гарантированно хранит в памяти значения, которые доступны или используются. + +Например: + +```js +// в переменной user находится сильная ссылка на объект +let user = { name: "John" }; + +// перезапишем значение переменной user +user = null; + +// ссылка теряется и объект будет удалён из памяти +``` + +Или же похожий, но немного усложнённый код с двумя сильными ссылками: + +```js +// в переменной user находится сильная ссылка на объект +let user = { name: "John" }; + +// скопировали сильную ссылку на объект в переменную admin +*!* +let admin = user; +*/!* + +// перезапишем значение переменной user +user = null; + +// объект всё ещё доступен через переменную admin +``` +Объект `{ name: "John" }` удалился бы из памяти только в случае отсутствия сильных ссылок на него (если бы мы также перезаписали значение переменной `admin`). + +В JavaScript существует концепция под названием `WeakRef`, которая ведёт себя немного иначе в этом случае. + +````smart header="Термины: \"Сильная ссылка\", \"Слабая ссылка\"" +**Сильная ссылка** - это ссылка на объект или значение, которая предотвращает их удаление сборщиком мусора. При этом, сохраняя объект или значение в памяти, на которые она указывает. + +Это означает, что объект или значение остаются в памяти и не удаляются сборщиком мусора до тех пор, пока на них есть активные сильные ссылки. + +В JavaScript стандартные ссылки на объекты являются *сильными* ссылками. Например: +```js +// переменная user содержит сильную ссылку на этот объект. +let user = { name: "John" }; +``` + +**Слабая ссылка** - это ссылка на объект или значение, которая *не* предотвращает их удаление сборщиком мусора. Объект или значение могут быть удалены сборщиком мусора в случае, если на них существуют только слабые ссылки. + +```` + +## WeakRef + +````warn header="Предостережение" + +Прежде чем мы перейдём к изучению, стоит отметить, что правильное применение структур, о которых пойдёт речь в этой статье, требует очень тщательного обдумывания, и по возможности их использования лучше избегать. + +```` + +`WeakRef` - это объект, содержащий слабую ссылку на другой объект, называемый `target` или `referent`. + +Особенность `WeakRef` заключается в том, что он не препятствует сборщику мусора удалять свой объект-референт. Другими словами, он просто не удерживает его "в живых". + +Теперь давайте возьмём переменную `user` в качестве "референта" и создадим слабую ссылку от неё к переменной `admin`. Чтобы создать слабую ссылку, необходимо использовать конструктор `WeakRef`, передав целевой объект (объект, на который вы хотите создать слабую ссылку). + +В нашем случае — это переменная `user`: + +```js +// в переменной user находится сильная ссылка на объект +let user = { name: "John" }; + +// в переменной admin находится слабая ссылка на объект +*!* +let admin = new WeakRef(user); +*/!* +``` + +На схеме ниже изображены два типа ссылок: сильная ссылка с использованием переменной `user` и слабая ссылка с использованием переменной `admin`: + +![](weakref-finalizationregistry-01.svg) + +Затем, в какой-то момент, мы перестаём использовать переменную `user` - она перезаписывается, выходит из области видимости и т.д., при этом сохраняя экземпляр `WeakRef` в переменной `admin`: + +```js +// перезапишем значение переменной user +user = null; +``` + +Слабой ссылки на объект недостаточно, чтобы сохранить его "в живых". Когда единственными оставшимися ссылками на объект-референт являются слабые ссылки, сборщик мусора вправе уничтожить этот объект и использовать его память для чего-то другого. + +Однако до тех пор, пока объект фактически не уничтожен, слабая ссылка может вернуть его, даже если на данный объект больше нет сильных ссылок. То есть наш объект становится своеобразным "[котом Шрёдингера](https://ru.wikipedia.org/wiki/Кот_Шрёдингера)" - мы не можем знать точно, "жив" он или "мёртв": + +![](weakref-finalizationregistry-02.svg) + +На этом этапе, чтобы получить объект из экземпляра `WeakRef`, мы воспользуемся его методом `deref()`. + +Метод `deref()` возвращает объект-референт, на который ссылается `WeakRef`, в случае, если объект всё ещё находится в памяти. Если объект был удалён сборщиком мусора, - метод `deref()` вернёт `undefined`: + +```js +let ref = admin.deref(); + +if (ref) { + // объект всё ещё доступен: можем произвести какие-либо манипуляции с ним +} else { + // объект был удалён сборщиком мусора +} +``` + +## Варианты использования WeakRef + +`WeakRef` обычно используется для создания кешей или +[ассоциативных массивов](https://ru.wikipedia.org/wiki/Ассоциативный_массив), в которых хранятся ресурсоёмкие объекты. Это позволяет избежать предотвращения удаления этих объектов сборщиком мусора только на основе их присутствия в кеше или ассоциативном массиве. + +Один из основных примеров - это ситуация, когда у нас есть большое количество объектов бинарных изображений (например, представленных в виде `ArrayBuffer` или `Blob`), +и мы хотим связать имя или путь с каждым изображением. Существующие структуры данных не совсем подходят для этих целей: + +- Использование `Map` для создания ассоциаций между именами и изображениями, или наоборот, сохранит объекты изображений в памяти, + поскольку они фигурируют в `Map` в качестве ключей или значений. +- `WeakMap` также не подойдёт в этом случае: из-за того, что объекты, представленные в качестве ключей `WeakMap` используют слабые ссылки, и не защищены от удаления сборщиком мусора. + +Но, в данной ситуации нам нужна структура данных, которая бы использовала слабые ссылки в своих значениях. + +Для этого мы можем использовать коллекцию `Map`, значениями которой являются экземпляры `WeakRef`, ссылающиеся на нужные нам большие объекты. Следовательно, мы не будем хранить в памяти эти большие и ненужные объекты дольше, чем требуется. + +В противном случае это способ получить объект изображения из кеша, если он всё ещё доступен. +Если же он был удалён сборщиком мусора, мы сгенерируем или скачаем его заново. + +Таким образом, в некоторых ситуациях используется меньше памяти. + +## Пример №1: применение WeakRef для кеширования + +Ниже находится фрагмент кода, который демонстрирует технику использования `WeakRef`. + +Говоря кратко, мы используем `Map` со строковыми ключами и объектами `WeakRef` в качестве их значений. Если объект `WeakRef` не был удалён сборщиком мусора, мы берём его из кеша. В противном случае мы скачиваем его заново и помещаем в кеш для возможности повторного использования в будущем: + +```js +function fetchImg() { + // абстрактная функция для загрузки изображений... +} + +function weakRefCache(fetchImg) { // (1) + const imgCache = new Map(); // (2) + + return (imgName) => { // (3) + const cachedImg = imgCache.get(imgName); // (4) + + if (cachedImg?.deref()) { // (5) + return cachedImg?.deref(); + } + + const newImg = fetchImg(imgName); // (6) + imgCache.set(imgName, new WeakRef(newImg)); // (7) + + return newImg; + }; +} + +const getCachedImg = weakRefCache(fetchImg); +``` + +Давайте подробно разберём всё, что тут произошло: + +1. `weakRefCache` - функция высшего порядка, которая принимает другую функцию `fetchImg` в качестве аргумента. В данном примере мы можем пренебречь подробным описанием функции `fetchImg`, так как это может быть абсолютно любая логика скачивания изображений. +2. `imgCache` - кеш изображений, который хранит кешированные результаты функции `fetchImg`, в виде строковых ключей (имя изображения) и объектов `WeakRef` в качестве их значений. +3. Возвращаем анонимную функцию, которая принимает имя изображения в качестве аргумента. Данный аргумент будет использоваться в качестве ключа для кешированного изображения. +4. Пытаемся получить кешированный результат из кеша, используя предоставленный ключ (имя изображения). +5. Если кеш содержит значение по указанному ключу, и объект `WeakRef` не был удалён сборщиком мусора, возвращаем кешированный результат. +6. Если в кеше нет записи с запрошенным ключом, либо метод `deref()` возвращает `undefined` (что означает, что объект `WeakRef` был удалён сборщиком мусора), функция `fetchImg` скачивает изображение заново. +7. Помещаем скачанное изображение в кеш в виде `WeakRef` объекта. + + +Теперь у нас есть коллекция `Map`, в которой ключи - это имена изображений в виде строк, а значения - это объекты `WeakRef`, содержащие сами изображения. + +Эта техника помогает избежать выделения большого объёма памяти на ресурсоёмкие объекты, которые больше никто не использует. Также она экономит память и время в случае повторного использования кешированных объектов. + +Вот визуальное представление того, как выглядит этот код: + +![](weakref-finalizationregistry-03.svg) + +Но, у данной реализации есть свои недостатки: со временем `Map` будет заполняться строками в качестве ключей, которые указывают на `WeakRef`, чей объект-референт уже был удалён сборщиком мусора: + +![](weakref-finalizationregistry-04.svg) + +Один из способов справиться с этой проблемой - это периодически проверять кеш и удалять "мёртвые" записи. Другой способ - использовать финализаторы, с которыми мы ознакомимся далее. + +## Пример №2: применение WeakRef для отслеживания объектов DOM + +Ещё один вариант использования `WeakRef` – отслеживание объектов DOM. + +Давайте представим ситуацию, когда какой-либо сторонний код или библиотека работают с элементами на нашей странице до тех пор, пока они существуют в DOM. Например, это может быть сторонняя утилита для мониторинга и оповещений о состоянии системы (так называемый "логгер" - программа, которая присылает информационные сообщения, называемые "логами"). + +Интерактивный пример: + +[codetabs height=420 src="weakref-dom"] + +При нажатии на кнопку "Начать отправку сообщений", в так называемом "окне отображения логов" (элемент с классом `.window__body`) начинают появляться надписи (логи). + +Но, как только этот элемент удалится из DOM, логгер должен перестать присылать сообщения. Чтобы воспроизвести удаление данного элемента, достаточно лишь нажать на кнопку "Закрыть" в правом верхнем углу. + +Для того, чтобы нам не усложнять работу, и не уведомлять сторонний код каждый раз, когда наш DOM-элемент доступен, а когда - нет, достаточно будет создать на него слабую ссылку с помощью `WeakRef`. + +После того как элемент будет удалён из DOM, логгер это увидит и перестанет присылать сообщения. + +Теперь давайте рассмотрим исходный код детальнее (вкладка `index.js`): + +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, например, для этого нужно открыть инструменты разработчика (`key:Ctrl` + `key:Shift` + `key:J` на Windows/Linux или `key:Option` + `key:⌘` + `key:J` на macOS), перейти во вкладку "Производительность (Performance)" и нажать на кнопку с иконкой урны - "Собрать мусор (Collect garbage)": + + ![](google-chrome-developer-tools.png) + + Данный функционал поддерживается в большинстве современных браузеров. После проделанных действий alert сработает незамедлительно. + +## FinalizationRegistry + +А теперь пришло время поговорить о финализаторах. Прежде чем мы перейдём дальше, давайте разберёмся с терминологией: + +**Колбэк очистки (финализатор)** - это функция, которая выполняется в случае, если объект, зарегистрированный в `FinalizationRegistry`, удаляется из памяти сборщиком мусора. + +Его цель - предоставить возможность выполнения дополнительных операций, связанных с объектом, после его окончательного удаления из памяти. + +**Реестр** (или `FinalizationRegistry`) - это специальный объект в JavaScript, который управляет регистрацией и отменой регистрации объектов и их колбэков очистки. + +Этот механизм позволяет зарегистрировать объект для отслеживания и связать с ним колбэк очистки. По сути, это структура, которая хранит информацию о зарегистрированных объектах и их колбэках очистки, а затем автоматически вызывает эти колбэки при удалении объектов из памяти. + +Для создания экземпляра реестра `FinalizationRegistry`, необходимо вызвать его конструктор, +который принимает единственный аргумент - колбэк очистки (финализатор). + +Синтаксис: + +```js +function cleanupCallback(heldValue) { + // код колбэка очистки +} + +const registry = new FinalizationRegistry(cleanupCallback); +``` + +Здесь: + +- `cleanupCallback` - колбэк очистки, который будет автоматически вызван при удалении зарегистрированного объекта из памяти. +- `heldValue` - значение, которое передаётся в качестве аргумента для колбэка очистки. Если `heldValue` является объектом, реестр сохраняет на него сильную ссылку. +- `registry` - экземпляр `FinalizationRegistry`. + +Методы `FinalizationRegistry`: + +- `register(target, heldValue [, unregisterToken])` - используется для регистрации объектов в реестре. + + `target` - регистрируемый для отслеживания объект. Если `target` будет удалён сборщиком мусора, колбэк очистки будет вызван с `heldValue` в качестве аргумента. + + Опциональный `unregisterToken` - токен отмены регистрации. Может быть передан для отмены регистрации до удаления объекта сборщиком мусора. Обычно в качестве `unregisterToken` используется объект `target`, что является стандартной практикой. +- `unregister(unregisterToken)` - метод `unregister` используется для отмены регистрации объекта в реестре. Он принимает один аргумент - `unregisterToken` (токен отмены регистрации, который был получен при регистрации объекта). + +Теперь перейдём к простому примеру. Воспользуемся уже известным нам объектом `user` и создадим экземпляр `FinalizationRegistry`: + +```js +let user = { name: "John" }; + +const registry = new FinalizationRegistry((heldValue) => { + console.log(`${heldValue} был собран сборщиком мусора.`); +}); +``` + +Затем зарегистрируем объект, для которого требуется колбэк очистки, вызвав метод `register`: + +```js +registry.register(user, user.name); +``` + +Реестр не хранит сильную ссылку на регистрируемый объект, так как это бы противоречило его предназначению. Если бы реестр сохранял сильную ссылку, то объект никогда бы не был очищен сборщиком мусора. + +Если же объект удаляется сборщиком мусора, наш колбэк очистки может быть вызван в какой-то момент в будущем, с переданным ему `heldValue`: + +```js +// Когда объект user удалится сборщиком мусора, в консоль будет выведено сообщение: +"John был собран сборщиком мусора." +``` + +Также существуют ситуации, когда даже в реализациях, где используется колбэк очистки, есть вероятность, что он не будет вызван. + +Например: +- Когда программа полностью завершает свою работу (например, при закрытии вкладки в браузере). +- Когда сам экземпляр `FinalizationRegistry` больше не доступен для JavaScript кода. + Если объект, создающий экземпляр `FinalizationRegistry`, выходит из области видимости или удаляется, то колбэки очистки, зарегистрированные в этом реестре, также могут быть не вызваны. + +## Кеширование с FinalizationRegistry + +Возвращаясь к нашему примеру *слабого* кеша, мы можем заметить следующее: +- Несмотря на то, что значения, обёрнутые в `WeakRef`, были собраны сборщиком мусора, всё ещё актуальна проблема "утечки памяти" в виде оставшихся ключей, значения которых были собраны сборщиком мусора. + +Вот улучшенный пример кеширования, в котором используется `FinalizationRegistry`: + +```js +function fetchImg() { + // абстрактная функция для загрузки изображений... +} + +function weakRefCache(fetchImg) { + const imgCache = new Map(); + + *!* + const registry = new FinalizationRegistry((imgName) => { // (1) + const cachedImg = imgCache.get(imgName); + if (cachedImg && !cachedImg.deref()) imgCache.delete(imgName); + }); + */!* + + return (imgName) => { + const cachedImg = imgCache.get(imgName); + + if (cachedImg?.deref()) { + return cachedImg?.deref(); + } + + const newImg = fetchImg(imgName); + imgCache.set(imgName, new WeakRef(newImg)); + *!* + registry.register(newImg, imgName); // (2) + */!* + + return newImg; + }; +} + +const getCachedImg = weakRefCache(fetchImg); +``` + +1. Для управления очисткой "мёртвых" записей в кеше, когда связанные с ними объекты `WeakRef` собираются сборщиком мусора, создаём реестр очистки `FinalizationRegistry`. + + Важным моментом здесь является то, что в колбэке очистки должно проверяться, была ли запись удалена сборщиком мусора и не была ли добавлена заново, чтобы не удалить "живую" запись. +2. После загрузки и установки нового значения (изображения) в кеш, регистрируем его в реестре финализатора для отслеживания объекта `WeakRef`. + +Данная реализация содержит только актуальные или "живые" пары ключ/значение. +В этом случае каждый объект `WeakRef` зарегистрирован в `FinalizationRegistry`. +А после того, как объекты будут очищены сборщиком мусора, колбэк очистки удалит все значения `undefined`. + +Вот визуальное представление обновлённого кода: + +![](weakref-finalizationregistry-05.svg) + +Ключевым аспектом в обновлённой реализации является то, что финализаторы позволяют создавать параллельные процессы между "основной" программой и колбэками очистки. В контексте JavaScript, "основная" программа - это наш JavaScript-код, который запускается и выполняется в нашем приложении или на веб-странице. + +Следовательно, с момента, когда объект помечается для удаления сборщиком мусора, до фактического выполнения колбэка очистки, может возникнуть определённый промежуток времени. Важно понимать, что в этом временном интервале основная программа может внести любые изменения в объект или даже вернуть его обратно в память. + +Поэтому, в колбэке очистки мы должны проверить, не была ли запись добавлена обратно в кеш основной программой, чтобы избежать удаления "живых" записей. Аналогично, при поиске ключа в кеше существует вероятность того, что значение было удалено сборщиком мусора, но колбэк очистки ещё не был выполнен. + +Такие ситуации требуют особого внимания, если вы работаете с `FinalizationRegistry`. + +## Использование WeakRef и FinalizationRegistry на практике + +Переходя от теории к практике, представьте себе реальный сценарий, когда пользователь синхронизирует свои фотографии на мобильном устройстве +с каким-либо облачным сервисом (таким как [iCloud](https://ru.wikipedia.org/wiki/ICloud) или [Google Photos](https://ru.wikipedia.org/wiki/Google_Фото)), и хочет просматривать их с других устройств. Подобные сервисы помимо основного функционала просмотра фотографий, предлагают массу дополнительных возможностей, например: + +- Редактирование фотографий и видео эффекты. +- Создание "воспоминаний" и альбомов. +- Монтаж видео из серии фотографий. +- ...и многое другое. + +В качестве примера здесь мы будем использовать достаточно примитивную реализацию подобного сервиса. Основная суть — показать возможный сценарий совместного использования `WeakRef` и `FinalizationRegistry` в реальной жизни. + +Вот как это выглядит: + +![](weakref_-finalizationregistry-demo-01.png) + +В левой части находится облачная библиотека фотографий (они отображаются в виде миниатюр). Мы можем выбрать нужные нам изображения и создать коллаж, нажав на кнопку "Create collage" в правой части страницы. Затем, получившийся результат можно будет скачать в виде изображения. + +Для увеличения скорости загрузки страницы разумно будет загружать и показывать миниатюры фотографий именно в *сжатом* качестве. Но, для создания коллажа из выбранных фотографий, загружать и использовать их в *полноразмерном* качестве. + +Ниже мы видим, что внутренний размер миниатюр составляет 240×240 пикселей. Размер был выбран специально для увеличения скорости загрузки. Кроме того, нам не нужны полноразмерные фотографии в режиме предпросмотра. + +![](weakref-finalizationregistry-demo-02.png) + +Предположим, что нам нужно создать коллаж из 4 фотографий: мы выбираем их, после чего нажимаем кнопку "Create collage". +На этом этапе уже известная нам функция weakRefCache проверяет, есть ли нужное изображение в кеше. Если нет, то скачивает его из облака и помещает в кеш для возможности дальнейшего использования. И так происходит для каждого выбранного изображения: + +![](weakref-finalizationregistry-demo-03.gif) + +Обратив внимание на вывод в консоли можно увидеть, какие из фотографий были загружены из облака - на это указывает FETCHED_IMAGE. Так как это первая попытка создания коллажа, это означает, что на данном этапе "слабый кеш" ещё был пуст, а все фотографии были скачаны из облака и помещены в него. + +Но, наряду с процессом загрузки изображений, происходит ещё и процесс очистки памяти сборщиком мусора. Это означает, что хранящийся в кеше объект, на который мы ссылаемся используя слабую ссылку, удаляется сборщиком мусора. И наш финализатор выполняется успешно, тем самым удаляя ключ, по которому изображение хранилось в кеше. Об этом нас уведомляет CLEANED_IMAGE: + +![](weakref-finalizationregistry-demo-04.jpg) + +Далее мы понимаем, что нам не нравится получившийся коллаж, и решаем изменить одно из изображений и создать новый. Для этого достаточно снять выделение с ненужного изображения, выбрать другое, и ещё раз нажать на кнопку "Create collage": + +![](weakref-finalizationregistry-demo-05.gif) + +Но, на этот раз не все изображения были скачаны из сети, и одно из них было взято из слабого кеша: об этом нам говорит сообщение CACHED_IMAGE. Это означает, что на момент создания коллажа сборщик мусора ещё не удалил наше изображение, и мы смело взяли его из кеша, +тем самым сократив количество сетевых запросов и ускорив общее время процесса создания коллажа: + +![](weakref-finalizationregistry-demo-06.jpg) + +Давайте ещё немного "поиграем", заменив одно из изображений ещё раз и создав новый коллаж: + +![](weakref-finalizationregistry-demo-07.gif) + +На этот раз результат ещё более внушительный. Из 4 выбранных изображений, 3 из них были взяты из слабого кеша, и только одно пришлось скачать из сети. +Снижение нагрузки на сеть составило около 75%. Впечатляет, не правда ли? + +![](weakref-finalizationregistry-demo-08.jpg) + +Конечно, не следует забывать, что такое поведение не является гарантированным, и зависит от конкретной реализации и работы сборщика мусора. + +Исходя из этого, сразу же возникает вполне логичный вопрос: почему бы нам не использовать обычный кеш, где мы можем сами управлять его сущностями, а не полагаться на сборщик мусора? Всё верно, в большинстве случаев нет необходимости использовать `WeakRef` и `FinalizationRegistry`. + +Здесь мы просто продемонстрировали альтернативную реализацию подобного функционала, используя нетривиальный подход с интересными особенностями языка. Всё же, мы не можем полагаться на этот пример, если нам необходим постоянный и предсказуемый результат. + +Вы можете [открыть данный пример в песочнице](sandbox:weakref-finalizationregistry). + +## Итого + +`WeakRef` - предназначен для создания слабых ссылок на объекты, что позволяет им быть удалёнными из памяти сборщиком мусора, если на них больше нет сильных ссылок. Это полезно для решения проблемы чрезмерного использования памяти и оптимизации использования системных ресурсов в приложениях. + +`FinalizationRegistry` - это средство регистрации колбэков, которые выполняются при уничтожении объектов, на которые больше нет сильных ссылок. Это позволяет освобождать связанные с объектом ресурсы или выполнять другие необходимые операции перед удалением объекта из памяти. diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/google-chrome-developer-tools.png b/1-js/99-js-misc/09-weakref-finalizationregistry/google-chrome-developer-tools.png new file mode 100644 index 0000000000..0216373425 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/google-chrome-developer-tools.png differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.css b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.css new file mode 100644 index 0000000000..f6df812d07 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.css @@ -0,0 +1,49 @@ +.app { + display: flex; + flex-direction: column; + gap: 16px; +} + +.start-messages { + width: fit-content; +} + +.window { + width: 100%; + border: 2px solid #464154; + overflow: hidden; +} + +.window__header { + position: sticky; + padding: 8px; + display: flex; + justify-content: space-between; + align-items: center; + background-color: #736e7e; +} + +.window__title { + margin: 0; + font-size: 24px; + font-weight: 700; + color: white; + letter-spacing: 1px; +} + +.window__button { + padding: 4px; + background: #4f495c; + outline: none; + border: 2px solid #464154; + color: white; + font-size: 16px; + cursor: pointer; +} + +.window__body { + height: 250px; + padding: 16px; + overflow: scroll; + background-color: #736e7e33; +} \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.html b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.html new file mode 100644 index 0000000000..c4adde9866 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + +
+ +
+
+

Сообщения:

+ +
+
+ Нет сообщений. +
+
+
+ + + + + diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.js b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.js new file mode 100644 index 0000000000..0b8e88f034 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-dom.view/index.js @@ -0,0 +1,23 @@ +const startMessagesBtn = document.querySelector('.start-messages'); // (1) +const closeWindowBtn = document.querySelector('.window__button'); // (2) +const windowElementRef = new WeakRef(document.querySelector(".window__body")); // (3) + +startMessagesBtn.addEventListener('click', () => { // (4) + startMessages(windowElementRef); + startMessagesBtn.disabled = true; +}); + +closeWindowBtn.addEventListener('click', () => document.querySelector(".window__body").remove()); // (5) + +const startMessages = (element) => { + const timerId = setInterval(() => { // (6) + if (element.deref()) { // (7) + const payload = document.createElement("p"); + payload.textContent = `Сообщение: Статус системы OK: ${new Date().toLocaleTimeString()}`; + element.deref().append(payload); + } else { // (8) + alert("Элемент был удалён."); // (9) + clearInterval(timerId); + } + }, 1000); +}; \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-01.svg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-01.svg new file mode 100644 index 0000000000..2a507dbcdb --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-01.svg @@ -0,0 +1,32 @@ + + + + + + + + user + + name: "John" + Object + + <global> + + + + + + + + + + + + + + + + admin + + + \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-02.svg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-02.svg new file mode 100644 index 0000000000..6cc199a128 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-02.svg @@ -0,0 +1,33 @@ + + + + + + + + + + <global> + + + name: "John" + Object + + + + + + + + + + + + admin + + + + + + + \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-03.svg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-03.svg new file mode 100644 index 0000000000..67425a58fd --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-03.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + ключ + значение + image-01.jpg + image-02.jpg + image-03.jpg + + + + + + + + + + + + + + Объект WeakRef + + + + + + + + + + + + + + + + Объект WeakRef + + + + + + + + + + + + + + + + + + + Объект WeakRef + + + + + + + \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-04.svg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-04.svg new file mode 100644 index 0000000000..64770a0780 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-04.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + ключ + значение + image-01.jpg + image-02.jpg + image-03.jpg + + + + + + + + + + + + + + Объект WeakRef + + + + + + + + + + + + + + + + Объект WeakRef + + + + + undefined + undefined + + + + + + + + + + + + + + + Объект WeakRef + + + \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-05.svg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-05.svg new file mode 100644 index 0000000000..423dfba958 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-05.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + image-02.jpg + image-03.jpg + + ключ + значение + image-01.jpg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Объект WeakRef + + + + + + + + + + + + + + + + Объект WeakRef + + + + + undefined + undefined + Удалено колбэком очистки FinalizationRegistry + + + + + + + + + + + + + + + Объект WeakRef + + + + \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-02.png b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-02.png new file mode 100644 index 0000000000..7d8bb01e88 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-02.png differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-03.gif b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-03.gif new file mode 100644 index 0000000000..b81966dda4 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-03.gif differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-04.jpg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-04.jpg new file mode 100644 index 0000000000..ba60f1e860 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-04.jpg differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-05.gif b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-05.gif new file mode 100644 index 0000000000..d34bda4d73 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-05.gif differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-06.jpg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-06.jpg new file mode 100644 index 0000000000..b2655540f9 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-06.jpg differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-07.gif b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-07.gif new file mode 100644 index 0000000000..51f8745188 Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-07.gif differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-08.jpg b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-08.jpg new file mode 100644 index 0000000000..5f98aec14d Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry-demo-08.jpg differ diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.css b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.css new file mode 100644 index 0000000000..e6c9e39606 --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.css @@ -0,0 +1,285 @@ +:root { + --mineralGreen: 60, 98, 85; + --viridianGreen: 97, 135, 110; + --swampGreen: 166, 187, 141; + --fallGreen: 234, 231, 177; + --brinkPink: #FA7070; + --silverChalice: 178, 178, 178; + --white: 255, 255, 255; + --black: 0, 0, 0; + + --topBarHeight: 64px; + --itemPadding: 32px; + --containerGap: 8px; +} + +@keyframes zoom-in { + 0% { + transform: scale(1, 1); + } + + 100% { + transform: scale(1.30, 1.30); + } +} + +body, html { + margin: 0; + padding: 0; +} + +.app { + min-height: 100vh; + background-color: rgba(var(--viridianGreen), 0.5); +} + +.header { + height: var(--topBarHeight); + padding: 0 24px; + display: flex; + justify-content: space-between; + align-items: center; + background-color: rgba(var(--mineralGreen), 1); +} + +.header-text { + color: white; +} + +.container { + display: flex; + gap: 24px; + padding: var(--itemPadding); +} + +.item { + width: 50%; +} + +.item--scrollable { + overflow-y: scroll; + height: calc(100vh - var(--topBarHeight) - (var(--itemPadding) * 2)); +} + +.thumbnails-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: center; + align-items: center; +} + +.thumbnail-item { + width: calc(25% - var(--containerGap)); + cursor: pointer; + position: relative; +} + +.thumbnail-item:hover { + z-index: 1; + animation: zoom-in 0.1s forwards; +} + +.thumbnail-item--selected { + outline: 3px solid rgba(var(--fallGreen), 1); + outline-offset: -3px; +} + +.badge { + width: 16px; + height: 16px; + display: flex; + justify-content: center; + align-items: center; + padding: 4px; + position: absolute; + right: 8px; + bottom: 8px; + border-radius: 50%; + border: 2px solid rgba(var(--fallGreen), 1); + background-color: rgba(var(--swampGreen), 1); +} + +.check { + display: inline-block; + transform: rotate(45deg); + border-bottom: 2px solid white; + border-right: 2px solid white; + width: 6px; + height: 12px; +} + +.img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-content: center; + padding: 0 0 16px 0; + gap: 8px; +} + +.select { + padding: 16px; + cursor: pointer; + font-weight: 700; + color: rgba(var(--black), 1); + border: 2px solid rgba(var(--swampGreen), 0.5); + background-color: rgba(var(--swampGreen), 1); +} + +.select:disabled { + cursor: not-allowed; + background-color: rgba(var(--silverChalice), 1); + color: rgba(var(--black), 0.5); + border: 2px solid rgba(var(--black), 0.25); +} + +.btn { + outline: none; + padding: 16px; + cursor: pointer; + font-weight: 700; + color: rgba(var(--black), 1); + border: 2px solid rgba(var(--black), 0.5); +} + +.btn--primary { + background-color: rgba(var(--mineralGreen), 1); +} + +.btn--primary:hover:not([disabled]) { + background-color: rgba(var(--mineralGreen), 0.85); +} + +.btn--secondary { + background-color: rgba(var(--viridianGreen), 0.5); +} + +.btn--secondary:hover:not([disabled]) { + background-color: rgba(var(--swampGreen), 0.25); +} + +.btn--success { + background-color: rgba(var(--fallGreen), 1); +} + +.btn--success:hover:not([disabled]) { + background-color: rgba(var(--fallGreen), 0.85); +} + +.btn:disabled { + cursor: not-allowed; + background-color: rgba(var(--silverChalice), 1); + color: rgba(var(--black), 0.5); + border: 2px solid rgba(var(--black), 0.25); +} + +.previewContainer { + margin-bottom: 16px; + display: flex; + width: 100%; + height: 40vh; + overflow: scroll; + border: 3px solid rgba(var(--black), 1); +} + +.previewContainer--disabled { + background-color: rgba(var(--black), 0.1); + cursor: not-allowed; +} + +.canvas { + margin: auto; + display: none; +} + +.canvas--ready { + display: block; +} + +.spinnerContainer { + display: flex; + gap: 8px; + flex-direction: column; + align-content: center; + align-items: center; + margin: auto; +} + +.spinnerContainer--hidden { + display: none; +} + +.spinnerText { + margin: 0; + color: rgba(var(--mineralGreen), 1); +} + +.spinner { + display: inline-block; + width: 50px; + height: 50px; + margin: auto; + border: 3px solid rgba(var(--mineralGreen), 0.3); + border-radius: 50%; + border-top-color: rgba(var(--mineralGreen), 0.9); + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loggerContainer { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0 8px 8px 8px; + width: 100%; + min-height: 30vh; + max-height: 30vh; + overflow: scroll; + border-left: 3px solid rgba(var(--black), 0.25); +} + +.logger-title { + display: flex; + align-items: center; + padding: 8px; + position: sticky; + height: 40px; + min-height: 40px; + top: 0; + left: 0; + background-color: rgba(var(--viridianGreen), 1); + font-size: 24px; + font-weight: 700; + margin: 0; +} + +.logger-item { + font-size: 14px; + padding: 8px; + border: 2px solid #5a5a5a; + color: white; +} + +.logger--primary { + background-color: #13315a; +} + +.logger--success { + background-color: #385a4e; +} + +.logger--error { + background-color: #5a1a24; +} \ No newline at end of file diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.html b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.html new file mode 100644 index 0000000000..e3637c46ec --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + +
+
+

+ Photo Library Collage +

+
+
+
+ +
+
+
+
+
+ + + + +
+
+
+
+

+
+ +
+
+

Logger:

+
+
+
+
+
+ + + + + diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.js b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.js new file mode 100644 index 0000000000..983b34d9ab --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/index.js @@ -0,0 +1,228 @@ +import { + createImageFile, + loadImage, + weakRefCache, + LAYOUTS, + images, + THUMBNAIL_PARAMS, + stateObj, +} from "./utils.js"; + +export const state = new Proxy(stateObj, { + set(target, property, value) { + const previousValue = target[property]; + + target[property] = value; + + if (previousValue !== value) { + handleStateChange(target); + } + + return true; + }, +}); + +// Elements. +const thumbnailsContainerEl = document.querySelector(".thumbnails-container"); +const selectEl = document.querySelector(".select"); +const previewContainerEl = document.querySelector(".previewContainer"); +const canvasEl = document.querySelector(".canvas"); +const createCollageBtn = document.querySelector(".btn-create-collage"); +const startOverBtn = document.querySelector(".btn-start-over"); +const downloadBtn = document.querySelector(".btn-download"); +const spinnerContainerEl = document.querySelector(".spinnerContainer"); +const spinnerTextEl = document.querySelector(".spinnerText"); +const loggerContainerEl = document.querySelector(".loggerContainer"); + +// Renders. +// Render thumbnails previews. +images.forEach((img) => { + const thumbnail = document.createElement("div"); + thumbnail.classList.add("thumbnail-item"); + + thumbnail.innerHTML = ` + + `; + + thumbnail.addEventListener("click", (e) => handleSelection(e, img)); + + thumbnailsContainerEl.appendChild(thumbnail); +}); +// Render layouts select. +LAYOUTS.forEach((layout) => { + const option = document.createElement("option"); + option.value = JSON.stringify(layout); + option.innerHTML = layout.name; + selectEl.appendChild(option); +}); + +const handleStateChange = (state) => { + if (state.loading) { + selectEl.disabled = true; + createCollageBtn.disabled = true; + startOverBtn.disabled = true; + downloadBtn.disabled = true; + previewContainerEl.classList.add("previewContainer--disabled"); + spinnerContainerEl.classList.remove("spinnerContainer--hidden"); + spinnerTextEl.innerText = "Loading..."; + canvasEl.classList.remove("canvas--ready"); + } else if (!state.loading) { + selectEl.disabled = false; + createCollageBtn.disabled = false; + startOverBtn.disabled = false; + downloadBtn.disabled = false; + previewContainerEl.classList.remove("previewContainer--disabled"); + spinnerContainerEl.classList.add("spinnerContainer--hidden"); + canvasEl.classList.add("canvas--ready"); + } + + if (!state.selectedImages.size) { + createCollageBtn.disabled = true; + document.querySelectorAll(".badge").forEach((item) => item.remove()); + } else if (state.selectedImages.size && !state.loading) { + createCollageBtn.disabled = false; + } + + if (!state.collageRendered) { + downloadBtn.disabled = true; + } else if (state.collageRendered) { + downloadBtn.disabled = false; + } +}; +handleStateChange(state); + +const handleSelection = (e, imgName) => { + const imgEl = e.currentTarget; + + imgEl.classList.toggle("thumbnail-item--selected"); + + if (state.selectedImages.has(imgName)) { + state.selectedImages.delete(imgName); + state.selectedImages = new Set(state.selectedImages); + imgEl.querySelector(".badge")?.remove(); + } else { + state.selectedImages = new Set(state.selectedImages.add(imgName)); + + const badge = document.createElement("div"); + badge.classList.add("badge"); + badge.innerHTML = ` +
+ `; + imgEl.prepend(badge); + } +}; + +// Make a wrapper function. +let getCachedImage; +(async () => { + getCachedImage = await weakRefCache(loadImage); +})(); + +const calculateGridRows = (blobsLength) => + Math.ceil(blobsLength / state.currentLayout.columns); + +const drawCollage = (images) => { + state.drawing = true; + + let context = canvasEl.getContext("2d"); + + /** + * Calculate canvas dimensions based on the current layout. + * */ + context.canvas.width = + state.currentLayout.itemWidth * state.currentLayout.columns; + context.canvas.height = + calculateGridRows(images.length) * state.currentLayout.itemHeight; + + let currentRow = 0; + let currentCanvasDx = 0; + let currentCanvasDy = 0; + + for (let i = 0; i < images.length; i++) { + /** + * Get current row of the collage. + * */ + if (i % state.currentLayout.columns === 0) { + currentRow += 1; + currentCanvasDx = 0; + + if (currentRow > 1) { + currentCanvasDy += state.currentLayout.itemHeight; + } + } + + context.drawImage( + images[i], + 0, + 0, + images[i].width, + images[i].height, + currentCanvasDx, + currentCanvasDy, + state.currentLayout.itemWidth, + state.currentLayout.itemHeight, + ); + + currentCanvasDx += state.currentLayout.itemWidth; + } + + state.drawing = false; + state.collageRendered = true; +}; + +const createCollage = async () => { + state.loading = true; + + const images = []; + + for (const image of state.selectedImages.values()) { + const blobImage = await getCachedImage(image.img); + + const url = URL.createObjectURL(blobImage); + const img = await createImageFile(url); + + images.push(img); + URL.revokeObjectURL(url); + } + + state.loading = false; + + drawCollage(images); +}; + +/** + * Clear all settled data to start over. + * */ +const startOver = () => { + state.selectedImages = new Set(); + state.collageRendered = false; + const context = canvasEl.getContext("2d"); + context.clearRect(0, 0, canvasEl.width, canvasEl.height); + + document + .querySelectorAll(".thumbnail-item--selected") + .forEach((item) => item.classList.remove("thumbnail-item--selected")); + + loggerContainerEl.innerHTML = '

Logger:

'; +}; + +const downloadCollage = () => { + const date = new Date(); + const fileName = `Collage-${date.getDay()}-${date.getMonth()}-${date.getFullYear()}.png`; + const img = canvasEl.toDataURL("image/png"); + const link = document.createElement("a"); + link.download = fileName; + link.href = img; + link.click(); + link.remove(); +}; + +const changeLayout = ({ target }) => { + state.currentLayout = JSON.parse(target.value); +}; + +// Listeners. +selectEl.addEventListener("change", changeLayout); +createCollageBtn.addEventListener("click", createCollage); +startOverBtn.addEventListener("click", startOver); +downloadBtn.addEventListener("click", downloadCollage); diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/utils.js b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/utils.js new file mode 100644 index 0000000000..f0140c116a --- /dev/null +++ b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref-finalizationregistry.view/utils.js @@ -0,0 +1,321 @@ +const loggerContainerEl = document.querySelector(".loggerContainer"); + +export const images = [ + { + img: "https://images.unsplash.com/photo-1471357674240-e1a485acb3e1", + }, + { + img: "https://images.unsplash.com/photo-1589118949245-7d38baf380d6", + }, + { + img: "https://images.unsplash.com/photo-1527631746610-bca00a040d60", + }, + { + img: "https://images.unsplash.com/photo-1500835556837-99ac94a94552", + }, + { + img: "https://images.unsplash.com/photo-1503220317375-aaad61436b1b", + }, + { + img: "https://images.unsplash.com/photo-1501785888041-af3ef285b470", + }, + { + img: "https://images.unsplash.com/photo-1528543606781-2f6e6857f318", + }, + { + img: "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9", + }, + { + img: "https://images.unsplash.com/photo-1539635278303-d4002c07eae3", + }, + { + img: "https://images.unsplash.com/photo-1533105079780-92b9be482077", + }, + { + img: "https://images.unsplash.com/photo-1516483638261-f4dbaf036963", + }, + { + img: "https://images.unsplash.com/photo-1502791451862-7bd8c1df43a7", + }, + { + img: "https://plus.unsplash.com/premium_photo-1663047367140-91adf819d007", + }, + { + img: "https://images.unsplash.com/photo-1506197603052-3cc9c3a201bd", + }, + { + img: "https://images.unsplash.com/photo-1517760444937-f6397edcbbcd", + }, + { + img: "https://images.unsplash.com/photo-1518684079-3c830dcef090", + }, + { + img: "https://images.unsplash.com/photo-1505832018823-50331d70d237", + }, + { + img: "https://images.unsplash.com/photo-1524850011238-e3d235c7d4c9", + }, + { + img: "https://plus.unsplash.com/premium_photo-1661277758451-b5053309eea1", + }, + { + img: "https://images.unsplash.com/photo-1541410965313-d53b3c16ef17", + }, + { + img: "https://images.unsplash.com/photo-1528702748617-c64d49f918af", + }, + { + img: "https://images.unsplash.com/photo-1502003148287-a82ef80a6abc", + }, + { + img: "https://plus.unsplash.com/premium_photo-1661281272544-5204ea3a481a", + }, + { + img: "https://images.unsplash.com/photo-1503457574462-bd27054394c1", + }, + { + img: "https://images.unsplash.com/photo-1499363536502-87642509e31b", + }, + { + img: "https://images.unsplash.com/photo-1551918120-9739cb430c6d", + }, + { + img: "https://plus.unsplash.com/premium_photo-1661382219642-43e54f7e81d7", + }, + { + img: "https://images.unsplash.com/photo-1497262693247-aa258f96c4f5", + }, + { + img: "https://images.unsplash.com/photo-1525254134158-4fd5fdd45793", + }, + { + img: "https://plus.unsplash.com/premium_photo-1661274025419-4c54107d5c48", + }, + { + img: "https://images.unsplash.com/photo-1553697388-94e804e2f0f6", + }, + { + img: "https://images.unsplash.com/photo-1574260031597-bcd9eb192b4f", + }, + { + img: "https://images.unsplash.com/photo-1536323760109-ca8c07450053", + }, + { + img: "https://images.unsplash.com/photo-1527824404775-dce343118ebc", + }, + { + img: "https://images.unsplash.com/photo-1612278675615-7b093b07772d", + }, + { + img: "https://images.unsplash.com/photo-1522010675502-c7b3888985f6", + }, + { + img: "https://images.unsplash.com/photo-1501555088652-021faa106b9b", + }, + { + img: "https://plus.unsplash.com/premium_photo-1669223469435-27e091439169", + }, + { + img: "https://images.unsplash.com/photo-1506012787146-f92b2d7d6d96", + }, + { + img: "https://images.unsplash.com/photo-1511739001486-6bfe10ce785f", + }, + { + img: "https://images.unsplash.com/photo-1553342385-111fd6bc6ab3", + }, + { + img: "https://images.unsplash.com/photo-1516546453174-5e1098a4b4af", + }, + { + img: "https://images.unsplash.com/photo-1527142879-95b61a0b8226", + }, + { + img: "https://images.unsplash.com/photo-1520466809213-7b9a56adcd45", + }, + { + img: "https://images.unsplash.com/photo-1516939884455-1445c8652f83", + }, + { + img: "https://images.unsplash.com/photo-1545389336-cf090694435e", + }, + { + img: "https://plus.unsplash.com/premium_photo-1669223469455-b7b734c838f4", + }, + { + img: "https://images.unsplash.com/photo-1454391304352-2bf4678b1a7a", + }, + { + img: "https://images.unsplash.com/photo-1433838552652-f9a46b332c40", + }, + { + img: "https://images.unsplash.com/photo-1506125840744-167167210587", + }, + { + img: "https://images.unsplash.com/photo-1522199873717-bc67b1a5e32b", + }, + { + img: "https://images.unsplash.com/photo-1495904786722-d2b5a19a8535", + }, + { + img: "https://images.unsplash.com/photo-1614094082869-cd4e4b2905c7", + }, + { + img: "https://images.unsplash.com/photo-1474755032398-4b0ed3b2ae5c", + }, + { + img: "https://images.unsplash.com/photo-1501554728187-ce583db33af7", + }, + { + img: "https://images.unsplash.com/photo-1515859005217-8a1f08870f59", + }, + { + img: "https://images.unsplash.com/photo-1531141445733-14c2eb7d4c1f", + }, + { + img: "https://images.unsplash.com/photo-1500259783852-0ca9ce8a64dc", + }, + { + img: "https://images.unsplash.com/photo-1510662145379-13537db782dc", + }, + { + img: "https://images.unsplash.com/photo-1573790387438-4da905039392", + }, + { + img: "https://images.unsplash.com/photo-1512757776214-26d36777b513", + }, + { + img: "https://images.unsplash.com/photo-1518855706573-84de4022b69b", + }, + { + img: "https://images.unsplash.com/photo-1500049242364-5f500807cdd7", + }, + { + img: "https://images.unsplash.com/photo-1528759335187-3b683174c86a", + }, +]; +export const THUMBNAIL_PARAMS = "w=240&h=240&fit=crop&auto=format"; + +// Console styles. +export const CONSOLE_BASE_STYLES = [ + "font-size: 12px", + "padding: 4px", + "border: 2px solid #5a5a5a", + "color: white", +].join(";"); +export const CONSOLE_PRIMARY = [ + CONSOLE_BASE_STYLES, + "background-color: #13315a", +].join(";"); +export const CONSOLE_SUCCESS = [ + CONSOLE_BASE_STYLES, + "background-color: #385a4e", +].join(";"); +export const CONSOLE_ERROR = [ + CONSOLE_BASE_STYLES, + "background-color: #5a1a24", +].join(";"); + +// Layouts. +export const LAYOUT_4_COLUMNS = { + name: "Layout 4 columns", + columns: 4, + itemWidth: 240, + itemHeight: 240, +}; +export const LAYOUT_8_COLUMNS = { + name: "Layout 8 columns", + columns: 8, + itemWidth: 240, + itemHeight: 240, +}; +export const LAYOUTS = [LAYOUT_4_COLUMNS, LAYOUT_8_COLUMNS]; + +export const createImageFile = async (src) => + new Promise((resolve, reject) => { + const img = new Image(); + img.src = src; + img.onload = () => resolve(img); + img.onerror = () => reject(new Error("Failed to construct image.")); + }); + +export const loadImage = async (url) => { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(String(response.status)); + } + + return await response.blob(); + } catch (e) { + console.log(`%cFETCHED_FAILED: ${e}`, CONSOLE_ERROR); + } +}; + +export const weakRefCache = (fetchImg) => { + const imgCache = new Map(); + const registry = new FinalizationRegistry(({ imgName, size, type }) => { + const cachedImg = imgCache.get(imgName); + if (cachedImg && !cachedImg.deref()) { + imgCache.delete(imgName); + console.log( + `%cCLEANED_IMAGE: Url: ${imgName}, Size: ${size}, Type: ${type}`, + CONSOLE_ERROR, + ); + + const logEl = document.createElement("div"); + logEl.classList.add("logger-item", "logger--error"); + logEl.innerHTML = `CLEANED_IMAGE: Url: ${imgName}, Size: ${size}, Type: ${type}`; + loggerContainerEl.appendChild(logEl); + loggerContainerEl.scrollTop = loggerContainerEl.scrollHeight; + } + }); + + return async (imgName) => { + const cachedImg = imgCache.get(imgName); + + if (cachedImg?.deref() !== undefined) { + console.log( + `%cCACHED_IMAGE: Url: ${imgName}, Size: ${cachedImg.size}, Type: ${cachedImg.type}`, + CONSOLE_SUCCESS, + ); + + const logEl = document.createElement("div"); + logEl.classList.add("logger-item", "logger--success"); + logEl.innerHTML = `CACHED_IMAGE: Url: ${imgName}, Size: ${cachedImg.size}, Type: ${cachedImg.type}`; + loggerContainerEl.appendChild(logEl); + loggerContainerEl.scrollTop = loggerContainerEl.scrollHeight; + + return cachedImg?.deref(); + } + + const newImg = await fetchImg(imgName); + console.log( + `%cFETCHED_IMAGE: Url: ${imgName}, Size: ${newImg.size}, Type: ${newImg.type}`, + CONSOLE_PRIMARY, + ); + + const logEl = document.createElement("div"); + logEl.classList.add("logger-item", "logger--primary"); + logEl.innerHTML = `FETCHED_IMAGE: Url: ${imgName}, Size: ${newImg.size}, Type: ${newImg.type}`; + loggerContainerEl.appendChild(logEl); + loggerContainerEl.scrollTop = loggerContainerEl.scrollHeight; + + imgCache.set(imgName, new WeakRef(newImg)); + registry.register(newImg, { + imgName, + size: newImg.size, + type: newImg.type, + }); + + return newImg; + }; +}; + +export const stateObj = { + loading: false, + drawing: true, + collageRendered: false, + currentLayout: LAYOUTS[0], + selectedImages: new Set(), +}; diff --git a/1-js/99-js-misc/09-weakref-finalizationregistry/weakref_-finalizationregistry-demo-01.png b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref_-finalizationregistry-demo-01.png new file mode 100644 index 0000000000..fc33a023af Binary files /dev/null and b/1-js/99-js-misc/09-weakref-finalizationregistry/weakref_-finalizationregistry-demo-01.png differ diff --git a/1-js/index.md b/1-js/index.md index 222ce34ec8..db327c8555 100644 --- a/1-js/index.md +++ b/1-js/index.md @@ -1,5 +1,5 @@ -# Язык программирования JavaScript +# Язык JavaScript Здесь вы можете изучить JavaScript, начиная с нуля и заканчивая продвинутыми концепциями вроде ООП. -Мы будем использовать браузер в качестве окружения, но основное внимание будет уделяться именно самому языку JavaScript. +Мы сосредоточимся на самом языке, изредка добавляя заметки о средах его исполнения. diff --git a/2-ui/1-document/01-browser-environment/article.md b/2-ui/1-document/01-browser-environment/article.md index 443dc5887a..a461966b8b 100644 --- a/2-ui/1-document/01-browser-environment/article.md +++ b/2-ui/1-document/01-browser-environment/article.md @@ -2,7 +2,7 @@ Язык JavaScript изначально был создан для веб-браузеров. Но с тех пор он значительно эволюционировал и превратился в кроссплатформенный язык программирования для решения широкого круга задач. -Сегодня JavaScript может использоваться в браузере, на веб-сервере или в какой-то другой среде, даже в кофеварке. Каждая среда предоставляет свой функционал, который спецификация JavaScript называет *окружением*. +Сегодня JavaScript может использоваться в браузере, на веб-сервере или в какой-то другой среде, даже в кофеварке. Каждая среда предоставляет свою функциональность, которую спецификация JavaScript называет *окружением*. Окружение предоставляет свои объекты и дополнительные функции, в дополнение базовым языковым. Браузеры, например, дают средства для управления веб-страницами. Node.js делает доступными какие-то серверные возможности и так далее. @@ -17,7 +17,7 @@ Например, здесь мы используем `window` как глобальный объект: -```js run +```js run global function sayHi() { alert("Hello"); } @@ -54,7 +54,7 @@ setTimeout(() => document.body.style.background = "", 1000); - **DOM Living Standard** на ```smart header="DOM - не только для браузеров" -Спецификация DOM описывает структуру документа и предоставляет объекты для манипуляций со страницей. Существует и другие, отличные от браузеров, инструменты, использующие DOM. +Спецификация DOM описывает структуру документа и предоставляет объекты для манипуляций со страницей. Существуют и другие, отличные от браузеров, инструменты, использующие DOM. Например, серверные скрипты, которые загружают и обрабатывают HTML-страницы, также могут использовать DOM. При этом они могут поддерживать спецификацию не полностью. ``` @@ -96,7 +96,7 @@ BOM является частью общей [спецификации HTML](htt Говоря о стандартах, у нас есть: Спецификация DOM -: описывает структуру документа, манипуляции с контентом и события, подробнее на . +: Описывает структуру документа, манипуляции с контентом и события, подробнее на . Спецификация CSSOM : Описывает файлы стилей, правила написания стилей и манипуляций с ними, а также то, как это всё связано со страницей, подробнее на . @@ -108,7 +108,7 @@ BOM является частью общей [спецификации HTML](htt Пожалуйста, заметьте для себя эти ссылки, так как по ним содержится очень много информации, которую невозможно изучить полностью и держать в уме. -Когда вам нужно будет прочитать о каком-то свойстве или методе, справочник на сайте Mozilla тоже очень хороший ресурс, хотя ничто не сравнится с чтением спецификации: она сложная и объёмная, но сделает ваши знания максимально полными. +Когда вам нужно будет прочитать о каком-то свойстве или методе, справочник на сайте Mozilla тоже очень хороший ресурс, хотя ничто не сравнится с чтением спецификации: она сложная и объёмная, но сделает ваши знания максимально полными. Для поиска чего-либо обычно удобно использовать интернет-поиск со словами "WHATWG [термин]" или "MDN [термин]", например , . diff --git a/2-ui/1-document/01-browser-environment/windowObjects.svg b/2-ui/1-document/01-browser-environment/windowObjects.svg index 3b525b2cb5..b7e18bb347 100644 --- a/2-ui/1-document/01-browser-environment/windowObjects.svg +++ b/2-ui/1-document/01-browser-environment/windowObjects.svg @@ -1 +1 @@ -windowdocumentObjectnavigatorscreenlocationframeshistoryArrayFunctionXMLHttpRequestBOMJavaScriptDOM \ No newline at end of file +windowdocumentObjectnavigatorscreenlocationframeshistoryArrayFunctionXMLHttpRequestBOMJavaScriptDOM \ No newline at end of file diff --git a/2-ui/1-document/02-dom-nodes/article.md b/2-ui/1-document/02-dom-nodes/article.md index 3f73007e5f..0856d16e75 100644 --- a/2-ui/1-document/02-dom-nodes/article.md +++ b/2-ui/1-document/02-dom-nodes/article.md @@ -45,7 +45,7 @@ DOM -- это представление HTML-документа в виде д
@@ -182,7 +182,7 @@ drawHtmlTree(node5, 'div.domtree', 600, 200);
@@ -206,7 +206,7 @@ drawHtmlTree(node6, 'div.domtree', 690, 500); ## Поэкспериментируйте сами -Чтобы посмотреть структуру DOM в реальном времени, попробуйте [Live DOM Viewer](http://software.hixie.ch/utilities/js/live-dom-viewer/). Просто введите что-нибудь в поле, и ниже вы увидите, как меняется DOM. +Чтобы посмотреть структуру DOM в реальном времени, попробуйте [Live DOM Viewer](https://software.hixie.ch/utilities/js/live-dom-viewer/). Просто введите что-нибудь в поле, и ниже вы увидите, как меняется DOM. Другой способ исследовать DOM - это использовать инструменты разработчика браузера. Это то, что мы каждый день делаем при разработке. diff --git a/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md b/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md index 732eeff02d..fd383ae49a 100644 --- a/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md +++ b/2-ui/1-document/03-dom-navigation/1-dom-children/solution.md @@ -1,6 +1,5 @@ Есть несколько способов для получения элементов, например: - DOM-узел элемента `
`: ```js diff --git a/2-ui/1-document/03-dom-navigation/1-dom-children/task.md b/2-ui/1-document/03-dom-navigation/1-dom-children/task.md index f4c604ee6e..5d95cf769a 100644 --- a/2-ui/1-document/03-dom-navigation/1-dom-children/task.md +++ b/2-ui/1-document/03-dom-navigation/1-dom-children/task.md @@ -18,7 +18,7 @@ importance: 5 ``` -Как получить: -- Напишите код, который получит элемент `
`? -- Напишите код, который получит `