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 411f2de6c0..64ef99485d 100644 --- a/1-js/01-getting-started/1-intro/article.md +++ b/1-js/01-getting-started/1-intro/article.md @@ -15,7 +15,7 @@ ```smart header="Почему JavaScript?" Когда 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,7 +45,7 @@ Современный JavaScript - это "безопасный" язык программирования. Он не предоставляет низкоуровневый доступ к памяти или процессору, потому что изначально был создан для браузеров, не требующих этого. -Возможности JavaScript сильно зависят от окружения, в котором он работает. Например, [Node.JS](https://ru.wikipedia.org/wiki/Node.js) поддерживает функции чтения/записи произвольных файлов, выполнения сетевых запросов и т.д. +Возможности JavaScript сильно зависят от окружения, в котором он работает. Например, [Node.js](https://ru.wikipedia.org/wiki/Node.js) поддерживает функции чтения/записи произвольных файлов, выполнения сетевых запросов и т.д. В браузере для JavaScript доступно всё, что связано с манипулированием веб-страницами, взаимодействием с пользователем и веб-сервером. @@ -106,9 +106,9 @@ 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. 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 bc00e6fa23..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,7 +5,7 @@ ## Спецификация -[Спецификация 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. Она определяет сам язык. Вначале спецификация может показаться тяжеловатой для понимания из-за слишком формального стиля изложения. Если вы ищете источник самой достоверной информации, то это правильное место, но она не для ежедневного использования. @@ -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 b5478da906..c2078d5efa 100644 --- a/1-js/01-getting-started/3-code-editors/article.md +++ b/1-js/01-getting-started/3-code-editors/article.md @@ -12,11 +12,8 @@ IDE загружает проект (который может состоять Если вы ещё не выбрали себе 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 b986e79d67..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. @@ -22,7 +22,7 @@ Chrome и Firefox снискали любовь подавляющего бол Она выглядит приблизительно следующим образом: -![chrome](chrome.png) +![](chrome.webp) Точный внешний вид инструментов разработки зависит от используемой версии Chrome. Время от времени некоторые детали изменяются, но в целом внешний вид остаётся примерно похожим на предыдущие версии. @@ -52,7 +52,7 @@ Safari (браузер для Mac, не поддерживается в сист Откройте Настройки (Preferences) и перейдите к панели "Продвинутые" (Advanced). В самом низу вы найдёте чекбокс: -![safari](safari.png) +![](safari.png) Теперь консоль можно активировать нажатием клавиш `key:Cmd+Opt+C`. Также обратите внимание на новый элемент меню "Разработка" ("Develop"). В нем содержится большое количество команд и настроек. 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/article.md b/1-js/02-first-steps/01-hello-world/article.md index b8ef12bf57..b42e30a558 100644 --- a/1-js/02-first-steps/01-hello-world/article.md +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -70,7 +70,7 @@ ``` -Здесь `/path/to/script.js` - это абсолютный путь до скрипта от корня сайта. Также можно указать относительный путь от текущей страницы. Например, `src="script.js"` или `src="./script.js"` будет означать, что файл `"script.js"` находится в текущей папке. +Здесь `/path/to/script.js` - это абсолютный путь от корневой папки до необходимого файла. Корневой папкой может быть корень диска или корень сайта, в зависимости от условий работы сайта. Также можно указать относительный путь от текущей страницы. Например, `src="script.js"` или `src="./script.js"` будет означать, что файл `"script.js"` находится в текущей папке. Можно указать и полный URL-адрес. Например: diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md index 63bf59c5b0..88a6e2bcea 100644 --- a/1-js/02-first-steps/02-structure/article.md +++ b/1-js/02-first-steps/02-structure/article.md @@ -56,38 +56,36 @@ 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`, чтобы код работал правильно. + +Это может произойти и в некоторых других ситуациях. ```` Мы рекомендуем ставить точку с запятой между инструкциями, даже если они отделены переносами строк. Это правило широко используется в сообществе разработчиков. Стоит отметить ещё раз -- в большинстве случаев *можно* не ставить точку с запятой. Но безопаснее, особенно для новичка, ставить её. 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 da77812085..15290f9dd4 100644 --- a/1-js/02-first-steps/03-strict-mode/article.md +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -19,7 +19,7 @@ ... ``` -Позже мы изучим функции (способ группировки команд). Забегая вперёд, заметим, что вместо всего скрипта `"use strict"` можно поставить в начале большинства видов функций. Это позволяет включить строгий режим только в конкретной функции. Но обычно люди используют его для всего файла. +Совсем скоро мы начнём изучать функции (способ группировки команд), поэтому заранее отметим, что в начале большинства видов функций можно поставить `"use strict"`. Это позволяет включить строгий режим только в конкретной функции. Но обычно люди используют его для всего файла. ````warn header="Убедитесь, что \"use strict\" находится в начале" @@ -51,6 +51,8 @@ alert("some code"); Иногда, когда `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() { diff --git a/1-js/02-first-steps/04-variables/article.md b/1-js/02-first-steps/04-variables/article.md index 8fdaa14925..b1bdebc933 100644 --- a/1-js/02-first-steps/04-variables/article.md +++ b/1-js/02-first-steps/04-variables/article.md @@ -12,19 +12,19 @@ JavaScript-приложению обычно нужно работать с ин Для создания переменной в 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,8 +90,7 @@ let user = 'John' Ключевое слово `var` - *почти* то же самое, что и `let`. Оно объявляет переменную, но немного по-другому, "устаревшим" способом. -Есть тонкие различия между `let` и `var`, но они пока не имеют для нас значения. -Мы подробно рассмотрим их в главе . +Есть тонкие различия между `let` и `var`, но они пока не имеют для нас значения. Мы подробно рассмотрим их в главе . ```` ## Аналогия из жизни @@ -106,6 +104,7 @@ let user = 'John' Мы можем положить любое значение в коробку. Мы также можем изменить его столько раз, сколько захотим: + ```js run let message; @@ -152,13 +151,11 @@ let message = "Другое"; // SyntaxError: 'message' has already been declare ```` ```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] @@ -175,8 +172,7 @@ let userName; let test123; ``` -Если имя содержит несколько слов, обычно используется [верблюжья нотация](https://ru.wikipedia.org/wiki/CamelCase), -то есть, слова следуют одно за другим, где каждое следующее слово начинается с заглавной буквы: `myVeryLongName`. +Если имя содержит несколько слов, обычно используется [верблюжья нотация](https://ru.wikipedia.org/wiki/CamelCase), то есть, слова следуют одно за другим, где каждое следующее слово начинается с заглавной буквы: `myVeryLongName`. Самое интересное -- знак доллара `'$'` и подчёркивание `'_'` также можно использовать в названиях. Это обычные символы, как и буквы, без какого-либо особого значения. @@ -198,7 +194,7 @@ let my-name; // дефис '-' не разрешён в имени ``` ```smart header="Регистр имеет значение" -Переменные с именами `apple` и `AppLE` -- это две разные переменные. +Переменные с именами `apple` и `APPLE` -- это две разные переменные. ``` ````smart header="Нелатинские буквы разрешены, но не рекомендуются" @@ -296,6 +292,7 @@ alert(color); // #FF7F00 Название "константа" просто означает, что значение переменной никогда не меняется. Но есть константы, которые известны до выполнения (например, шестнадцатеричное значение для красного цвета), а есть константы, которые *вычисляются* во время выполнения сценария, но не изменяются после их первоначального назначения. Например: + ```js const pageLoadTime = /* время, потраченное на загрузку веб-страницы */; ``` diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md index 5a1de55e80..5410236e3c 100644 --- a/1-js/02-first-steps/05-types/article.md +++ b/1-js/02-first-steps/05-types/article.md @@ -46,13 +46,15 @@ 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 "безопасны". Мы можем делать что угодно: делить на ноль, обращаться с нечисловыми строками как с числами и т.д. @@ -66,9 +68,20 @@ n = 12.345; ## BigInt -В JavaScript тип "number" не может содержать числа больше, чем (253-1) (т. е. `9007199254740991`), или меньше, чем -(253-1) для отрицательных чисел. Это техническое ограничение вызвано их внутренним представлением. +В JavaScript тип `number` не может безопасно работать с числами, большими, чем (253-1) (т. е. `9007199254740991`) или меньшими, чем -(253-1) для отрицательных чисел. -Для большинства случаев этого достаточно. Но иногда нам нужны действительно гигантские числа, например, в криптографии или при использовании метки времени ("timestamp") с микросекундами. +Если говорить совсем точно, то, технически, тип `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, чтобы дать возможность работать с целыми числами произвольной длины. @@ -79,7 +92,7 @@ n = 12.345; const bigInt = 1234567890123456789012345678901234567890n; ``` -Так как `BigInt`-числа нужны достаточно редко, мы рассмотрим их в отдельной главе . Ознакомьтесь с ней, когда вам понадобятся настолько большие числа. +Так как необходимость в использовании `BigInt`--чисел появляется достаточно редко, мы рассмотрим их в отдельной главе . Ознакомьтесь с ней, когда вам понадобятся настолько большие числа. ```smart header="Поддержка" В данный момент `BigInt` поддерживается только в браузерах Firefox, Chrome, Edge и Safari, но не поддерживается в IE. @@ -212,10 +225,20 @@ alert(age); // "undefined" У него есть две синтаксические формы: -1. Синтаксис оператора: `typeof x`. -2. Синтаксис функции: `typeof(x)`. +```js +// Обычный синтаксис +typeof 5 // Выведет "number" +// Синтаксис, напоминающий вызов функции (встречается реже) +typeof(5) // Также выведет "number" +``` + +Если передается выражение, то нужно заключать его в скобки, т.к. typeof имеет более высокий приоритет, чем бинарные операторы: -Другими словами, он работает со скобками или без скобок. Результат одинаковый. +```js +typeof 50 + " Квартир"; // Выведет "number Квартир" +typeof (50 + " Квартир"); // Выведет "string" +``` +Другими словами, скобки необходимы для определения типа значения, которое получилось в результате выполнения выражения в них. Вызов `typeof x` возвращает строку с именем типа: @@ -254,16 +277,18 @@ typeof alert // "function" (3) ## Итого -В JavaScript есть 8 основных типов. - -- `number` для любых чисел: целочисленных или чисел с плавающей точкой; целочисленные значения ограничены диапазоном ±(253-1). -- `bigint` для целых чисел произвольной длины. -- `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/07-type-conversions/article.md b/1-js/02-first-steps/07-type-conversions/article.md index 253641a024..bd413fa539 100644 --- a/1-js/02-first-steps/07-type-conversions/article.md +++ b/1-js/02-first-steps/07-type-conversions/article.md @@ -7,7 +7,9 @@ Есть также случаи, когда нам нужно явно преобразовать значение в ожидаемый тип. ```smart header="Пока что мы не говорим об объектах" -В этой главе мы не касаемся объектов. Сначала мы разберём преобразование примитивных значений. Мы разберём преобразование объектов позже, в главе . +В этой главе мы не касаемся объектов. Сначала мы разберём преобразование примитивных значений. + +Мы разберём преобразование объектов позже, в главе . ``` ## Строковое преобразование @@ -68,7 +70,7 @@ alert(age); // NaN, преобразование не удалось |`undefined`|`NaN`| |`null`|`0`| |true / false | `1` / `0` | -| `string` | Пробельные символы по краям обрезаются. Далее, если остаётся пустая строка, то получаем `0`, иначе из непустой строки "считывается" число. При ошибке результат `NaN`.| +| `string` | Пробельные символы (пробелы, знаки табуляции `\t`, знаки новой строки `\n` и т. п.) по краям обрезаются. Далее, если остаётся пустая строка, то получаем `0`, иначе из непустой строки "считывается" число. При ошибке результат `NaN`.| Примеры: @@ -104,7 +106,7 @@ alert( Boolean("Привет!") ); // true alert( Boolean("") ); // false ``` -````warn header="Заметим, что строчка с нулём `\"0\"` — это `true`" +````warn header="Заметим, что строка с нулём `\"0\"` — это `true`" Некоторые языки (к примеру, PHP) воспринимают строку `"0"` как `false`. Но в JavaScript, если строка не пустая, то она всегда `true`. ```js run diff --git a/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md index a71061606e..24d683549d 100644 --- a/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md @@ -9,7 +9,6 @@ true + false = 1 "$" + 4 + 5 = "$45" "4" - 2 = 2 "4px" - 2 = NaN -7 / 0 = Infinity " -9 " + 5 = " -9 5" // (3) " -9 " - 5 = -14 // (4) null + 1 = 1 // (5) diff --git a/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md index 1009616c35..ccabd3689b 100644 --- a/1-js/02-first-steps/08-operators/3-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 diff --git a/1-js/02-first-steps/08-operators/article.md b/1-js/02-first-steps/08-operators/article.md index 677ebac556..95685286e7 100644 --- a/1-js/02-first-steps/08-operators/article.md +++ b/1-js/02-first-steps/08-operators/article.md @@ -52,28 +52,29 @@ ```js run alert( 5 % 2 ); // 1, остаток от деления 5 на 2 alert( 8 % 3 ); // 2, остаток от деления 8 на 3 +alert( 8 % 4 ); // 0, остаток от деления 8 на 4 ``` ### Возведение в степень ** -В выражении `a ** b` оператор возведения в степень умножает `a` на само себя `b` раз. +Оператор возведения в степень `a ** b` возводит `a` в степень `b`. + +В школьной математике мы записываем это как ab. Например: ```js run -alert( 2 ** 2 ); // 4 (2 умножено на себя 2 раза) -alert( 2 ** 3 ); // 8 (2 * 2 * 2, 3 раза) -alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2, 4 раза) +alert( 2 ** 2 ); // 2² = 4 +alert( 2 ** 3 ); // 2³ = 8 +alert( 2 ** 4 ); // 2⁴ = 16 ``` -Математически, оператор работает и для нецелых чисел. Например, квадратный корень является возведением в степень `1/2`: +Математически, оператор работает и для нецелых чисел. Например, квадратный корень является возведением в степень ½: ```js run alert( 4 ** (1/2) ); // 2 (степень 1/2 эквивалентна взятию квадратного корня) -alert( 8 ** (1/3) ); // 2 (степень 1/3 эквивалентна взятию кубического корня) ``` - ## Сложение строк при помощи бинарного + Давайте рассмотрим специальные возможности операторов JavaScript, которые выходят за рамки школьной арифметики. @@ -104,7 +105,7 @@ alert( 2 + '1' ); // "21" alert(2 + 2 + '1' ); // будет "41", а не "221" ``` -Здесь операторы работают один за другим. Первый `+` складывает два числа и возвращает `4`, затем следующий `+` объединяет результат со строкой, производя действие `4 + '1' = 41`. +Здесь операторы работают один за другим. Первый `+` складывает два числа и возвращает `4`, затем следующий `+` объединяет результат со строкой, производя действие `4 + '1' = '41'`. Сложение и преобразование строк — это особенность бинарного плюса `+`. Другие арифметические операторы работают только с числами и всегда преобразуют операнды в числа. @@ -166,7 +167,7 @@ alert( +apples + +oranges ); // 5 // alert( Number(apples) + Number(oranges) ); // 5 ``` -С точки зрения математика, такое изобилие плюсов выглядит странным. Но с точки зрения программиста тут нет ничего особенного: сначала выполнятся унарные плюсы, которые приведут строки к числам, а затем бинарный `'+'` их сложит. +С точки зрения математики, такое изобилие плюсов выглядит странным. Но с точки зрения программиста тут нет ничего особенного: сначала выполнятся унарные плюсы, которые приведут строки к числам, а затем бинарный `'+'` их сложит. Почему унарные плюсы выполнились до бинарного сложения? Как мы сейчас увидим, дело в их приоритете. @@ -185,22 +186,22 @@ alert( +apples + +oranges ); // 5 | Приоритет | Название | Обозначение | |------------|------|------| | ... | ... | ... | -| 17 | унарный плюс | `+` | -| 17 | унарный минус | `-` | -| 16 | возведение в степень | `**` | -| 15 | умножение | `*` | -| 15 | деление | `/` | -| 13 | сложение | `+` | -| 13 | вычитание | `-` | +| 15 | унарный плюс | `+` | +| 15 | унарный минус | `-` | +| 14 | возведение в степень | `**` | +| 13 | умножение | `*` | +| 13 | деление | `/` | +| 12 | сложение | `+` | +| 12 | вычитание | `-` | | ... | ... | ... | -| 3 | присваивание | `=` | +| 2 | присваивание | `=` | | ... | ... | ... | -Так как "унарный плюс" имеет приоритет `17`, который выше, чем `13` у "сложения" (бинарный плюс), то в выражении `"+apples + +oranges"` сначала выполнятся унарные плюсы, а затем сложение. +Так как "унарный плюс" имеет приоритет `15`, который выше, чем `12` у "сложения" (бинарный плюс), то в выражении `"+apples + +oranges"` сначала выполнятся унарные плюсы, а затем сложение. ## Присваивание -Давайте отметим, что в таблице приоритетов также есть оператор присваивания `=`. У него один из самых низких приоритетов: `3`. +Давайте отметим, что в таблице приоритетов также есть оператор присваивания `=`. У него один из самых низких приоритетов: `2`. Именно поэтому, когда переменной что-либо присваивают, например, `x = 2 * 2 + 1`, то сначала выполнится арифметика, а уже затем произойдёт присваивание `=` с сохранением результата в `x`. @@ -287,7 +288,7 @@ n *= 2; // теперь n = 14 (работает как n = n * 2) alert( n ); // 14 ``` -Подобные краткие формы записи существуют для всех арифметических и побитовых операторов: `/=`, `-=` и так далее. +Подобные краткие формы записи существуют для **всех** арифметических и побитовых операторов: `/=`, `-=`, `**=` и так далее. Вызов с присваиванием имеет в точности такой же приоритет, как обычное присваивание, то есть выполнится после большинства других операций: @@ -428,7 +429,9 @@ counter++; - RIGHT SHIFT(правый сдвиг) ( `>>` ) - ZERO-FILL RIGHT SHIFT(правый сдвиг с заполнением нулями) ( `>>>` ) -Они используются редко, когда возникает необходимость оперировать с числами на очень низком (побитовом) уровне. В ближайшем времени они нам не понадобятся, так как веб-разработчики редко к ним прибегают, хотя в некоторых сферах (например, в криптографии) они полезны. Можете прочитать [раздел о них](https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Expressions_and_Operators#Битовые_поразрядные_операторы) на MDN, когда возникнет реальная необходимость. +Они используются редко, когда возникает необходимость оперировать с числами на очень низком (побитовом) уровне. В ближайшем времени они нам не понадобятся, так как веб-разработчики редко к ним прибегают, хотя в некоторых сферах (например, в криптографии) они полезны. + +Вы можете прочитать о них в главе , когда возникнет реальная необходимость. ## Оператор "запятая" @@ -448,12 +451,22 @@ alert( a ); // 7 (результат вычисления 3 + 4) Первое выражение `1 + 2` выполняется, а результат отбрасывается. Затем идёт `3 + 4`, выражение выполняется и возвращается результат. -```smart header="Запятая имеет очень низкий приоритет" +````smart header="Запятая имеет очень низкий приоритет" Пожалуйста, обратите внимание, что оператор `,` имеет очень низкий приоритет, ниже `=`, поэтому скобки важны в приведённом выше примере. -Без них в `a = 1 + 2, 3 + 4` сначала выполнится `+`, суммируя числа в `a = 3, 7`, затем оператор присваивания `=` присвоит `a = 3`, а то, что идёт дальше, будет игнорировано. Всё так же, как в `(a = 1 + 2), 3 + 4`. +Попробуйте запустить следующий код (**строгий режим `"use strict"` в примере ниже не используется, иначе мы бы получили ошибку**): + +```js run no-strict +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/09-comparison/article.md b/1-js/02-first-steps/09-comparison/article.md index ddb3b9fddc..ced30d93d8 100644 --- a/1-js/02-first-steps/09-comparison/article.md +++ b/1-js/02-first-steps/09-comparison/article.md @@ -200,7 +200,7 @@ alert( undefined == 0 ); // false (3) ### Как избежать проблем -Зачем мы рассмотрели все эти примеры? Должны ли мы постоянно помнить обо всех этих особенностях? Не обязательно. Со временем все они станут вам знакомы, но можно избежать проблем, если следовать надёжным правилам: +Зачем мы рассмотрели все эти примеры? Должны ли мы постоянно помнить обо всех этих особенностях? Необязательно. Со временем все они станут вам знакомы, но можно избежать проблем, если следовать надёжным правилам: - Относитесь очень осторожно к любому сравнению с `undefined/null`, кроме случаев строгого равенства `===`. - Не используйте сравнения `>= > < <=` с переменными, которые могут принимать значения `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 9980c800b4..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 @@ -НачалоНе знаете? “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/article.md b/1-js/02-first-steps/10-ifelse/article.md index 1799b27d3c..598a1d25b5 100644 --- a/1-js/02-first-steps/10-ifelse/article.md +++ b/1-js/02-first-steps/10-ifelse/article.md @@ -29,7 +29,7 @@ if (year == 2015) { } ``` -Мы рекомендуем использовать фигурные скобки `{}` всегда, когда вы используете инструкцию `if`, даже если выполняется только одна команда. Это улучшает читабельность кода. +Мы рекомендуем использовать фигурные скобки `{}` всегда, когда вы используете инструкцию `if`, даже если выполняется только одна команда. Это улучшает читаемость кода. ## Преобразование к логическому типу @@ -84,7 +84,7 @@ if (year == 2015) { ## Несколько условий: "else if" -Иногда, нужно проверить несколько вариантов условия. Для этого используется блок `else if`. +Иногда нужно проверить несколько вариантов условия. Для этого используется блок `else if`. Например: @@ -153,7 +153,7 @@ let accessAllowed = (age > 18) ? true : false; let accessAllowed = age > 18 ? true : false; ``` -Но скобки делают код более читабельным, поэтому мы рекомендуем их использовать. +Но скобки делают код более простым для восприятия, поэтому мы рекомендуем их использовать. ````smart В примере выше вы можете избежать использования оператора вопросительного знака `?`, т.к. сравнение само по себе уже возвращает `true/false`: @@ -221,7 +221,7 @@ let company = prompt('Какая компания создала JavaScript?', ' **Не рекомендуется использовать оператор вопросительного знака таким образом.** -Несмотря на то, что такая запись короче, чем эквивалентная инструкция `if`, она менее читабельна. +Несмотря на то, что такая запись короче, чем эквивалентная инструкция `if`, она хуже читается. Вот, для сравнения, тот же код, использующий `if`: 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/10-check-login/ifelse_task.svg similarity index 100% rename from 1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg rename to 1-js/02-first-steps/11-logical-operators/10-check-login/ifelse_task.svg 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/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/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 3337e324d5..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. **Сокращённое вычисление.** + В первой строке оператор ИЛИ `||` останавливает выполнение сразу после того, как сталкивается с истинным значением (`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-coalescing-operator/article.md b/1-js/02-first-steps/12-nullish-coalescing-operator/article.md deleted file mode 100644 index 9160c2cb02..0000000000 --- a/1-js/02-first-steps/12-nullish-coalescing-operator/article.md +++ /dev/null @@ -1,166 +0,0 @@ -# Оператор объединения с null '??' - -[recent browser="new"] - -В этой статье мы будем говорить, что значение выражения "определено", если оно отличается от `null` или `undefined`. - -Оператор объединения с null представляет собой два вопросительных знака `??`. - -Результат выражения `a ?? b` будет следующим: -- `a`, если значение `a` определено, -- `b`, если значение `a` не определено. - -То есть оператор `??` возвращает первый аргумент, если он не `null/undefined`, иначе второй. - -Оператор объединения с null не является чем-то принципиально новым. Это всего лишь удобный синтаксис, как из двух значений получить одно "определённое". - -Вот как можно переписать выражение `result = a ?? b`, используя уже знакомые нам операторы: - -```js -result = (a !== null && a !== undefined) ? a : b; -``` - -Как правило, оператор `??` нужен для того, чтобы задать значение по умолчанию для потенциально неопределённой переменной. - -Например, в следующем примере, если переменная `user` не определена, покажем модальное окно с надписью `Аноним`: - -```js run -let user; - -alert(user ?? "Аноним"); // Аноним -``` - -Конечно, если бы переменная `user` содержала любое значение, кроме `null/undefined`, то мы бы увидели его: - -```js run -let user = "Иван"; - -alert(user ?? "Аноним"); // Иван -``` - -Кроме этого, можно записать последовательность из операторов `??`, чтобы получить первое значение из списка, которое не является `null/undefined`. - -Допустим, у нас есть данные пользователя в переменных `firstName`, `lastName` или `nickName`. Все они могут быть неопределёнными, если отсутствует соответствующая информация. - -Выведем имя пользователя, используя одну из этих переменных, а в случае если все они не определены, то покажем "Аноним". - -Для этого воспользуемся оператором `??`: - -```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, поэтому ранее для решения похожих задач разработчики использовали именно его. - -С другой стороны, сравнительно недавно в язык был добавлен оператор объединения с null `??` как раз потому, что многие были недовольны оператором `||`. - -Важное различие между ними заключается в том, что: -- `||` возвращает первое *истинное* значение. -- `??` возвращает первое *определённое* значение. - -Проще говоря, оператор `||` не различает `false`, `0`, пустую строку `""` и `null/undefined`. Для него они все одинаковые, т.е. являются ложными значениями. Если первым аргументом для оператора `||` будет любое из перечисленных значений, то в качестве результата мы получим второй аргумент. - -Однако на практике часто требуется использовать значение по умолчанию только тогда, когда переменная является `null/undefined`. Ведь именно тогда значение действительно неизвестно/не определено. - -Например, рассмотрим следующий пример: - -```js run -let height = 0; - -alert(height || 100); // 100 -alert(height ?? 100); // 0 -``` - -- `height || 100` проверяет, имеет ли переменная `height` ложное значение, что так и есть, - - поэтому результатом является второй аргумент, т.е. `100`. -- `height ?? 100` проверяет, что переменная `height` содержит `null/undefined`, а поскольку это не так, - - то результатом является сама переменная `height`, т.е. `0`. - -Если нулевая высота является "нормальным" значением, которое не должно заменяться значением по умолчанию, то оператор `??` делает как раз то, что нужно. - -## Приоритет - -Оператор `??` имеет довольно низкий приоритет: `5`, согласно [таблице на 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 `??` — это быстрый способ выбрать первое "определённое" значение из списка. - - Используется для присвоения переменным значений по умолчанию: - - ```js - // будет height=100, если переменная height равна null или undefined - height = height ?? 100; - ``` - -- Оператор `??` имеет очень низкий приоритет, лишь немного выше, чем у `?` и `=`, поэтому при использовании его в выражении, скорее всего, потребуются скобки. -- Запрещено использовать вместе с `||` или `&&` без явно указанных круглых скобок. 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/13-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md index 050b1ad13e..2f053d259d 100644 --- a/1-js/02-first-steps/13-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/13-while-for/article.md b/1-js/02-first-steps/13-while-for/article.md index 7ba0c3740d..b7f3d4f67c 100644 --- a/1-js/02-first-steps/13-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/14-switch/article.md b/1-js/02-first-steps/14-switch/article.md index 551d990f8f..7f356dc2da 100644 --- a/1-js/02-first-steps/14-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/15-function-basics/4-pow/solution.md b/1-js/02-first-steps/15-function-basics/4-pow/solution.md index 4375dab037..a374abc83e 100644 --- a/1-js/02-first-steps/15-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/15-function-basics/4-pow/task.md b/1-js/02-first-steps/15-function-basics/4-pow/task.md index de3b84475f..e4e513c313 100644 --- a/1-js/02-first-steps/15-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/15-function-basics/article.md b/1-js/02-first-steps/15-function-basics/article.md index b24625a5e6..4f3bacf687 100644 --- a/1-js/02-first-steps/15-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/16-function-expressions/article.md b/1-js/02-first-steps/16-function-expressions/article.md index 18472035b2..795217fc39 100644 --- a/1-js/02-first-steps/16-function-expressions/article.md +++ b/1-js/02-first-steps/16-function-expressions/article.md @@ -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)` с тремя параметрами: @@ -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, могут быть вызваны раньше своих определений. Например, так будет работать: @@ -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,17 +365,17 @@ welcome(); // теперь всё в порядке Также функции вида `function f(…) {…}` чуть более заметны в коде, чем `let f = function(…) {…}`. Function Declaration легче "ловятся глазами". -...Но если Function Declaration нам не подходит по какой-то причине (мы рассмотрели это в примере выше), то можно использовать объявление при помощи Function Expression. +...Но если Function Declaration нам не подходит по какой-то причине, или нам нужно условное объявление (мы рассмотрели это в примере выше), то следует использовать Function Expression. ``` ## Итого - Функции - это значения. Они могут быть присвоены, скопированы или объявлены в любом месте кода. -- Если функция объявлена как отдельная инструкция в основном потоке кода, то это Function Declaration. -- Если функция была создана как часть выражения, то считается, что эта функция объявлена при помощи Function Expression. +- Если функция объявлена как отдельная инструкция в основном потоке кода, то это “Function Declaration”. +- Если функция была создана как часть выражения, то это “Function Expression”. - Function Declaration обрабатываются перед выполнением блока кода. Они видны во всём блоке. -- Функции, объявленные при помощи Function Expression, создаются, только когда поток выполнения достигает их. +- Функции, объявленные при помощи Function Expression, создаются только когда поток выполнения достигает их. -В большинстве случаев, когда нам нужно создать функцию, предпочтительно использовать Function Declaration, т.к. функция будет видима до своего объявления в коде. Это позволяет более гибко организовывать код, и улучшает его читаемость. +В большинстве случаев, когда нам нужно объявить функцию, Function Declaration предпочтительнее, т.к функция будет видна до своего объявления в коде. Это даёт нам больше гибкости в организации кода, и, как правило, делает его более читабельным. -Таким образом, мы должны прибегать к объявлению функций при помощи Function Expression в случае, когда синтаксис Function Declaration не подходит для нашей задачи. Мы рассмотрели несколько таких примеров в этой главе, и рассмотрим их ещё больше в будущем. +Исходя из этого, мы должны использовать Function Expression только тогда, когда Function Declaration не подходит для нашей задачи. Мы рассмотрели несколько таких примеров в этой главе, и увидим ещё больше в будущем. 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 index 374f6d9172..25bc971ce9 100644 --- a/1-js/02-first-steps/17-arrow-functions-basics/article.md +++ b/1-js/02-first-steps/17-arrow-functions-basics/article.md @@ -1,16 +1,16 @@ -# Функции-стрелки, основы +# Стрелочные функции, основы -Существует ещё более простой и краткий синтаксис для создания функций, который часто лучше, чем синтаксис Function Expression. +Существует ещё один очень простой и лаконичный синтаксис для создания функций, который часто лучше, чем Function Expression. Он называется "функции-стрелки" или "стрелочные функции" (arrow functions), т.к. выглядит следующим образом: ```js -let func = (arg1, arg2, ...argN) => expression +let func = (arg1, arg2, ...argN) => expression; ``` -...Такой код создаёт функцию `func` с аргументами `arg1..argN` и вычисляет `expression` с правой стороны с их использованием, возвращая результат. +Это создаёт функцию `func`, которая принимает аргументы `arg1..argN`, затем вычисляет `expression` в правой части с их использованием и возвращает результат. -Другими словами, это более короткий вариант такой записи: +Другими словами, это сокращённая версия: ```js let func = function(arg1, arg2, ...argN) { @@ -18,12 +18,12 @@ let func = function(arg1, arg2, ...argN) { }; ``` -Давайте взглянем на конкретный пример: +Давайте рассмотрим конкретный пример: ```js run let sum = (a, b) => a + b; -/* Более короткая форма для: +/* Эта стрелочная функция представляет собой более короткую форму: let sum = function(a, b) { return a + b; @@ -34,21 +34,20 @@ alert( sum(1, 2) ); // 3 ``` -То есть, `(a, b) => a + b` задаёт функцию с двумя аргументами `a` и `b`, которая при запуске вычисляет выражение справа `a + b` и возвращает его результат. +Как вы можете видеть, `(a, b) => a + b` задаёт функцию, которая принимает два аргумента с именами `a` и `b`. И при выполнении она вычисляет выражение `a + b` и возвращает результат. - Если у нас только один аргумент, то круглые скобки вокруг параметров можно опустить, сделав запись ещё короче: ```js run - // тоже что и - // let double = function(n) { return n * 2 } *!* let double = n => n * 2; + // примерно то же, что и: let double = function(n) { return n * 2 } */!* alert( double(3) ); // 6 ``` -- Если нет аргументов, указываются пустые круглые скобки: +- Если аргументов нет, круглые скобки будут пустыми, но они должны присутствовать: ```js run let sayHi = () => alert("Hello!"); @@ -56,7 +55,7 @@ alert( sum(1, 2) ); // 3 sayHi(); ``` -Функции-стрелки могут быть использованы так же, как и Function Expression. +Стрелочные функции можно использовать так же, как и Function Expression. Например, для динамического создания функции: @@ -64,46 +63,50 @@ alert( sum(1, 2) ); // 3 let age = prompt("Сколько Вам лет?", 18); let welcome = (age < 18) ? - () => alert('Привет') : + () => alert('Привет!') : () => alert("Здравствуйте!"); -welcome(); // теперь всё в порядке +welcome(); ``` -Поначалу функции-стрелки могут показаться необычными и трудночитаемыми, но это быстро пройдёт, как только глаза привыкнут к этим конструкциям. +Поначалу стрелочные функции могут показаться необычными и даже трудночитаемыми, но это быстро пройдёт по мере того, как глаза привыкнут к этим конструкциям. -Они очень удобны для простых однострочных действий, когда лень писать много букв. +Они очень удобны для простых однострочных действий, когда лень писать много слов. ## Многострочные стрелочные функции -В примерах выше аргументы использовались слева от `=>`, а справа вычислялось выражение с их значениями. +Стрелочные функции, которые мы видели до этого, были очень простыми. Они брали аргументы слева от `=>` и вычисляли и возвращали выражение справа. -Порой нам нужно что-то посложнее, например, выполнить несколько инструкций. Это также возможно, нужно лишь заключить инструкции в фигурные скобки. И использовать `return` внутри них, как в обычной функции. +Иногда нам нужна более сложная функция, с несколькими выражениями и инструкциями. Это также возможно, нужно лишь заключить их в фигурные скобки. При этом важное отличие - в том, что в таких скобках для возврата значения нужно использовать `return` (как в обычных функциях). -Например: +Вроде этого: ```js run let sum = (a, b) => { // фигурная скобка, открывающая тело многострочной функции let result = a + b; *!* - return result; // при фигурных скобках для возврата значения нужно явно вызвать return + return result; // если мы используем фигурные скобки, то нам нужно явно указать "return" */!* }; alert( sum(1, 2) ); // 3 ``` -```smart header="Дальше будет ещё информация" -Здесь мы рассмотрели функции-стрелки как способ писать меньше букв. Но это далеко не всё! +```smart header="Дальше - больше" +Здесь мы представили главной целью стрелочных функций краткость. Но это ещё не всё! -Стрелочные функции обладают другими интересными особенностями. Их изучение требует знания некоторых других возможностей языка JavaScript, поэтому мы вернёмся к стрелочным функциям позже, в главе . +Стрелочные функции обладают и другими интересными возможностями. + +Чтобы изучить их более подробно, нам сначала нужно познакомиться с некоторыми другими аспектами JavaScript, поэтому мы вернёмся к стрелочным функциям позже, в главе . А пока мы можем использовать их для простых однострочных действий и колбэков. ``` ## Итого -Функции-стрелки очень удобны для однострочных действий. Они бывают двух типов: +Стрелочные функции очень удобны для простых действий, особенно для однострочных. + +Они бывают двух типов: -1. Без фигурных скобок: `(...args) => expression` -- правая сторона выражение: функция выполняет его и возвращает результат. -2. С фигурными скобками: `(...args) => { body }` -- скобки позволяют нам писать многострочные инструкции внутри функции, но при этом необходимо указывать директиву `return`, чтобы вернуть какое-либо значение. +1. Без фигурных скобок: `(...args) => expression` -- правая сторона выражения: функция вычисляет его и возвращает результат. Скобки можно не ставить, если аргумент только один: `n => n * 2`. +2. С фигурными скобками: `(...args) => { body }` -- скобки позволяют нам писать несколько инструкций внутри функции, но при этом необходимо явно вызывать `return`, чтобы вернуть значение. diff --git a/1-js/02-first-steps/18-javascript-specials/article.md b/1-js/02-first-steps/18-javascript-specials/article.md index 1daaa3aef3..4bd0206e36 100644 --- a/1-js/02-first-steps/18-javascript-specials/article.md +++ b/1-js/02-first-steps/18-javascript-specials/article.md @@ -31,7 +31,7 @@ alert("После этого сообщения ждите ошибку") ```js function f() { - // после объявления функции необязательно ставить точку с запятой + // после объявления функции необязательно ставить точку с запятой } for(;;) { @@ -152,6 +152,9 @@ JavaScript поддерживает следующие операторы: Логические операторы : Логические И `&&`, ИЛИ `||` используют так называемое "ленивое вычисление" и возвращают значение, на котором оно остановилось (не обязательно `true` или `false`). Логическое НЕ `!` конвертирует операнд в логический тип и возвращает инвертированное значение. +Оператор нулевого слияния +: Оператор `??` предоставляет способ выбора определённого значения из списка переменных. Результатом `a ?? b` будет `a`, если только оно не равно `null/undefined`, тогда `b`. + Сравнение : Проверка на равенство `==` значений разных типов конвертирует их в число (за исключением `null` и `undefined`, которые могут равняться только друг другу), так что примеры ниже равны: @@ -171,7 +174,7 @@ JavaScript поддерживает следующие операторы: Другие операторы : Существуют и другие операторы, такие как запятая. -Подробности: , , . +Подробности: , , , . ## Циклы 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/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 ba57e624a3..e7c3974760 100644 --- a/1-js/03-code-quality/02-coding-style/article.md +++ b/1-js/03-code-quality/02-coding-style/article.md @@ -109,7 +109,7 @@ if ( Существует два типа отступов: -- **Горизонтальные отступы: два или четыре пробела.** +- **Горизонтальные отступы: 2 или 4 пробела.** Горизонтальный отступ выполняется с помощью 2 или 4 пробелов, или символа табуляции (клавиша `key:Tab`). Какой из них выбрать - это уже на ваше усмотрение. Пробелы больше распространены. @@ -300,17 +300,17 @@ function pow(x, n) { Вот некоторые известные инструменты для проверки: -- [JSLint](http://www.jslint.com/) -- проверяет код на соответствие [стилю JSLint](http://jslint.com/index.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 23eff7f6b4..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 f220e00859..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="Осторожно, ирония!" -Многие пытались пройти по пути ниндзя. Мало, кто преуспел. +Многие пытались пройти по пути ниндзя! Мало кто преуспел. ``` ## Краткость – сестра таланта! @@ -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 80023c71e6..9279fcd7b3 100644 --- a/1-js/03-code-quality/05-testing-mocha/article.md +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -69,7 +69,7 @@ describe("pow", function() { 1. Пишется начальная спецификация с тестами, проверяющими основную функциональность. 2. Создаётся начальная реализация. -3. Для запуска тестов мы используем фреймворк [Mocha](http://mochajs.org/) (подробнее о нём чуть позже). Пока функция не готова, будут ошибки. Вносим изменения до тех пор, пока всё не начнёт работать так, как нам нужно. +3. Для запуска тестов мы используем фреймворк [Mocha](https://mochajs.org/) (подробнее о нём чуть позже). Пока функция не готова, будут ошибки. Вносим изменения до тех пор, пока всё не начнёт работать так, как нам нужно. 4. Теперь у нас есть правильно работающая начальная реализация и тесты. 5. Мы добавляем новые способы использования в спецификацию, возможно, ещё не реализованные в тестируемом коде. Тесты начинают "падать" (выдавать ошибки). 6. Возвращаемся на шаг 3, дописываем реализацию до тех пор, пока тесты не начнут завершаться без ошибок. @@ -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/) -- библиотека, позволяющая наблюдать за функциями, эмулировать встроенные функции и многое другое. Нам она пригодится позднее. Эти библиотеки подходят как для тестирования внутри браузера, так и на стороне сервера. Мы рассмотрим вариант с браузером. @@ -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`: 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/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 8c1b2e9f57..3c5474b851 100644 --- a/1-js/04-object-basics/01-object/article.md +++ b/1-js/04-object-basics/01-object/article.md @@ -83,6 +83,7 @@ let user = { ![](object-user-props.svg) Последнее свойство объекта может заканчиваться запятой: + ```js let user = { name: "John", @@ -205,6 +206,7 @@ alert( bag.apple ); // 5, если fruit="apple" И если посетитель введёт слово `"apple"`, то в объекте `bag` теперь будет лежать свойство `{apple: 5}`. По сути, пример выше работает так же, как и следующий пример: + ```js run let fruit = prompt("Какой фрукт купить?", "apple"); let bag = {}; @@ -359,7 +361,7 @@ alert( *!*key*/!* in user ); // true, имя свойства было взят Для чего вообще нужен оператор `in`? Разве недостаточно сравнения с `undefined`? -В большинстве случаев прекрасно сработает сравнение с `undefined`. Но есть особый случай, когда оно не подходит, и нужно использовать `"in"`. +В большинстве случаев прекрасно сработает сравнение с `undefined`. Но есть особый случай, когда оно не подходит и нужно использовать `"in"`. Это когда свойство существует, но содержит значение `undefined`: @@ -374,9 +376,9 @@ alert( "test" in obj ); // true, свойство существует! В примере выше свойство `obj.test` технически существует в объекте. Оператор `in` сработал правильно. -Подобные ситуации случаются очень редко, так как `undefined` обычно явно не присваивается. Для "неизвестных" или "пустых" свойств мы используем значение `null`. Таким образом, оператор `in` является экзотическим гостем в коде. +Подобные ситуации случаются очень редко, так как `undefined` обычно явно не присваивается. Для "неизвестных" или "пустых" свойств мы используем значение `null`. -## Цикл "for..in" +## Цикл "for..in" [#forin] Для перебора всех свойств объекта используется цикл `for..in`. Этот цикл отличается от изученного ранее цикла `for(;;)`. diff --git a/1-js/04-object-basics/02-object-copy/article.md b/1-js/04-object-basics/02-object-copy/article.md index fa1e4d05a9..e648d34098 100644 --- a/1-js/04-object-basics/02-object-copy/article.md +++ b/1-js/04-object-basics/02-object-copy/article.md @@ -1,10 +1,13 @@ + # Копирование объектов и ссылки -Одним из фундаментальных отличий объектов от примитивных типов данных является то, что они хранятся и копируются "по ссылке". +Одно из фундаментальных отличий объектов от примитивов заключается в том, что объекты хранятся и копируются "по ссылке", тогда как примитивные значения: строки, числа, логические значения и т.д. – всегда копируются "как целое значение". -Примитивные типы: строки, числа, логические значения - присваиваются и копируются "по значению". +Это легко понять, если мы немного заглянем под капот того, что происходит, когда мы копируем значение. -Например: +Давайте начнём с примитива, такого как строка. + +Здесь мы помещаем копию `message` во `phrase`: ```js let message = "Привет!"; @@ -15,32 +18,38 @@ let phrase = message; ![](variable-copy-value.svg) +Вполне очевидный результат, не так ли? Объекты ведут себя иначе. -**Переменная хранит не сам объект, а его "адрес в памяти", другими словами "ссылку" на него.** +**Переменная, которой присвоен объект, хранит не сам объект, а его "адрес в памяти" – другими словами, "ссылку" на него.** -Проиллюстрируем это: +Давайте рассмотрим пример такой переменной: ```js let user = { - name: "Иван" + name: "John" }; ``` +И вот как это на самом деле хранится в памяти: + ![](variable-contains-reference.svg) -Сам объект хранится где-то в памяти. А в переменной `user` лежит "ссылка" на эту область памяти. +Объект хранится где-то в памяти (справа от изображения), в то время как переменная `user` (слева) имеет лишь "ссылку" на него. -**Когда переменная объекта копируется - копируется ссылка, сам же объект не дублируется.** +Мы можем думать о переменной объекта, такой как `user`, как о листе бумаги с адресом объекта на нем. -Если мы представляем объект как ящик, то переменная – это ключ к нему. Копирование переменной дублирует ключ, но не сам ящик. +Когда мы выполняем действия с объектом, к примеру, берём свойство `user.name `, движок JavaScript просматривает то, что находится по этому адресу, и выполняет операцию с самим объектом. -Например: +Теперь вот почему это важно. + +**При копировании переменной объекта копируется ссылка, но сам объект не дублируется.** +Например: ```js no-beautify -let user = { name: "Иван" }; +let user = { name: "John" }; let admin = user; // копируется ссылка ``` @@ -49,39 +58,39 @@ let admin = user; // копируется ссылка ![](variable-copy-reference.svg) -Мы можем использовать любую из переменных для доступа к ящику и изменения его содержимого: +Как вы можете видеть, все ещё есть один объект, но теперь с двумя переменными, которые ссылаются на него. + +Мы можем использовать любую переменную для доступа к объекту и изменения его содержимого: ```js run -let user = { name: 'Иван' }; +let user = { name: 'John' }; let admin = user; *!* -admin.name = 'Петя'; // изменено по ссылке из переменной "admin" +admin.name = 'Pete'; // изменено по ссылке из переменной "admin" */!* -alert(*!*user.name*/!*); // 'Петя', изменения видны по ссылке из переменной "user" +alert(*!*user.name*/!*); // 'Pete', изменения видны по ссылке из переменной "user" ``` -Приведённый выше пример демонстрирует, что объект только один. Как если бы у нас был один ящик с двумя ключами и мы использовали один из них (`admin`), чтобы войти в него и что-то изменить, а затем, открыв ящик другим ключом (`user`), мы бы увидели эти изменения. +Это как если бы у нас был шкафчик с двумя ключами, и мы использовали один из них (`admin`), чтобы войти в него и внести изменения. А затем, если мы позже используем другой ключ (`user`), мы все равно открываем тот же шкафчик и можем получить доступ к изменённому содержимому. ## Сравнение по ссылке -Операторы равенства `==` и строгого равенства `===` для объектов работают одинаково. - -**Два объекта равны только в том случае, если это один и тот же объект.** +Два объекта равны только в том случае, если это один и тот же объект. -В примере ниже две переменные ссылаются на один и тот же объект, поэтому они равны друг другу: +Например, здесь `a` и `b` ссылаются на один и тот же объект, поэтому они равны: ```js run let a = {}; let b = a; // копирование по ссылке -alert( a == b ); // true, т.к. обе переменные ссылаются на один и тот же объект +alert( a == b ); // true, обе переменные ссылаются на один и тот же объект alert( a === b ); // true ``` -В другом примере два разных объекта не равны, хотя оба пусты: +И здесь два независимых объекта не равны, даже если они выглядят одинаково (оба пусты): ```js run let a = {}; @@ -90,42 +99,42 @@ let b = {}; // два независимых объекта alert( a == b ); // false ``` -Для сравнений типа `obj1 > obj2` или для сравнения с примитивом `obj == 5` объекты преобразуются в примитивы. Мы скоро изучим, как работают такие преобразования объектов, но, по правде говоря, сравнения такого рода необходимы очень редко и обычно являются результатом ошибки программиста. +Для сравнений типа `obj1 > obj2` или для сравнения с примитивом `obj == 5` объекты преобразуются в примитивы. Очень скоро мы изучим, как работают преобразования объектов, но, по правде говоря, такие сравнения требуются очень редко и обычно они появляются в результате ошибок программиста. -## Клонирование и объединение объектов, Object.assign +## Клонирование и объединение, Object.assign [#cloning-and-merging-object-assign] -Таким образом, при копировании переменной с объектом создаётся ещё одна ссылка на тот же самый объект. +Итак, копирование объектной переменной создаёт ещё одну ссылку на тот же объект. Но что, если нам всё же нужно дублировать объект? Создать независимую копию, клон? -Это выполнимо, но немного сложно, так как в JavaScript нет встроенного метода для этого. На самом деле, такая нужда возникает редко. В большинстве случаев нам достаточно копирования по ссылке. +Это тоже выполнимо, но немного сложнее, потому что в JavaScript для этого нет встроенного метода. Но на самом деле в этом редко возникает необходимость, копирования по ссылке в большинстве случаев вполне хватает. -Но если мы действительно этого хотим, то нам нужно создавать новый объект и повторять структуру дублируемого объекта, перебирая его свойства и копируя их. +Но если мы действительно этого хотим, то нам нужно создать новый объект и воспроизвести структуру существующего, перебрав его свойства и скопировав их на примитивном уровне. Например так: ```js run let user = { - name: "Иван", + name: "John", age: 30 }; *!* let clone = {}; // новый пустой объект -// скопируем все свойства user в него +// давайте скопируем все свойства user в него for (let key in user) { clone[key] = user[key]; } */!* -// теперь в переменной clone находится абсолютно независимый клон объекта -clone.name = "Пётр"; // изменим в нём данные +// теперь clone это полностью независимый объект с тем же содержимым +clone.name = "Pete"; // изменим в нём данные -alert( user.name ); // в оригинальном объекте значение свойства `name` осталось прежним – Иван. +alert( user.name ); // все ещё John в первоначальном объекте ``` -Кроме того, для этих целей мы можем использовать метод [Object.assign](mdn:js/Object/assign). +Также мы можем использовать для этого метод [Object.assign](mdn:js/Object/assign). Синтаксис: @@ -134,13 +143,13 @@ Object.assign(dest, [src1, src2, src3...]) ``` - Первый аргумент `dest` — целевой объект. -- Остальные аргументы `src1, ..., srcN` (может быть столько, сколько нужно) являются исходными объектами -- Метод копирует свойства всех исходных объектов `src1, ..., srcN` в целевой объект `dest`. То есть, свойства всех перечисленных объектов, начиная со второго, копируются в первый объект. +- Остальные аргументы `src1, ..., srcN` (может быть столько, сколько необходимо) являются исходными объектами +- Метод копирует свойства всех исходных объектов `src1, ..., srcN` в целевой объект `dest`. Другими словами, свойства всех аргументов, начиная со второго, копируются в первый объект. - Возвращает объект `dest`. -Например, объединим несколько объектов в один: +Например, мы можем использовать его для объединения нескольких объектов в один: ```js -let user = { name: "Иван" }; +let user = { name: "John" }; let permissions1 = { canView: true }; let permissions2 = { canEdit: true }; @@ -150,24 +159,24 @@ let permissions2 = { canEdit: true }; Object.assign(user, permissions1, permissions2); */!* -// теперь user = { name: "Иван", canView: true, canEdit: true } +// теперь user = { name: "John", canView: true, canEdit: true } ``` -Если принимающий объект (`user`) уже имеет свойство с таким именем, оно будет перезаписано: +Если скопированное имя свойства уже существует, оно будет перезаписано: ```js run -let user = { name: "Иван" }; +let user = { name: "John" }; -Object.assign(user, { name: "Пётр" }); +Object.assign(user, { name: "Pete" }); -alert(user.name); // теперь user = { name: "Пётр" } +alert(user.name); // теперь user = { name: "Pete" } ``` -Мы также можем использовать `Object.assign` для замены `for..in` на простое клонирование: +Мы также можем использовать `Object.assign` для замены цикла `for..in ` для простого клонирования: ```js let user = { - name: "Иван", + name: "John", age: 30 }; @@ -176,16 +185,18 @@ let clone = Object.assign({}, user); */!* ``` -Этот метод скопирует все свойства объекта `user` в пустой объект и возвратит его. +Он копирует все свойства `user` в пустой объект и возвращает его. + +Также существуют и другие методы клонирования объекта. Например, с использованием [оператора расширения](info:rest-parameters-spread-operator) `clone = {...user}`, рассмотренного далее в учебнике. ## Вложенное клонирование -До сих пор мы предполагали, что все свойства объекта `user` хранят примитивные значения. Но свойства могут быть ссылками на другие объекты. Что с ними делать? +До сих пор мы предполагали, что все свойства `user` примитивныe. Но свойства могут быть и ссылками на другие объекты. Что с ними делать? Например, есть объект: ```js run let user = { - name: "Иван", + name: "John", sizes: { height: 182, width: 50 @@ -195,11 +206,11 @@ let user = { alert( user.sizes.height ); // 182 ``` -Теперь при клонировании недостаточно просто скопировать `clone.sizes = user.sizes`, поскольку `user.sizes` - это объект, он будет скопирован по ссылке. А значит объекты `clone` и `user` в своих свойствах `sizes` будут ссылаться на один и тот же объект: +Теперь недостаточно просто скопировать `clone.sizes = user.sizes`, потому что `user.sizes` - это объект, он будет скопирован по ссылке. Таким образом, `clone` и `user` будут иметь общий объект `sizes`: ```js run let user = { - name: "Иван", + name: "John", sizes: { height: 182, width: 50 @@ -208,21 +219,47 @@ let user = { let clone = Object.assign({}, user); -alert( user.sizes === clone.sizes ); // true, один и тот же объект +alert( user.sizes === clone.sizes ); // true, тот же объект -// user и clone обращаются к одному sizes -user.sizes.width++; // меняем свойство в одном объекте -alert(clone.sizes.width); // 51, видим результат в другом объекте +// user и clone обладают общим свойством sizes +user.sizes.width++; // изменяем свойства в первом объекте +alert(clone.sizes.width); // 51, видим результат в другом ``` -Чтобы исправить это, мы должны в цикле клонирования делать проверку, не является ли значение `user[key]` объектом, и если это так - скопировать и его структуру тоже. Это называется "глубокое клонирование". +Чтобы исправить это, мы должны использовать цикл клонирования, который проверяет каждое значение `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=...` в целом. -Мы можем реализовать глубокое клонирование, используя рекурсию. Или, чтобы не изобретать велосипед, использовать готовую реализацию — метод [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) из JavaScript-библиотеки [lodash](https://lodash.com). +Тем не менее, если нам действительно нужно создать постоянные свойства объекта, это тоже возможно, но с использованием совершенно других методов. Мы затронем это в главе . +```` ## Итого -Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не "значение объекта", а "ссылку" (адрес в памяти) на это значение. Поэтому копирование такой переменной или передача её в качестве аргумента функции приводит к копированию этой ссылки, а не самого объекта. +Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не "значение объекта", а "ссылку" (адрес в памяти) на это значение. Таким образом, копирование такой переменной или передача её в качестве аргумента функции копирует эту ссылку, а не сам объект. -Все операции с использованием скопированных ссылок (например, добавление или удаление свойств) выполняются с одним и тем же объектом. +Все операции с использованием скопированных ссылок (например, добавление/удаление свойств) выполняются с одним и тем же объектом. -Для "простого клонирования" объекта можно использовать `Object.assign`. Необходимо помнить, что `Object.assign` не делает глубокое клонирование объекта. Если внутри копируемого объекта есть свойство, значение которого не является примитивом, оно будет передано по ссылке. Для создания "настоящей копии" (полного клона объекта) можно воспользоваться методом из сторонней JavaScript-библиотеки [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). +Чтобы создать "реальную копию" (клон), мы можем использовать `Object.assign` для так называемой "поверхностной копии" (вложенные объекты копируются по ссылке) или функцию "глубокого клонирования", такую как [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). 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 index 0d6ca67bc6..f12e7fc4c8 100644 --- 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 @@ -1 +1 @@ -"Hello!"message"Hello!"phrase \ No newline at end of file +"Привет!"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 index 3882dfe2ec..4c627d2687 100644 --- a/1-js/04-object-basics/03-garbage-collection/article.md +++ b/1-js/04-object-basics/03-garbage-collection/article.md @@ -2,7 +2,7 @@ Управление памятью в JavaScript выполняется автоматически и незаметно. Мы создаём примитивы, объекты, функции... Всё это занимает память. -Но что происходит, когда что-то больше не нужно? Как JavaScript понимает, что пора очищать память? +Но что происходит, когда что-то больше не нужно? Как движок JavaScript обнаруживает, что пора очищать память? ## Достижимость @@ -14,8 +14,8 @@ Например: - - Локальные переменные и параметры текущей функции. - - Переменные и параметры других функций в текущей цепочке вложенных вызовов. + - Выполняемая в данный момент функция, её локальные переменные и параметры. + - Другие функции в текущей цепочке вложенных вызовов, их локальные переменные и параметры. - Глобальные переменные. - (некоторые другие внутренние значения) @@ -23,9 +23,9 @@ 2. Любое другое значение считается достижимым, если оно доступно из корня по ссылке или по цепочке ссылок. - Например, если в локальной переменной есть объект, и он имеет свойство, в котором хранится ссылка на другой объект, то этот объект считается достижимым. И те, на которые он ссылается, тоже достижимы. Далее вы познакомитесь с подробными примерами на эту тему. + Например, если в глобальной переменной есть объект, и он имеет свойство, в котором хранится ссылка на другой объект, то *этот* объект считается достижимым. И те, на которые он ссылается, тоже достижимы. Далее вы познакомитесь с подробными примерами на эту тему. -В интерпретаторе JavaScript есть фоновый процесс, который называется [сборщик мусора](https://ru.wikipedia.org/wiki/Сборка_мусора). Он следит за всеми объектами и удаляет те, которые стали недостижимы. +В движке JavaScript есть фоновый процесс, который называется [сборщиком мусора](https://ru.wikipedia.org/wiki/Сборка_мусора). Он отслеживает все объекты и удаляет те, которые стали недоступными. ## Простой пример @@ -40,7 +40,7 @@ let user = { ![](memory-user-john.svg) -Здесь стрелка обозначает ссылку на объект. Глобальная переменная `user` ссылается на объект `{name: "John"}` (мы будем называть его просто "John"). В свойстве `"name"` объекта John хранится примитив, поэтому оно нарисовано внутри объекта. +Здесь стрелка обозначает ссылку на объект. Глобальная переменная `user` ссылается на объект `{name: "John"}` (мы будем называть его просто "John" для краткости). В свойстве `"name"` объекта John хранится примитив, поэтому оно нарисовано внутри объекта. Если перезаписать значение `user`, то ссылка потеряется: @@ -115,7 +115,7 @@ delete family.mother.husband; ![](family-delete-refs.svg) -Недостаточно удалить только одну из этих ссылок, потому что все объекты останутся достижимыми. +Недостаточно удалить только одну из этих двух ссылок, потому что все объекты останутся достижимыми. Но если мы удалим обе, то увидим, что у объекта John больше нет входящих ссылок: @@ -129,7 +129,7 @@ delete family.mother.husband; ## Недостижимый "остров" -Вполне возможна ситуация, при которой целый "остров" связанных объектов может стать недостижимым и удалиться из памяти. +Вполне возможна ситуация, при которой целый "остров" взаимосвязанных объектов может стать недостижимым и удалиться из памяти. Возьмём объект `family` из примера выше. А затем: @@ -145,25 +145,25 @@ family = null; Объекты John и Ann всё ещё связаны, оба имеют входящие ссылки, но этого недостаточно. -У объекта `family` больше нет ссылки от корня, поэтому весь "остров" становится недостижимым и будет удалён. +Бывший объект `family` был отсоединён от корня, на него больше нет ссылки, поэтому весь "остров" становится недостижимым и будет удалён. ## Внутренние алгоритмы -Основной алгоритм сборки мусора - "алгоритм пометок" (англ. "mark-and-sweep"). +Основной алгоритм сборки мусора называется "алгоритм пометок" (от англ. "mark-and-sweep"). Согласно этому алгоритму, сборщик мусора регулярно выполняет следующие шаги: - Сборщик мусора "помечает" (запоминает) все корневые объекты. -- Затем он идёт по их ссылкам и помечает все найденные объекты. -- Затем он идёт по ссылкам помеченных объектов и помечает объекты, на которые есть ссылка от них. Все объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды. -- ...И так далее, пока не будут посещены все ссылки (достижимые от корней). +- Затем он идёт по ним и "помечает" все ссылки из них. +- Затем он идёт по отмеченным объектам и отмечает их ссылки. Все посещённые объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды. +- ...И так далее, пока не будут посещены все достижимые (из корней) ссылки. - Все непомеченные объекты удаляются. Например, пусть наша структура объектов выглядит так: ![](garbage-collection-1.svg) -Явно виден "недостижимый остров" справа. Теперь посмотрим, как будет работать "алгоритм пометок" сборщика мусора. +Мы ясно видим "недостижимый остров" справа. Теперь давайте посмотрим, как будет работать "алгоритм пометок" сборщика мусора. На первом шаге помечаются корни: @@ -173,40 +173,40 @@ family = null; ![](garbage-collection-3.svg) -...а затем объекты по их ссылкам и так далее, пока это вообще возможно: +...А затем объекты по их ссылкам и так далее, пока это возможно: ![](garbage-collection-4.svg) -Теперь объекты, до которых не удалось дойти от корней, считаются недостижимыми и будут удалены: +Теперь объекты, которые не удалось посетить в процессе, считаются недостижимыми и будут удалены: ![](garbage-collection-5.svg) -Это и есть принцип работы сборки мусора. +Мы также можем представить себе этот процесс как выливание огромного ведра краски из корней, которая течёт по всем ссылкам и отмечает все достижимые объекты. Затем непомеченные удаляются. -Интерпретаторы JavaScript применяют множество оптимизаций, чтобы сборка мусора работала быстрее и не влияла на производительность. +Это концепция того, как работает сборка мусора. Движки JavaScript применяют множество оптимизаций, чтобы она работала быстрее и не задерживала выполнение кода. Вот некоторые из оптимизаций: -- **Сборка по поколениям (Generational collection)** - объекты делятся на "новые" и "старые". Многие объекты появляются, выполняют свою задачу и быстро умирают, их можно удалять более агрессивно. Те, которые живут достаточно долго, становятся "старыми" и проверяются реже. -- **Инкрементальная сборка (Incremental collection)** - если объектов много, то обход всех ссылок и пометка достижимых объектов может занять значительное время и привести к видимым задержкам выполнения скрипта. Поэтому интерпретатор пытается организовать сборку мусора поэтапно. Этапы выполняются по отдельности один за другим. Это требует дополнительного учёта для отслеживания изменений между этапами, но зато теперь у нас есть много крошечных задержек вместо одной большой. +- **Сборка по поколениям (Generational collection)** – объекты делятся на два набора: "новые" и "старые". В типичном коде многие объекты имеют короткую жизнь: они появляются, выполняют свою работу и быстро умирают, так что имеет смысл отслеживать новые объекты и, если это так, быстро очищать от них память. Те, которые выживают достаточно долго, становятся "старыми" и проверяются реже. +- **Инкрементальная сборка (Incremental collection)** – если объектов много, и мы пытаемся обойти и пометить весь набор объектов сразу, это может занять некоторое время и привести к видимым задержкам в выполнении скрипта. Так что движок делит всё множество объектов на части, и далее очищает их одну за другой. Получается несколько небольших сборок мусора вместо одной всеобщей. Это требует дополнительного учёта для отслеживания изменений между частями, но зато получается много крошечных задержек вместо одной большой. - **Сборка в свободное время (Idle-time collection)** - чтобы уменьшить возможное влияние на производительность, сборщик мусора старается работать только во время простоя процессора. -Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось описать их здесь, я должен воздержаться от этого, потому что разные интерпретаторы JavaScript применяют разные приёмы и хитрости. И, что более важно, всё меняется по мере развития интерпретаторов, поэтому углубляться в эту тему заранее, без реальной необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будут полезны некоторые ссылки ниже. +Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось описать их здесь, я должен воздержаться, потому что разные движки реализуют разные хитрости и методы. И, что ещё более важно, все меняется по мере развития движков, поэтому изучать тему глубоко "заранее", без реальной необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будет несколько ссылок ниже. ## Итого -Главное из того, что мы узнали: +Главное, что нужно знать: - Сборка мусора выполняется автоматически. Мы не можем ускорить или предотвратить её. - Объекты сохраняются в памяти, пока они достижимы. -- Наличие ссылки не гарантирует, что объект достижим (от корня): несколько взаимосвязанных объектов могут стать недостижимыми как единое целое. +- Если на объект есть ссылка - вовсе не факт, что он является достижимым (из корня): набор взаимосвязанных объектов может стать недоступен в целом, как мы видели в примере выше. -Современные интерпретаторы реализуют передовые алгоритмы сборки мусора. +Современные движки реализуют разные продвинутые алгоритмы сборки мусора. -Некоторые из них освещены в книге "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones и др.). +О многих из них рассказано в прекрасной книге о сборке мусора "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 находится в статье [A tour of V8: Garbage Collection](https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). -Также в [блоге интерпретатора V8](https://v8.dev/) время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы изучить сборку мусора, вам необходимо понимать, как устроен внутри интерпретатор V8 в целом. Об этом вы можете почитать в блоге [Вячеслава Егорова](http://mrale.ph), одного из инженеров, разрабатывавших V8. Я говорю про "V8", потому что он лучше всего освещён статьями в интернете. В других интерпретаторах многие подходы схожи, но сборка мусора во многих аспектах отличается. +Также в [блоге V8](https://v8.dev/) время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы изучить сборку мусора, вам лучше подготовиться, узнав о том как устроен движок V8 внутри в целом и почитав блог [Вячеслава Егорова](https://mrale.ph), одного из инженеров, разрабатывавших V8. Я говорю про "V8", потому что он лучше всего освещается в статьях в Интернете. Для других движков многие подходы схожи, но сборка мусора отличается во многих аспектах. -Глубокое понимание работы интерпретаторов необходимо, когда вам нужны низкоуровневые оптимизации. Было бы разумно запланировать их изучение как следующий шаг после освоения языка. +Глубокое понимание работы движков полезно, когда вам нужна низкоуровневая оптимизация. Было бы разумно запланировать их изучение как следующий шаг после того, как вы познакомитесь с языком. 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 d9c84f2d30..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,7 +49,7 @@ let user = { }; *!* -// сначала объявляем +// сначала, объявляем function sayHi() { alert("Привет!"); } @@ -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` в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок. - -Здесь мы не будем судить о том, является ли это решение в языке хорошим или плохим. Мы должны понимать, как с этим работать, чтобы получать выгоды и избегать проблем. -``` - -## Внутренняя реализация: Ссылочный тип - -```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)(); // Ошибка! -*/!* -``` +Если вы до этого изучали другие языки программирования, то вы, вероятно, привыкли к идее "фиксированного`this`" - когда методы, определённые в объекте, всегда имеют `this`, ссылающееся на этот объект. -В последней строчке кода используется условный оператор `?`, который определяет, какой будет вызван метод (`user.hi` или `user.bye`) в зависимости от выполнения условия. В данном случае будет выбран `user.hi`. +В JavaScript `this` является "свободным", его значение вычисляется в момент вызова метода и не зависит от того, где этот метод был объявлен, а скорее от того, какой объект вызывает метод (какой объект стоит "перед точкой"). -Затем метод тут же вызывается с помощью скобок `()`. Но вызов не работает как положено! +Эта концепция вычисления `this` в момент исполнения имеет как свои плюсы, так и минусы. С одной стороны, функция может быть повторно использована в качестве метода у различных объектов (что повышает гибкость). С другой стороны, большая гибкость увеличивает вероятность ошибок. -Вы можете видеть, что при вызове будет ошибка, потому что значением `"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/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 index eb4377daab..53005e7356 100644 --- a/1-js/04-object-basics/07-optional-chaining/article.md +++ b/1-js/04-object-basics/07-optional-chaining/article.md @@ -7,46 +7,92 @@ ## Проблема "несуществующего свойства" -Если вы только начали читать учебник и изучать JavaScript, то, возможно, эта проблема вам пока незнакома, но она достаточно распространена. +Если вы только начали читать учебник и изучать JavaScript, то, возможно, проблема вас ещё не коснулась, но она довольно распространена. -Например, рассмотрим объекты для пользователей `user`. У большинства пользователей есть адрес `user.address` с улицей `user.address.street`, но некоторые адрес не указали. +В качестве примера предположим, что у нас есть объекты `user`, которые содержат информацию о наших пользователях. -В этом случае при попытке получить свойство `user.address.street` будет ошибка: +У большинства наших пользователей есть адреса в свойстве `user.address` с улицей `user.address.street`, но некоторые из них их не указали. + +В таком случае, когда мы попытаемся получить `user.address.street`, а пользователь окажется без адреса, мы получим ошибку: ```js run -let user = {}; // пользователь без свойства address +let user = {}; // пользователь без свойства "address" -alert(user.address.street); // ошибка! +alert(user.address.street); // Ошибка! ``` -Это нормальный результат, так работает JavaScript, но во многих реальных ситуациях удобнее было бы получать не ошибку, а просто `undefined` ("нет улицы"). +Это ожидаемый результат. JavaScript работает следующим образом. Поскольку `user.address` имеет значение `undefined`, попытка получить `user.address.street` завершается ошибкой. + +Во многих практических случаях мы бы предпочли получить здесь `undefined` вместо ошибки (что означало бы "улицы нет"). -Или ещё пример. В веб-разработке нам бывает нужно получить данные об HTML-элементе, который иногда может отсутствовать на странице: +...Или ещё один пример. В веб-разработке мы можем получить объект, соответствующий элементу веб-страницы, с помощью специального вызова метода, такого как `document.querySelector('.elem')`, и он возвращает `null`, когда такого элемента нет. ```js run -// Произойдёт ошибка, если querySelector(...) равен null. -let html = document.querySelector('.my-element').innerHTML; +// 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 && user.address && user.address.street ); // undefined (без ошибки) +alert( user.address && user.address.street && user.address.street.name ); // undefined (без ошибки) ``` -Использование логического И со всей цепочкой свойств гарантирует, что все они существуют (а если нет - вычисление прекращается), но это довольно длинная и громоздкая конструкция. +Проход при помощи логического оператора И `&&` через весь путь к свойству гарантирует, что все компоненты существуют (если нет, вычисление прекращается), но также не является идеальным. + +Как вы можете видеть, имена свойств по-прежнему дублируются в коде. Например, в приведённом выше коде `user.address` появляется три раза. + +Вот почему в язык была добавлена опциональная цепочка `?.`. Чтобы решить эту проблему - раз и навсегда! ## Опциональная цепочка -Опциональная цепочка `?.` останавливает вычисление и возвращает `undefined`, если часть перед `?.` имеет значение `undefined` или `null`. +Опциональная цепочка `?.` останавливает вычисление и возвращает `undefined`, если значение перед `?.` равно `undefined` или `null`. + +**Далее в этой статье, для краткости, мы будем говорить, что что-то "существует", если оно не является `null` и не `undefined`.** -**Для краткости в этой статье мы будем говорить о значении, что оно "существует", если оно отличается от `null` или `undefined`.** +Другими словами, `value?.prop`: +- работает как `value.prop`, если значение `value` существует, +- в противном случае (когда `value` равно `undefined/null`) он возвращает `undefined`. -Вот безопасный способ обратиться к свойству `user.address.street`: +Вот безопасный способ получить доступ к `user.address.street`, используя `?.`: ```js run let user = {}; // пользователь без адреса @@ -54,7 +100,15 @@ let user = {}; // пользователь без адреса alert( user?.address?.street ); // undefined (без ошибки) ``` -Чтение адреса с помощью конструкции `user?.address` выполняется без ошибок, даже если объекта `user` не существует: +Код лаконичный и понятный, в нем вообще нет дублирования. + +А вот пример с `document.querySelector`: + +```js run +let html = document.querySelector('.elem')?.innerHTML; // будет undefined, если элемента нет +``` + +Считывание адреса с помощью `user?.address` работает, даже если объект `user` не существует: ```js run let user = null; @@ -63,35 +117,33 @@ alert( user?.address ); // undefined alert( user?.address.street ); // undefined ``` -Обратите внимание, что синтаксис `?.` делает необязательным только свойство перед ним, а не какое-либо последующее. - -В приведённом выше примере конструкция `user?.` допускает, что переменная `user` может содержать `null/undefined`. +Обратите внимание: синтаксис `?.` делает необязательным значение перед ним, но не какое-либо последующее. -С другой стороны, если объект `user` существует, то в нём должно быть свойство `user.address`, иначе выполнение `user?.address.street` вызовет ошибку из-за второй точки. +Так например, в записи `user?.address.street.name` `?.` позволяет `user` безопасно быть `null/undefined` (и в этом случае возвращает `undefined`), но это так только для `user`. Доступ к последующим свойствам осуществляется обычным способом. Если мы хотим, чтобы некоторые из них были необязательными, тогда нам нужно будет заменить больше `.` на `?.`. ```warn header="Не злоупотребляйте опциональной цепочкой" -Используйте `?.` только тогда, когда допускаете ситуацию, что значение перед ним не существует. +Нам следует использовать `?.` только там, где нормально, что чего-то не существует. -Например, если по нашей логике объект `user` точно существует, но его свойство `address` является необязательным, то предпочтительнее использовать следующую конструкцию: `user.address?.street`. +К примеру, если, в соответствии с логикой нашего кода, объект `user` должен существовать, но `address` является необязательным, то нам следует писать `user.address?.street`, но не `user?.address?.street`. -Тогда если переменная `user` по ошибке окажется пустой, мы увидим программную ошибку и исправим это. +В этом случае, если вдруг `user` окажется `undefined`, мы увидим программную ошибку по этому поводу и исправим её. В противном случае, если слишком часто использовать `?.`, ошибки могут замалчиваться там, где это неуместно, и их будет сложнее отлаживать. ``` ````warn header="Переменная перед `?.` должна быть объявлена" -Если переменной `user` вообще не существует, то выражение `user?.anything` выдаст ошибку: +Если переменной `user` вообще нет, то `user?.anything` приведёт к ошибке: ```js run // ReferenceError: user is not defined user?.address; ``` -Объявление переменной (например `let/const/var user`) обязательно должно быть. Опциональная цепочка работает только с существующими переменными. +Переменная должна быть объявлена (к примеру, как `let/const/var user` или как параметр функции). Опциональная цепочка работает только с объявленными переменными. ```` ## Сокращённое вычисление -Как уже говорилось, `?.` немедленно останавливает вычисление, если левой части не существует. +Как было сказано ранее, `?.` немедленно останавливает вычисление, если левая часть не существует. -Таким образом, последующие вызовы функций или операции не будут выполнены. +Так что если после `?.` есть какие-то вызовы функций или операции, то они не произойдут. Например: @@ -99,86 +151,85 @@ user?.address; let user = null; let x = 0; -user?.sayHi(x++); // нет user, поэтому до x++ вычисление не дойдет +user?.sayHi(x++); // нет "user", поэтому выполнение не достигает вызова sayHi и x++ -alert(x); // 0, значение не было увеличено на единицу +alert(x); // 0, значение не увеличилось ``` ## Другие варианты применения: ?.(), ?.[] Опциональная цепочка `?.` — это не оператор, а специальная синтаксическая конструкция, которая также работает с функциями и квадратными скобками. -Например, `?.()` используется для вызова потенциально несуществующей функции. +Например, `?.()` используется для вызова функции, которая может не существовать. -В следующем примере не у всех пользователей есть метод `admin`: +В приведённом ниже коде у некоторых наших пользователей есть метод `admin`, а у некоторых его нет: ```js run -let user1 = { +let userAdmin = { admin() { - alert("Я администратор"); + alert("Я админ"); } -} +}; -let user2 = {}; +let userGuest = {}; *!* -user1.admin?.(); // Я администратор -user2.admin?.(); +userAdmin.admin?.(); // Я админ +*/!* + +*!* +userGuest.admin?.(); // ничего не произойдет (такого метода нет) */!* ``` -В обоих вызовах сначала используем точку (`user1.admin`), чтобы получить свойство `admin`, потому что объект пользователя точно существует, к нему можно обратиться без какой-либо ошибки. +Здесь в обеих строках мы сначала используем точку (`userAdmin.admin`), чтобы получить свойство `admin`, потому что мы предполагаем, что объект `userAdmin` существует, так что читать из него безопасно. -Затем уже `?.()` проверяет левую часть: если функция `admin` существует, то она выполнится (это так для `user1`). Иначе (для `user2`) вычисление остановится без ошибок. +Затем `?.()` проверяет левую часть: если функция `admin` существует, то она запускается (это так для `userAdmin`). В противном случае (для `userGuest`) вычисление остановится без ошибок. -Также существует синтаксис `?.[]`, если значение свойства требуется получить с помощью квадратных скобок `[]`, а не через точку `.`. Как и в остальных случаях, такой способ позволяет защититься от ошибок при доступе к свойству объекта, которого может не быть. +Синтаксис `?.[]` также работает, если мы хотим использовать скобки `[]` для доступа к свойствам вместо точки `.`. Как и в предыдущих случаях, он позволяет безопасно считывать свойство из объекта, который может не существовать. ```js run +let key = "firstName"; + let user1 = { - firstName: "Иван" + firstName: "John" }; -let user2 = null; // Представим, что пользователь не авторизован +let user2 = null; -let key = "firstName"; - -alert( user1?.[key] ); // Иван +alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined - -alert( user1?.[key]?.something?.not?.existing); // undefined ``` -Кроме этого, `?.` можно использовать совместно с `delete`: +Также мы можем использовать `?.` с `delete`: ```js run -delete user?.name; // Удалить user.name, если пользователь существует +delete user?.name; // удаляет user.name если пользователь существует ``` -````warn header="Можно использовать `?.` для безопасного чтения и удаления, но не для записи" +````warn header="Мы можем использовать `?.` для безопасного чтения и удаления, но не для записи" Опциональная цепочка `?.` не имеет смысла в левой части присваивания. Например: - ```js run -let user; +let user = null; -user?.name = "John"; // Ошибка, это не сработает -// это по сути то же самое что undefined = "John" +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`. +1. `obj?.prop` -- возвращает `obj.prop` если `obj` существует, в противном случае `undefined`. +2. `obj?.[prop]` -- возвращает `obj[prop]` если `obj` существует, в противном случае `undefined`. +3. `obj.method?.()` -- вызывает `obj.method()`, если `obj.method` существует, в противном случае возвращает `undefined`. -Как мы видим, все они просты и понятны в использовании. `?.` проверяет левую часть выражения на равенство `null/undefined`, и продолжает дальнейшее вычисление, только если это не так. +Как мы видим, все они просты и понятны в использовании. `?.` проверяет левую часть на `null/undefined` и позволяет продолжить вычисление, если это не так. -Цепочка `?.` позволяет без возникновения ошибок обратиться к вложенным свойствам. +Цепочка `?.` позволяет безопасно получать доступ к вложенным свойствам. -Тем не менее, нужно разумно использовать `?.` — только там, где это уместно, если допустимо, что левая часть не существует. Чтобы таким образом не скрывать возможные ошибки программирования. +Тем не менее, мы должны использовать `?.` осторожно, только там, где по логике кода допустимо, что левая часть не существует. Чтобы он не скрывал от нас ошибки программирования, если они возникнут. diff --git a/1-js/04-object-basics/08-symbol/article.md b/1-js/04-object-basics/08-symbol/article.md index 9d40466541..6edf9869ce 100644 --- a/1-js/04-object-basics/08-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"); *!* @@ -269,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 index ac2575dd70..ed6a70e668 100644 --- a/1-js/04-object-basics/09-object-toprimitive/article.md +++ b/1-js/04-object-basics/09-object-toprimitive/article.md @@ -3,28 +3,49 @@ Что произойдёт, если сложить два объекта `obj1 + obj2`, вычесть один из другого `obj1 - obj2` или вывести их на экран, воспользовавшись `alert(obj)`? -В этом случае объекты сначала автоматически преобразуются в примитивы, а затем выполняется операция. +JavaScript совершенно не позволяет настраивать, как операторы работают с объектами. В отличие от некоторых других языков программирования, таких как Ruby или C++, мы не можем реализовать специальный объектный метод для обработки сложения (или других операторов). -В главе мы видели правила для численных, строковых и логических преобразований. Но обделили вниманием объекты. Теперь, поскольку мы уже знаем о методах объектов и символах, можно исправить это упущение. +В случае таких операций, объекты автоматически преобразуются в примитивы, затем выполняется сама операция над этими примитивами, и на выходе мы получим примитивное значение. -1. Все объекты в логическом контексте являются `true`. Существуют лишь их численные и строковые преобразования. -2. Численные преобразования происходят, когда мы вычитаем объекты или выполняем математические операции. Например, объекты `Date` (мы рассмотрим их в статье ) могут вычитаться, и результатом `date1 - date2` будет временной отрезок между двумя датами. -3. Что касается строковых преобразований -- они обычно происходят, когда мы выводим объект `alert(obj)`, а также в других случаях, когда объект используется как строка. +Это важное ограничение: результатом `obj1 + obj2` (или другой математической операции) не может быть другой объект! -## Преобразование к примитивам +К примеру, мы не можем создавать объекты, представляющие векторы или матрицы (или достижения или может ещё что-то), складывать их и ожидать в качестве результата "суммированный" объект. Такие архитектурные ходы автоматически оказываются "за бортом". -Мы можем тонко настраивать строковые и численные преобразования, используя специальные методы объекта. +Итак, поскольку мы технически здесь мало что можем сделать, в реальных проектах нет математики с объектами. Если она всё же происходит, то за редким исключением, это из-за ошибок в коде. -Существуют три варианта преобразований ("три хинта"), описанные в [спецификации](https://tc39.github.io/ecma262/#sec-toprimitive): +В этой главе мы рассмотрим, как объект преобразуется в примитив и как это можно настроить. + +У нас есть две цели: + +1. Это позволит нам понять, что происходит в случае ошибок в коде, когда такая операция произошла случайно. +2. Есть исключения, когда такие операции возможны и вполне уместны. Например, вычитание или сравнение дат (`Date` объекты). Мы встретимся с ними позже. + +## Правила преобразования + +В главе мы рассмотрели правила для числовых, строковых и логических преобразований примитивов. Но мы оставили пробел для объектов. Теперь, когда мы уже знаем о методах и символах, пришло время заполнить этот пробел. + +1. Не существует преобразования к логическому значению. В логическом контексте все объекты являются `true`, всё просто. Существует лишь их числовое и строковое преобразование. +2. Числовое преобразование происходит, когда мы вычитаем объекты или применяем математические функции. Например, объекты `Date` (которые будут рассмотрены в главе ) могут быть вычтены, и результатом `date1 - date2` будет разница во времени между двумя датами. +3. Что касается преобразований к строке -- оно обычно происходит, когда мы выводим на экран объект при помощи `alert(obj)` и в подобных контекстах. + +Мы можем реализовать свои преобразования к строкам и числам, используя специальные объектные методы. + +Теперь давайте углубимся в детали. Это единственный путь для того, чтобы разобраться в нюансах этой темы. + +## Хинты + +Как JavaScript решает, какое преобразование применить? + +Существует три варианта преобразования типов, которые происходят в различных ситуациях. Они называются "хинтами", как описано в [спецификации](https://tc39.github.io/ecma262/#sec-toprimitive): `"string"` -: Для преобразования объекта к строке, когда операция ожидает получить строку, например `alert`: +: Для преобразования объекта к строке, когда мы выполняем операцию над объектом, которая ожидает строку, например `alert`: ```js // вывод alert(obj); - // используем объект в качестве имени свойства + // используем объект в качестве ключа anotherObj[obj] = 123; ``` @@ -35,7 +56,7 @@ // явное преобразование let num = Number(obj); - // математическое (исключая бинарный оператор "+") + // математические (не считая бинарного плюса) let n = +obj; // унарный плюс let delta = date1 - date2; @@ -43,45 +64,52 @@ let greater = user1 > user2; ``` + Большинство встроенных математических функций также включают в себя такое преобразование. + `"default"` : Происходит редко, когда оператор "не уверен", какой тип ожидать. - Например, бинарный плюс `+` может работать с обоими типами: строками (объединять их) и числами (складывать). Таким образом, и те, и другие будут вычисляться. Или когда происходит сравнение объектов с помощью нестрогого равенства `==` со строкой, числом или символом, и неясно, какое преобразование должно быть выполнено. + Например, бинарный плюс `+` может работать как со строками (объединяя их в одну), так и с числами (складывая их). Поэтому, если бинарный плюс получает объект в качестве аргумента, он использует хинт `"default"` для его преобразования. + + Также, если объект сравнивается с помощью `==` со строкой, числом или символом, тоже неясно, какое преобразование следует выполнить, поэтому используется хинт `"default"`. ```js - // бинарный плюс - let total = car1 + car2; + // бинарный плюс использует хинт "default" + let total = obj1 + obj2; - // obj == string/number/symbol + // obj == number использует хинт "default" if (user == 1) { ... }; ``` - Оператор больше/меньше `<>` также может работать как со строками, так и с числами. Однако, по историческим причинам он использует хинт "number", а не "default". + Операторы сравнения больше/меньше, такие как `<` `>`, также могут работать как со строками, так и с числами. Тем не менее, по историческим причинам, они используют хинт `"number"`, а не `"default"`. - На практике все встроенные объекты, исключая `Date` (мы познакомимся с ним чуть позже), реализуют `"default"` преобразования тем же способом, что и `"number"`. И нам следует поступать так же. +Впрочем на практике, всё немного проще. -Обратите внимание, что существуют лишь три варианта хинтов. Всё настолько просто. Не существует хинта со значением "boolean" (все объекты являются `true` в логическом контексте) или каких-либо ещё. И если мы считаем `"default"` и `"number"` одинаковыми, как большинство встроенных объектов, то остаются всего два варианта преобразований. +Все встроенные объекты, за исключением одного (объект `Date`, который мы рассмотрим позже), реализуют `"default"` преобразование тем же способом, что и `"number"`. И нам следует поступать так же. -**В процессе преобразования движок JavaScript пытается найти и вызвать три следующих метода объекта:** +**Чтобы выполнить преобразование, JavaScript пытается найти и вызвать три следующих метода объекта:** -1. Вызывает `obj[Symbol.toPrimitive](hint)` - метод с символьным ключом `Symbol.toPrimitive` (системный символ), если такой метод существует, и передаёт ему хинт. +1. Вызвать `obj[Symbol.toPrimitive](hint)` - метод с символьным ключом `Symbol.toPrimitive` (системный символ), если такой метод существует, 2. Иначе, если хинт равен `"string"` - - пытается вызвать `obj.toString()`, а если его нет, то `obj.valueOf()`, если он существует. -3. В случае, если хинт равен `"number"` или `"default"` - - пытается вызвать `obj.valueOf()`, а если его нет, то `obj.toString()`, если он существует. + - попробовать вызвать `obj.toString()` или `obj.valueOf()`, смотря какой из них существует. +3. Иначе, если хинт равен `"number"` или `"default"` + - попробовать вызвать `obj.valueOf()` или `obj.toString()`, смотря какой из них существует. ## Symbol.toPrimitive -Начнём с универсального подхода - символа `Symbol.toPrimitive`: метод с таким названием (если есть) используется для всех преобразований: +Давайте начнём с первого метода. Есть встроенный символ с именем `Symbol.toPrimitive`, который следует использовать для обозначения метода преобразования, вот так: ```js obj[Symbol.toPrimitive] = function(hint) { - // должен вернуть примитивное значение - // hint равно чему-то одному из: "string", "number" или "default" + // вот код для преобразования этого объекта в примитив + // он должен вернуть примитивное значение + // hint = чему-то из "string", "number", "default" }; ``` -Для примера используем его в реализации объекта `user`: +Если метод `Symbol.toPrimitive` существует, он используется для всех хинтов, и больше никаких методов не требуется. + +Например, здесь объект `user` реализует его: ```js run let user = { @@ -100,19 +128,41 @@ alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500 ``` -Как мы видим из кода, `user` преобразовывается либо в информативную читаемую строку, либо в денежный счёт в зависимости от значения хинта. Единственный метод `user[Symbol.toPrimitive]` смог обработать все случаи преобразований. +Как мы можем видеть из кода, `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` возвращает сам объект. -## Методы toString/valueOf +Взгляните на пример: -Методы `toString` и `valueOf` берут своё начало с древних времён. Это не символы, так как в то время символов ещё не существовало, а просто обычные методы объектов со строковыми именами. Они предоставляют "устаревший" способ реализации преобразований объектов. +```js run +let user = {name: "John"}; + +alert(user); // [object Object] +alert(user.valueOf() === user); // true +``` -Если нет метода `Symbol.toPrimitive`, движок JavaScript пытается найти эти методы и вызвать их следующим образом: +Таким образом, если мы попытаемся использовать объект в качестве строки, как например в `alert` или вроде того, то по умолчанию мы увидим `[object Object]`. -- `toString -> valueOf` для хинта со значением "string". -- `valueOf -> toString` -- в ином случае. +Значение по умолчанию `valueOf` упоминается здесь только для полноты картины, чтобы избежать какой-либо путаницы. Как вы можете видеть, он возвращает сам объект и поэтому игнорируется. Не спрашивайте меня почему, это по историческим причинам. Так что мы можем предположить, что его не существует. -Для примера, используем их в реализации всё того же объекта `user`. Воспроизведём его поведение комбинацией методов `toString` и `valueOf`: +Давайте применим эти методы для настройки преобразования. + +Для примера, используем их в реализации всё того же объекта `user`. Но уже используя комбинацию `toString` и `valueOf` вместо `Symbol.toPrimitive`: ```js run let user = { @@ -138,7 +188,7 @@ alert(user + 500); // valueOf -> 1500 Как видим, получилось то же поведение, что и в предыдущем примере с `Symbol.toPrimitive`. -Довольно часто мы хотим описать одно "универсальное" преобразование объекта к примитиву для всех ситуаций. Для этого достаточно создать один `toString`: +Довольно часто нам нужно единое "универсальное" место для обработки всех примитивных преобразований. В этом случае мы можем реализовать только `toString`: ```js run let user = { @@ -153,71 +203,77 @@ alert(user); // toString -> John alert(user + 500); // toString -> John500 ``` -В отсутствие `Symbol.toPrimitive` и `valueOf`, `toString` обработает все случаи преобразований к примитивам. - +В отсутствие `Symbol.toPrimitive` и `valueOf`, `toString` обработает все примитивные преобразования. -## Возвращаемые типы +### Преобразование может вернуть любой примитивный тип -Важно понимать, что все описанные методы для преобразований объектов не обязаны возвращать именно требуемый "хинтом" тип примитива. +Важная вещь, которую следует знать обо всех методах преобразования примитивов, заключается в том, что они не обязательно возвращают подсказанный хинтом примитив. -Нет обязательного требования, чтобы `toString()` возвращал именно строку, или чтобы метод `Symbol.toPrimitive` возвращал именно число для хинта "number". +Нет никакого контроля над тем, вернёт ли `toString` именно строку, или чтобы метод `Symbol.toPrimitive` возвращал именно число для хинта `"number"`. -**Единственное обязательное требование: методы должны возвращать примитив, а не объект.** +Единственное обязательное условие: эти методы должны возвращать примитив, а не объект. ```smart header="Историческая справка" -По историческим причинам, если `toString` или `valueOf` вернёт объект, то ошибки не будет, но такое значение будет проигнорировано (как если бы метода вообще не существовало). +По историческим причинам, если `toString` или `valueOf` вернёт объект, то ошибки не будет, но такое значение будет проигнорировано (как если бы метода вообще не существовало). Это всё потому, что в древние времена в JavaScript не было хорошей концепции "ошибки". -Метод `Symbol.toPrimitive`, напротив, *обязан* возвращать примитив, иначе будет ошибка. +А вот `Symbol.toPrimitive` уже "четче", этот метод *обязан* возвращать примитив, иначе будет ошибка. ``` -## Последующие операции +## Дальнейшие преобразования + +Как мы уже знаем, многие операторы и функции выполняют преобразования типов, например, умножение `*` преобразует операнды в числа. -Операция, инициировавшая преобразование, получает примитив и затем продолжает работу с ним, производя дальнейшие преобразования, если это необходимо. +Если мы передаём объект в качестве аргумента, то в вычислениях будут две стадии: +1. Объект преобразуется в примитив (с использованием правил, описанных выше). +2. Если необходимо для дальнейших вычислений, этот примитив преобразуется дальше. Например: -- Математические операции, исключая бинарный плюс, преобразуют примитив к числу: +```js run +let obj = { + // toString обрабатывает все преобразования в случае отсутствия других методов + toString() { + return "2"; + } +}; - ```js run - let obj = { - // toString обрабатывает все преобразования в случае отсутствия других методов - toString() { - return "2"; - } - }; +alert(obj * 2); // 4, объект был преобразован к примитиву "2", затем умножение сделало его числом +``` - alert(obj * 2); // 4, объект был преобразован к примитиву "2", затем умножение сделало его числом - ``` +1. Умножение `obj * 2` сначала преобразует объект в примитив (это строка `"2"`). +2. Затем `"2" * 2` становится `2 * 2` (строка преобразуется в число). -- Бинарный плюс `+` в аналогичном случае сложит строки: +А вот, к примеру, бинарный плюс в подобной ситуации соединил бы строки, так как он совсем не брезгует строк: - ```js run - let obj = { - toString() { - return "2"; - } - }; +```js run +let obj = { + toString() { + return "2"; + } +}; - alert(obj + 2); // 22 (преобразование к примитиву вернуло строку => конкатенация) - ``` +alert(obj + 2); // "22" ("2" + 2), преобразование к примитиву вернуло строку => конкатенация +``` ## Итого -Преобразование объектов в примитивы вызывается автоматически многими встроенными функциями и операторами, которые ожидают примитив в качестве аргумента. +Преобразование объекта в примитив вызывается автоматически многими встроенными функциями и операторами, которые ожидают примитив в качестве значения. -Существует всего 3 типа преобразований (хинтов): +Существует всего 3 типа (хинта) для этого: - `"string"` (для `alert` и других операций, которым нужна строка) - `"number"` (для математических операций) -- `"default"` (для некоторых операций) +- `"default"` (для некоторых других операторов, обычно объекты реализуют его как `"number"`) -В спецификации явно указано, какой хинт должен использовать каждый оператор. И существует совсем немного операторов, которые не знают, что ожидать, и используют хинт со значением `"default"`. Обычно для встроенных объектов хинт `"default"` обрабатывается так же, как `"number"`. Таким образом, последние два очень часто объединяют вместе. +Спецификация явно описывает для каждого оператора, какой ему следует использовать хинт. -Алгоритм преобразований к примитивам следующий: +Алгоритм преобразования таков: -1. Сначала вызывается метод `obj[Symbol.toPrimitive](hint)`, если он существует. -2. Иначе, если хинт равен `"string"` - - происходит попытка вызвать `obj.toString()`, затем `obj.valueOf()`, смотря что есть. -3. Иначе, если хинт равен `"number"` или `"default"` - - происходит попытка вызвать `obj.valueOf()`, затем `obj.toString()`, смотря что есть. +1. Сначала вызывается метод `obj[Symbol.toPrimitive](hint)`, если он существует, +2. В случае, если хинт равен `"string"` + - происходит попытка вызвать `obj.toString()` и `obj.valueOf()`, смотря что есть. +3. В случае, если хинт равен `"number"` или `"default"` + - происходит попытка вызвать `obj.valueOf()` и `obj.toString()`, смотря что есть. + +Все эти методы должны возвращать примитив (если определены). -На практике довольно часто достаточно реализовать только `obj.toString()` как "универсальный" метод для всех типов преобразований, возвращающий "читаемое" представление объекта, достаточное для логирования или отладки. +На практике часто бывает достаточно реализовать только `obj.toString()` в качестве универсального метода для преобразований к строке, который должен возвращать удобочитаемое представление объекта для целей логирования или отладки. 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 78c3f734a3..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 позволяет нам работать с примитивными типами данных - строками, числами и т.д., как будто они являются объектами. У них есть и методы. Мы изучим их позже, а сначала разберём, как это всё работает, потому что, конечно, примитивы - не объекты. @@ -39,8 +39,8 @@ roma.sayHi(); // Привет, дружище! Вот парадокс, с которым столкнулся создатель JavaScript: -- Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно, если бы мы могли работать с ними через вызовы методов. -- Примитивы должны быть лёгкими и быстрыми. +- Есть много всего, что хотелось бы сделать с примитивами, такими как строка или число. Было бы замечательно, если бы мы могли обращаться к ним при помощи методов. +- Примитивы должны быть лёгкими и быстрыми насколько это возможно. Выбранное решение, хотя выглядит оно немного неуклюже: @@ -48,7 +48,7 @@ roma.sayHi(); // Привет, дружище! 2. Язык позволяет осуществлять доступ к методам и свойствам строк, чисел, булевых значений и символов. 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 37a9cc515e..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 @@ -27,21 +27,7 @@ alert( randomInteger(1, 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 3245a5b6b4..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,8 @@ # Числа В современном JavaScript существует два типа чисел: -1. Обычные числа в JavaScript хранятся в 64-битном формате [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), который также называют "числа с плавающей точкой двойной точности" (double precision floating point numbers). Это числа, которые мы будем использовать чаще всего. Мы поговорим о них в этой главе. -2. `BigInt` числа дают возможность работать с целыми числами произвольной длины. Они нужны достаточно редко и используются в случаях, когда необходимо работать со значениями более чем 253 или менее чем -253. Так как `BigInt` числа нужны достаточно редко, мы рассмотрим их в отдельной главе . +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` числа нужны достаточно редко, мы рассмотрим их в отдельной главе . В данной главе мы рассмотрим только первый тип чисел: числа типа `number`. Давайте глубже изучим, как с ними работать в JavaScript. @@ -14,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 // 1 делится на 1 с 3 нулями -1e-3 = 1 / 1000 (=0.001) +1e-3 === 1 / 1000 (=0.001) // 1.23 делится на 1 с 6 нулями -1.23e-6 = 1.23 / 1000000 (=0.00000123) +1.23e-6 === 1.23 / 1000000 (=0.00000123) ``` ### Шестнадцатеричные, двоичные и восьмеричные числа @@ -67,11 +74,10 @@ 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 @@ -151,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` знаков после запятой и возвращает строковое представление результата. @@ -179,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 вернёт бесконечность: @@ -209,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` становится бесконечной дробью. @@ -227,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`. В других случаях можно использовать унарный оператор `+`, чтобы преобразовать строку в число: @@ -244,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 @@ -295,7 +307,7 @@ alert( 9999999999999999 ); // покажет 10000000000000000 alert( isNaN("str") ); // true ``` - Нужна ли нам эта функция? Разве не можем ли мы просто сравнить `=== NaN`? К сожалению, нет. Значение `NaN` уникально тем, что оно не является равным ни чему другому, даже самому себе: + Нужна ли нам эта функция? Разве не можем ли мы просто сравнить `=== NaN`? К сожалению, нет. Значение `NaN` уникально тем, что оно не является равным ничему другому, даже самому себе: ```js run alert( NaN === NaN ); // false @@ -313,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`" @@ -420,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 b0396ab408..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 любые текстовые данные являются строками. Не существует отдельного типа "символ", который есть в ряде других языков. @@ -79,28 +80,12 @@ alert(str1 == str2); // true |-----------|-------------| |`\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" ``` ## Получение подстроки @@ -451,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="Какой метод выбрать?" @@ -506,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 @@ -535,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), обеспечивающий правильное сравнение строк на разных языках с учётом их правил. Для этого есть соответствующий метод. @@ -559,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`, когда надо только проверить, есть ли вхождение. @@ -683,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/article.md b/1-js/05-data-types/04-array/article.md index 6306a6a1ce..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 = ["Яблоко"]; @@ -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; i { + 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/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 227224cba4..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`, но вместо подстрок возвращает подмассивы. @@ -136,13 +136,13 @@ 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="Функция сравнения может вернуть любое число" -На самом деле от функции сравнения требуется любое положительное число, чтобы сказать "больше", и отрицательное число, чтобы сказать "меньше". +На самом деле от функции сравнения требуется любое положительное число, чтобы сказать «больше», и отрицательное число, чтобы сказать «меньше». Это позволяет писать более короткие функции: @@ -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/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md index 46bbdbb6d4..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,7 +288,6 @@ alert( slice(str, 1, 3) ); // 😂𩷶 alert( str.slice(1, 3) ); // мусор (две части различных суррогатных пар) ``` - ## Итого Объекты, которые можно использовать в цикле `for..of`, называются *итерируемыми*. 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/article.md b/1-js/05-data-types/07-map-set/article.md index 0961e94d07..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)`. @@ -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.values()` -- возвращает перебираемый объект для значений, -- `set.keys()` -- то же самое, что и `set.values()`, присутствует для обратной совместимости с `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/article.md b/1-js/05-data-types/08-weakmap-weakset/article.md index bfd0db0956..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; @@ -107,7 +108,6 @@ john = null; // перезаписываем ссылку на объект ## Пример: дополнительные данные - В основном, `WeakMap` используется в качестве *дополнительного хранилища данных*. Если мы работаем с объектом, который "принадлежит" другому коду, может быть даже сторонней библиотеке, и хотим сохранить у себя какие-то данные для него, которые должны существовать лишь пока существует этот объект, то `WeakMap` - как раз то, что нужно. @@ -142,7 +142,7 @@ function countUser(user) { // 📁 main.js let john = { name: "John" }; -countUser(john); //ведём подсчёт посещений +countUser(john); // ведём подсчёт посещений // пользователь покинул нас john = null; @@ -274,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 b927527d80..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 @@ -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 8e86e9dfd5..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"; 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). Мы можем использовать его с деструктуризацией для цикличного перебора ключей и значений объекта: @@ -114,31 +121,69 @@ user.set("name", "John"); user.set("age", "30"); *!* +// Map перебирает как пары [ключ, значение], что очень удобно для деструктурирования for (let [key, value] of user) { */!* 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 8f6bcc033b..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/article.md b/1-js/05-data-types/11-date/article.md index 6d6f9327b5..40402e0f92 100644 --- a/1-js/05-data-types/11-date/article.md +++ b/1-js/05-data-types/11-date/article.md @@ -57,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`. @@ -348,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); @@ -376,7 +376,7 @@ for (let i = 0; i < 10; i++) { ```warn header="Будьте осторожны с микробенчмарками" Современные интерпретаторы JavaScript выполняют множество оптимизаций. Они могут повлиять на результаты "искусственных тестов" по сравнению с "нормальным использованием", особенно если мы тестируем что-то очень маленькое, например, работу оператора или встроенной функции. Поэтому если хотите серьёзно понять производительность, пожалуйста, изучите, как работают интерпретаторы JavaScript. И тогда вам, вероятно, уже не понадобятся микробенчмарки. -Отличный набор статей о V8 можно найти на . +Отличный набор статей о V8 можно найти на . ``` ## Разбор строки с датой @@ -416,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/06-advanced-functions/01-recursion/01-sum-to/solution.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md index 7c2b33c5d0..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/article.md b/1-js/06-advanced-functions/01-recursion/article.md index 140935c420..4a8d323447 100644 --- a/1-js/06-advanced-functions/01-recursion/article.md +++ b/1-js/06-advanced-functions/01-recursion/article.md @@ -1,3 +1,4 @@ + # Рекурсия и стек Вернёмся к функциям и изучим их более подробно. @@ -61,7 +62,7 @@ pow(2, 4) = 16 if n==1 = x / pow(x, n) = - \ + \ else = x * pow(x, n - 1) ``` @@ -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/recursive-salaries.svg b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg index 61f32dbc08..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 3f1e721436..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 @@ -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 100% 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 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 c488da0c2b..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 5d915d9088..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) +Если переменная объявлена внутри блока кода `{...}`, то она видна только внутри этого блока. -Итак, в процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное): +Например: -- Внутреннее лексическое окружение соответствует текущему выполнению `say`. - - В нём находится одна переменная `name`, аргумент функции. Мы вызываем `say("John")`, так что значение переменной `name` равно `"John"`. -- Внешнее лексическое окружение -- это глобальное лексическое окружение. +```js run +{ + // выполняем некоторые действия с локальной переменной, которые не должны быть видны снаружи - В нём находятся переменная `phrase` и сама функция. + let message = "Hello"; // переменная видна только в этом блоке -У внутреннего лексического окружения есть ссылка `outer` на внешнее. + alert(message); // Hello +} -**Когда код хочет получить доступ к переменной -- сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.** +alert(message); // ReferenceError: message is not defined +``` -Если переменная не была найдена, это будет ошибкой в `strict mode`. Без `strict mode`, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким именем. +С помощью блоков `{...}` мы можем изолировать часть кода, выполняющую свою собственную задачу, с переменными, принадлежащими только ей: -Давайте посмотрим, как происходит поиск в нашем примере: - -- Когда `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(); ``` -Из-за того, что у блока есть собственное лексическое окружение, код снаружи него (или в другом скрипте) не видит переменные этого блока. +В начале каждого вызова `makeCounter()` создается новый объект лексического окружения, в котором хранятся переменные для конкретного запуска `makeCounter`. -### IIFE +Таким образом, мы имеем два вложенных лексических окружения, как в примере выше: -В прошлом в JavaScript не было лексического окружения на уровне блоков кода. +![](lexenv-nested-makecounter-2.svg) -Так что программистам пришлось что-то придумать. И то, что они сделали, называется "immediately-invoked function expressions" (аббревиатура IIFE), что означает функцию, запускаемую сразу после объявления. +Отличие заключается в том, что во время выполнения `makeCounter()` создается крошечная вложенная функция, состоящая всего из одной строки: `return count++`. Мы ее еще не запускаем, а только создаем. -Это не то, что мы должны использовать сегодня, но, так как вы можете встретить это в старых скриптах, полезно понимать принцип работы. +Все функции помнят лексическое окружение, в котором они были созданы. Технически здесь нет никакой магии: все функции имеют скрытое свойство `[[Environment]]`, которое хранит ссылку на лексическое окружение, в котором была создана функция: -IIFE выглядит так: +![](lexenv-nested-makecounter-3.svg) -```js run -(function() { +Таким образом, `counter.[[Environment]]` имеет ссылку на `{count: 0}` лексического окружения. Так функция запоминает, где она была создана, независимо от того, где она вызывается. Ссылка на `[[Environment]]` устанавливается один раз и навсегда при создании функции. - let message = "Hello"; +Впоследствии, при вызове `counter()`, для этого вызова создается новое лексическое окружение, а его внешняя ссылка на лексическое окружение берется из `counter.[[Environment]]`: - alert(message); // Hello +![](closure-makecounter-nested-call.svg) -})(); -``` +Теперь, когда код внутри `counter()` ищет переменную `count`, он сначала ищет ее в собственном лексическом окружении (пустом, так как там нет локальных переменных), а затем в лексическом окружении внешнего вызова `makeCounter()`, где находит `count` и изменяет ее. -Здесь создаётся и немедленно вызывается Function Expression. Так что код выполняется сразу же и у него есть свои локальные переменные. +**Переменная обновляется в том лексическом окружении, в котором она существует.** -Function Expression обёрнуто в скобки `(function {...})`, потому что, когда JavaScript встречает `"function"` в основном потоке кода, он воспринимает это как начало Function Declaration. Но у Function Declaration должно быть имя, так что такой код вызовет ошибку: +Вот состояние после выполнения: -```js run -// Попробуйте объявить и сразу же вызвать функцию -function() { // <-- Error: Unexpected token ( - - let message = "Hello"; - - alert(message); // Hello - -}(); -``` +![](closure-makecounter-nested-call-2.svg) -Даже если мы скажем: "хорошо, давайте добавим имя", -- это не сработает, потому что JavaScript не позволяет вызывать Function Declaration немедленно. +Если мы вызовем `counter()` несколько раз, то в одном и том же месте переменная `count` будет увеличена до `2`, `3` и т.д. -```js run -// ошибка синтаксиса из-за скобок ниже -function go() { - -}(); // <-- не можете вызывать Function Declaration немедленно -``` -Так что, скобки вокруг функции -- это трюк, который позволяет показать JavaScript, что функция была создана в контексте другого выражения, и, таким образом, это функциональное выражение: ей не нужно имя и её можно вызвать немедленно. - -Кроме скобок, существуют и другие пути показать JavaScript, что мы имеем в виду Function Expression: - -```js run -// Пути создания IIFE - -(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(); // пока g существует, -// соответствующее лексическое окружение существует +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/04-var/article.md b/1-js/06-advanced-functions/04-var/article.md index 6a9e684796..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` @@ -99,22 +105,22 @@ let user; // SyntaxError: 'user' has already been declared Используя `var`, можно переобъявлять переменную сколько угодно раз. Повторные `var` игнорируются: ```js run -var user = "Pete"; +var user = "Пётр"; var user; // ничего не делает, переменная объявлена раньше // ...нет ошибки -alert(user); // Pete +alert(user); // Пётр ``` Если дополнительно присвоить значение, то переменная примет новое значение: ```js run -var user = "Pete"; +var user = "Пётр"; -var user = "John"; +var user = "Иван"; -alert(user); // John +alert(user); // Иван ``` ## "var" обрабатываются в начале запуска функции @@ -217,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 243f984a2b..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`, то такого бы не произошло: @@ -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 b7e2a6823c..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/Интроспекция_(программирование)) в функциях, которые работают с другими функциями. 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 5677fc09a6..3bf5997b0f 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/article.md +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md @@ -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 мс).** Это потому, что новый вызов планируется в конце предыдущего. 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 fe5949e7df..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,18 +4,21 @@ 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()` слишком ресурсоёмкая, чтобы делать это при каждом микродвижении. Да и нет смысла делать обновление чаще, чем один раз в 1000 мс. 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 94cac25d51..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 @@ -62,7 +62,7 @@ alert( "Again: " + slow(2) ); // возвращаем из кеша - При необходимости мы можем объединить несколько декораторов (речь об этом пойдёт позже). -## Применение "func.call" для передачи контекста. +## Применение "func.call" для передачи контекста Упомянутый выше кеширующий декоратор не подходит для работы с методами объектов. 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/07-object-properties/01-property-descriptors/article.md b/1-js/07-object-properties/01-property-descriptors/article.md index 7f776c76fd..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`) иногда предустановлен для некоторых встроенных объектов и свойств. -Неконфигурируемое свойство не может быть удалено. +Неконфигурируемое свойство не может быть удалено, его атрибуты не могут быть изменены. Например, свойство `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 023449fa6f..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() { 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 eb354126cb..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,6 +124,8 @@ alert(longEar.jumps); // true (из rabbit) ![](proto-animal-rabbit-chain.svg) +Теперь, если мы прочтём что-нибудь из `longEar`, и оно будет отсутствовать, JavaScript будет искать его в `rabbit`, а затем в `animal`. + Есть только два ограничения: 1. Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить `__proto__` по кругу. @@ -137,6 +133,18 @@ alert(longEar.jumps); // true (из rabbit) Это вполне очевидно, но всё же: может быть только один `[[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` и другие - игнорируют унаследованные свойства. 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/03-native-prototypes/article.md b/1-js/08-prototypes/03-native-prototypes/article.md index 035ff13fc7..7e365b3062 100644 --- a/1-js/08-prototypes/03-native-prototypes/article.md +++ b/1-js/08-prototypes/03-native-prototypes/article.md @@ -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]]`: 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/09-classes/01-class/article.md b/1-js/09-classes/01-class/article.md index 9407856101..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 @@ -165,7 +165,7 @@ user.sayHi(); ``` 2. Методы класса являются неперечислимыми. - Определение класса устанавливает флаг `enumerable` в` false` для всех методов в `"prototype"`. + Определение класса устанавливает флаг `enumerable` в `false` для всех методов в `"prototype"`. И это хорошо, так как если мы проходимся циклом `for..in` по объекту, то обычно мы не хотим при этом получать методы класса. 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/article.md b/1-js/09-classes/02-class-inheritance/article.md index edbe624ccd..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 { @@ -16,57 +21,26 @@ class Animal { } 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,24 +54,30 @@ let rabbit = new Rabbit("Белый кролик"); rabbit.run(5); // Белый кролик бежит со скоростью 5. rabbit.hide(); // Белый кролик прячется! ``` -Теперь код `Rabbit` стал короче, так как используется конструктор класса `Animal` по умолчанию и кролик может использовать метод `run` как и все животные. -Ключевое слово `extends` работает, используя прототипы. Оно устанавливает `Rabbit.prototype.[[Prototype]]` в `Animal.prototype`. Так что если метод не найден в `Rabbit.prototype`, JavaScript берёт его из `Animal.prototype`. +Объект класса `Rabbit` имеет доступ как к методам `Rabbit`, таким как `rabbit.hide()`, так и к методам `Animal`, таким как `rabbit.run()`. + +Внутри ключевое слово `extends` работает по старой доброй механике прототипов. Оно устанавливает `Rabbit.prototype.[[Prototype]]` в `Animal.prototype`. Таким образом, если метода не оказалось в `Rabbit.prototype`, JavaScript берет его из `Animal.prototype`. ![](animal-rabbit-extends.svg) -Как мы помним из главы , в JavaScript используется наследование на прототипах для встроенных объектов. Например `Date.prototype.[[Prototype]]` это `Object.prototype`, поэтому у дат есть универсальные методы объекта. +Например, чтобы найти метод `rabbit.run`, движок проверяет (снизу вверх на картинке): +1. Объект `rabbit` (не имеет `run`). +2. Его прототип, то есть `Rabbit.prototype` (имеет `hide`, но не имеет `run`). +3. Его прототип, то есть (вследствие `extends`) `Animal.prototype`, в котором, наконец, есть метод `run`. + +Как мы помним из главы , сам JavaScript использует наследование на прототипах для встроенных объектов. Например, `Date.prototype.[[Prototype]]` является `Object.prototype`, поэтому у дат есть универсальные методы объекта. ````smart header="После `extends` разрешены любые выражения" -Синтаксис создания класса допускает указывать после `extends` не только класс, но любое выражение. +Синтаксис создания класса допускает указывать после `extends` не только класс, но и любое выражение. Пример вызова функции, которая генерирует родительский класс: ```js run function f(phrase) { return class { - sayHi() { alert(phrase) } - } + sayHi() { alert(phrase); } + }; } *!* @@ -113,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(...)` для вызова родительского конструктора (работает только внутри нашего конструктора). Пусть наш кролик автоматически прячется при остановке: @@ -178,6 +160,7 @@ rabbit.stop(); // Белый кролик стоит. Белый кролик п Как упоминалось в главе , стрелочные функции не имеют `super`. При обращении к `super` стрелочной функции он берётся из внешней функции: + ```js class Rabbit extends Animal { stop() { @@ -247,11 +230,13 @@ let rabbit = new Rabbit("Белый кролик", 10); // Error: this is not de Упс! При создании кролика - ошибка! Что не так? -Если коротко, то в классах-потомках конструктор обязан вызывать `super(...)`, и (!) делать это перед использованием `this`. +Если коротко, то: + +- **Конструкторы в наследуемых классах должны обязательно вызывать `super(...)`, и (!) делать это перед использованием `this`.** ...Но почему? Что происходит? Это требование кажется довольно странным. -Конечно, всему есть объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит. +Конечно, всему есть своё объяснение. Давайте углубимся в детали, чтобы вы действительно поняли, что происходит. В JavaScript существует различие между "функцией-конструктором наследующего класса" и всеми остальными. В наследующем классе соответствующая функция-конструктор помечена специальным внутренним свойством `[[ConstructorKind]]:"derived"`. @@ -295,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`. Здесь есть некоторые интересные моменты. @@ -316,6 +395,7 @@ alert(rabbit.earLength); // 10 Вы можете пропустить эту часть и перейти ниже к подсекции `[[HomeObject]]`, если не хотите знать детали. Вреда не будет. Или читайте далее, если хотите разобраться. В примере ниже `rabbit.__proto__ = animal`. Попробуем в `rabbit.eat()` вызвать `animal.eat()`, используя `this.__proto__`: + ```js run let animal = { name: "Animal", @@ -417,7 +497,6 @@ longEar.eat(); // Error: Maximum call stack size exceeded Давайте посмотрим, как это работает - опять же, используя простые объекты: - ```js run let animal = { name: "Животное", @@ -463,7 +542,7 @@ longEar.eat(); // Длинноух ест. ```js run let animal = { sayHi() { - console.log("Я животное"); + alert("Я животное"); } }; @@ -477,7 +556,7 @@ let rabbit = { let plant = { sayHi() { - console.log("Я растение"); + alert("Я растение"); } }; @@ -547,4 +626,4 @@ rabbit.eat(); // Ошибка вызова super (потому что нет [[ - Поэтому копировать метод, использующий `super`, между разными объектами небезопасно. Также: -- У функций-стрелок нет своего `this` и `super`, поэтому они "прозрачно" встраиваются во внешний контекст. +- У стрелочных функций нет своего `this` и `super`, поэтому они "прозрачно" встраиваются во внешний контекст. diff --git a/1-js/09-classes/03-static-properties-methods/1-class-extend-object/solution.md b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/solution.md index ad7402930c..3c1a42f600 100644 --- a/1-js/09-classes/03-static-properties-methods/1-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 {} @@ -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/03-static-properties-methods/1-class-extend-object/task.md b/1-js/09-classes/03-static-properties-methods/1-class-extend-object/task.md index 407b04dafe..55586e055a 100644 --- a/1-js/09-classes/03-static-properties-methods/1-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 --- 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 56b26311f4..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 { @@ -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] Статические свойства и методы наследуются. 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 4a3694b9e4..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 @@ -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 */!* @@ -260,7 +260,7 @@ class MegaCoffeeMachine extends CoffeeMachine { ````warn Приватные поля особенные. -Как мы помним, обычно мы можем получить доступ к полям объекта с помощью this[name]: +Как мы помним, обычно мы можем получить доступ к полям объекта с помощью `this[name]`: ```js class User { diff --git a/1-js/09-classes/06-instanceof/article.md b/1-js/09-classes/06-instanceof/article.md index c246ed5c22..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` позволяет проверить, принадлежит ли объект указанному классу, с учётом наследования. Такая проверка может потребоваться во многих случаях. Здесь мы используем её для создания *полиморфной* функции, которая интерпретирует аргументы по-разному в зависимости от их типа. @@ -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/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md index f5afa3ee14..e92a1aae8a 100644 --- a/1-js/09-classes/07-mixins/article.md +++ b/1-js/09-classes/07-mixins/article.md @@ -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}`)); */!* 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 669356eb3c..5a9e089544 100644 --- a/1-js/10-error-handling/1-try-catch/article.md +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -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 b in JSON at position 2 + alert(e.message); // Expected property name or '}' in JSON at position 2 (line 1 column 3) } ``` @@ -628,7 +628,7 @@ window.onerror = function(message, url, line, col, error) { Роль глобального обработчика `window.onerror` обычно заключается не в восстановлении выполнения скрипта -- это скорее всего невозможно в случае программной ошибки, а в отправке сообщения об ошибке разработчикам. -Существуют также веб-сервисы, которые предоставляют логирование ошибок для таких случаев, такие как или . +Существуют также веб-сервисы, которые предоставляют логирование ошибок для таких случаев, такие как . Они работают так: 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 4d522392b3..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,15 +61,15 @@ newFunction(); // такой функции не существует! function loadScript(src, *!*callback*/!*) { let script = document.createElement('script'); script.src = src; - *!* script.onload = () => callback(script); */!* - document.head.append(script); } ``` +Событие `onload` описано в статье , оно в основном выполняет функцию после загрузки и выполнения скрипта. + Теперь, если мы хотим вызвать функцию из скрипта, нужно делать это в колбэке: ```js 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 20ef3df032..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 @@ -210,59 +211,84 @@ 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` для загрузки скрипта из предыдущей главы. Давайте вспомним, как выглядел вариант с колбэками: 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 1ed00b191b..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 bb0b918db4..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/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/05-promise-api/article.md b/1-js/11-async/05-promise-api/article.md index 9e24a48b8e..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`. Например, если у нас есть массив ссылок, то мы можем загрузить их вот так: @@ -123,6 +123,12 @@ Promise.all([ [recent browser="new"] +Синтаксис: + +```js +let promise = Promise.allSettled(iterable); +``` + `Promise.all` завершается с ошибкой, если она возникает в любом из переданных промисов. Это подходит для ситуаций "всё или ничего", когда нам нужны *все* результаты для продолжения: ```js @@ -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/12-generators-iterators/1-generators/generateSequence-2.svg b/1-js/12-generators-iterators/1-generators/generateSequence-2.svg index 4c64e983e1..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 0af8e9efdc..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/13-modules/01-modules-intro/article.md b/1-js/13-modules/01-modules-intro/article.md index 6da06ae14e..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. +``` + ## Основные возможности модулей Чем отличаются модули от "обычных" скриптов? diff --git a/1-js/13-modules/02-import-export/article.md b/1-js/13-modules/02-import-export/article.md index ecccec16e0..a1d5f586b5 100644 --- a/1-js/13-modules/02-import-export/article.md +++ b/1-js/13-modules/02-import-export/article.md @@ -93,7 +93,7 @@ say.sayBye('John'); Для этого есть несколько причин. -1. Современные инструменты сборки ([webpack](http://webpack.github.io) и другие) собирают модули вместе и оптимизируют их, ускоряя загрузку и удаляя неиспользуемый код. +1. Современные инструменты сборки ([webpack](https://webpack.js.org) и другие) собирают модули вместе и оптимизируют их, ускоряя загрузку и удаляя неиспользуемый код. Предположим, мы добавили в наш проект стороннюю библиотеку `say.js` с множеством функций: ```js diff --git a/1-js/99-js-misc/01-proxy/article.md b/1-js/99-js-misc/01-proxy/article.md index 950f5b96e8..1c1e77c3e6 100644 --- a/1-js/99-js-misc/01-proxy/article.md +++ b/1-js/99-js-misc/01-proxy/article.md @@ -454,7 +454,6 @@ user = { Впрочем, приватные свойства имеют свои недостатки. В частности, они не наследуются. ``` - ## "В диапазоне" с ловушкой "has" Давайте посмотрим ещё примеры. 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 acaa504456..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: "Guest" name: getter_name: "Admin"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/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 99% 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 2343f1b387..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 @@ -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/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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 100% rename from 1-js/99-js-misc/04-bitwise-operators/3-bitwise-symmetry/task.md rename to 1-js/99-js-misc/05-bitwise-operators/3-bitwise-symmetry/task.md diff --git a/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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 1-js/99-js-misc/04-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/1-js/99-js-misc/04-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/1-js/99-js-misc/04-bitwise-operators/article.md b/1-js/99-js-misc/05-bitwise-operators/article.md similarity index 99% rename from 1-js/99-js-misc/04-bitwise-operators/article.md rename to 1-js/99-js-misc/05-bitwise-operators/article.md index 97a149d8d4..ee23388ecc 100644 --- a/1-js/99-js-misc/04-bitwise-operators/article.md +++ b/1-js/99-js-misc/05-bitwise-operators/article.md @@ -262,7 +262,7 @@ alert( access2 ); // 11000 Ключ: 1220461917 в двоичном виде: 01001000101111101100010101011101 -Результат операции 1220461917 ^ key: +Результат операции 1220461908 ^ key: 00000000000000000000000000001001 Результат в 10-ной системе (исходное сообщение): 9 diff --git a/1-js/99-js-misc/04-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 1-js/99-js-misc/04-bitwise-operators/head.html rename to 1-js/99-js-misc/05-bitwise-operators/head.html diff --git a/1-js/99-js-misc/05-bigint/article.md b/1-js/99-js-misc/06-bigint/article.md similarity index 97% rename from 1-js/99-js-misc/05-bigint/article.md rename to 1-js/99-js-misc/06-bigint/article.md index fcc961b768..5f92b46238 100644 --- a/1-js/99-js-misc/05-bigint/article.md +++ b/1-js/99-js-misc/06-bigint/article.md @@ -50,12 +50,14 @@ alert(Number(bigint) + number); // 3 ````smart header="К `BigInt` числам нельзя применить унарный оператор `+`" Унарный оператор `+value` является хорошо известным способом конвертировать произвольное значение `value` в число. -Данный оператор не поддерживается при работе с `BigInt` числами. +Данный оператор не поддерживается при работе с `BigInt` числами: ```js run let bigint = 1n; -alert( +bigint ); // SyntaxError: Unexpected identifier +alert( +bigint ); // Ошибка! ``` + +Мы должны использовать `Number()` для преобразования `bigint` к `number`. ```` ## Операции сравнения 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 95% rename from 1-js/99-js-misc/90-intl/article.md rename to 1-js/99-js-misc/08-intl/article.md index 710f7f5bc5..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), предназначенный решить эти проблемы навсегда. ## Основные объекты @@ -45,7 +45,7 @@ Также через суффикс `-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). Все методы принимают локаль в виде строки или массива, содержащего несколько локалей в порядке предпочтения. @@ -244,7 +244,7 @@ let 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` -- по умолчанию, на усмотрение окружения (браузера). Использование: @@ -331,7 +331,7 @@ formatter.format(number); // форматирование currency Алфавитный код валюты - См. Список кодов валюты, например USD + См. Список кодов валюты, например USD @@ -377,7 +377,7 @@ formatter.format(number); // форматирование maximumSignificantDigits Максимальное количество значимых цифр от minimumSignificantDigits до 21 - minimumSignificantDigits + 21 @@ -399,7 +399,7 @@ alert( formatter.format(1234567890.123) ); // 1 230 000 000 С опциями для валюты: -```js +```js run let formatter = new Intl.NumberFormat("ru", { style: "currency", currency: "GBP" @@ -410,7 +410,7 @@ alert( formatter.format(1234.5) ); // 1 234,5 £ С двумя цифрами после запятой: -```js +```js run let formatter = new Intl.NumberFormat("ru", { style: "currency", currency: "GBP", 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/2-ui/1-document/01-browser-environment/article.md b/2-ui/1-document/01-browser-environment/article.md index bcae22be2a..a461966b8b 100644 --- a/2-ui/1-document/01-browser-environment/article.md +++ b/2-ui/1-document/01-browser-environment/article.md @@ -17,7 +17,7 @@ Например, здесь мы используем `window` как глобальный объект: -```js run +```js run global function sayHi() { alert("Hello"); } @@ -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/02-dom-nodes/article.md b/2-ui/1-document/02-dom-nodes/article.md index c3c556625f..0856d16e75 100644 --- a/2-ui/1-document/02-dom-nodes/article.md +++ b/2-ui/1-document/02-dom-nodes/article.md @@ -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/article.md b/2-ui/1-document/03-dom-navigation/article.md index ae7e533532..d8ef379328 100644 --- a/2-ui/1-document/03-dom-navigation/article.md +++ b/2-ui/1-document/03-dom-navigation/article.md @@ -86,7 +86,7 @@ DOM позволяет нам делать что угодно с элемент ``` -...А потомки ``-- это и прямые дети `
`, `