diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..d3877a5382 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.svg binary diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..490051876d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: iliakan diff --git a/.gitignore b/.gitignore index 6f90fd1907..1a71fb7c82 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ sftp-config.json Thumbs.db +/svgs \ No newline at end of file diff --git a/1-js/01-getting-started/1-intro/article.md b/1-js/01-getting-started/1-intro/article.md index 69bb189af8..2f4f518f3e 100644 --- a/1-js/01-getting-started/1-intro/article.md +++ b/1-js/01-getting-started/1-intro/article.md @@ -1,53 +1,53 @@ # An Introduction to JavaScript -Let's see what's so special about JavaScript, what we can achieve with it and which other technologies play well with it. +Let's see what's so special about JavaScript, what we can achieve with it, and what other technologies play well with it. ## What is JavaScript? -*JavaScript* was initially created to *"make webpages alive"*. +*JavaScript* was initially created to "make web pages alive". -The programs in this language are called *scripts*. They can be written right in the HTML and execute automatically as the page loads. +The programs in this language are called *scripts*. They can be written right in a web page's HTML and run automatically as the page loads. -Scripts are provided and executed as a plain text. They don't need a special preparation or a compilation to run. +Scripts are provided and executed as plain text. They don't need special preparation or compilation to run. -In this aspect, JavaScript is very different from another language called [Java](http://en.wikipedia.org/wiki/Java). +In this aspect, JavaScript is very different from another language called [Java](https://en.wikipedia.org/wiki/Java_(programming_language)). -```smart header="Why JavaScript?" -When JavaScript was created, it initially had another name: "LiveScript". But Java language was very popular at that time, so it was decided that positioning a new language as a "younger brother" of Java would help. +```smart header="Why is it called JavaScript?" +When JavaScript was created, it initially had another name: "LiveScript". But Java was very popular at that time, so it was decided that positioning a new language as a "younger brother" of Java would help. -But as it evolved, JavaScript became a fully independent language, with its own specification called [ECMAScript](http://en.wikipedia.org/wiki/ECMAScript), and now it has no relation to Java at all. +But as it evolved, JavaScript became a fully independent language with its own specification called [ECMAScript](http://en.wikipedia.org/wiki/ECMAScript), and now it has no relation to Java at all. ``` -At present, JavaScript can execute not only in the browser, but also on the server, or actually on any device where there exists a special program called [the JavaScript engine](https://en.wikipedia.org/wiki/JavaScript_engine). +Today, JavaScript can execute not only in the browser, but also on the server, or actually on any device that has a special program called [the JavaScript engine](https://en.wikipedia.org/wiki/JavaScript_engine). -The browser has an embedded engine, sometimes it's also called a "JavaScript virtual machine". +The browser has an embedded engine sometimes called a "JavaScript virtual machine". -Different engines have different "codenames", for example: +Different engines have different "codenames". For example: -- [V8](https://en.wikipedia.org/wiki/V8_(JavaScript_engine)) -- in Chrome and Opera. +- [V8](https://en.wikipedia.org/wiki/V8_(JavaScript_engine)) -- in Chrome, Opera and Edge. - [SpiderMonkey](https://en.wikipedia.org/wiki/SpiderMonkey) -- in Firefox. -- ...There are other codenames like "Trident", "Chakra" for different versions of IE, "ChakraCore" for Microsoft Edge, "Nitro" and "SquirrelFish" for Safari etc. +- ...There are other codenames like "Chakra" for IE, "JavaScriptCore", "Nitro" and "SquirrelFish" for Safari, etc. -The terms above are good to remember, because they are used in developer articles on the internet. We'll use them too. For instance, if "a feature X is supported by V8", then it probably works in Chrome and Opera. +The terms above are good to remember because they are used in developer articles on the internet. We'll use them too. For instance, if "a feature X is supported by V8", then it probably works in Chrome, Opera and Edge. ```smart header="How do engines work?" Engines are complicated. But the basics are easy. 1. The engine (embedded if it's a browser) reads ("parses") the script. -2. Then it converts ("compiles") the script to the machine language. +2. Then it converts ("compiles") the script to machine code. 3. And then the machine code runs, pretty fast. -The engine applies optimizations on every stage of the process. It even watches the compiled script as it runs, analyzes the data that flows through it and applies optimizations to the machine code based on that knowledge. At the end, scripts are quite fast. +The engine applies optimizations at each step of the process. It even watches the compiled script as it runs, analyzes the data that flows through it, and further optimizes the machine code based on that knowledge. ``` ## What can in-browser JavaScript do? -The modern JavaScript is a "safe" programming language. It does not provide low-level access to memory or CPU, because it was initially created for browsers which do not require it. +Modern JavaScript is a "safe" programming language. It does not provide low-level access to memory or the CPU, because it was initially created for browsers which do not require it. -The capabilities greatly depend on the environment that runs JavaScript. For instance, [Node.JS](https://wikipedia.org/wiki/Node.js) supports functions that allow JavaScript to read/write arbitrary files, perform network requests etc. +JavaScript's capabilities greatly depend on the environment it's running in. For instance, [Node.js](https://wikipedia.org/wiki/Node.js) supports functions that allow JavaScript to read/write arbitrary files, perform network requests, etc. -In-browser JavaScript can do everything related to webpage manipulation, interaction with the user and the webserver. +In-browser JavaScript can do everything related to webpage manipulation, interaction with the user, and the webserver. For instance, in-browser JavaScript is able to: @@ -59,25 +59,25 @@ For instance, in-browser JavaScript is able to: ## What CAN'T in-browser JavaScript do? -JavaScript's abilities in the browser are limited for the sake of the user's safety. The aim is to prevent an evil webpage from accessing private information or harming the user's data. +JavaScript's abilities in the browser are limited to protect the user's safety. The aim is to prevent an evil webpage from accessing private information or harming the user's data. -The examples of such restrictions are: +Examples of such restrictions include: -- JavaScript on a webpage may not read/write arbitrary files on the hard disk, copy them or execute programs. It has no direct access to OS system functions. +- JavaScript on a webpage may not read/write arbitrary files on the hard disk, copy them or execute programs. It has no direct access to OS functions. Modern browsers allow it to work with files, but the access is limited and only provided if the user does certain actions, like "dropping" a file into a browser window or selecting it via an `` tag. - There are ways to interact with camera/microphone and other devices, but they require a user's explicit permission. So a JavaScript-enabled page may not sneakily enable a web-camera, observe the surroundings and send the information to the [NSA](https://en.wikipedia.org/wiki/National_Security_Agency). -- Different tabs/windows generally do not know about each other. Sometimes they do, for example when one window uses JavaScript to open the other one. But even in this case, JavaScript from one page may not access the other if they come from different sites (from a different domain, protocol or port). + There are ways to interact with the camera/microphone and other devices, but they require a user's explicit permission. So a JavaScript-enabled page may not sneakily enable a web-camera, observe the surroundings and send the information to the [NSA](https://en.wikipedia.org/wiki/National_Security_Agency). +- Different tabs/windows generally do not know about each other. Sometimes they do, for example when one window uses JavaScript to open the other one. But even in this case, JavaScript from one page may not access the other page if they come from different sites (from a different domain, protocol or port). - This is called the "Same Origin Policy". To work around that, *both pages* must contain a special JavaScript code that handles data exchange. + This is called the "Same Origin Policy". To work around that, *both pages* must agree for data exchange and must contain special JavaScript code that handles it. We'll cover that in the tutorial. - The limitation is again for user's safety. A page from `http://anysite.com` which a user has opened must not be able to access another browser tab with the URL `http://gmail.com` and steal information from there. -- JavaScript can easily communicate over the net to the server where the current page came from. But its ability to receive data from other sites/domains is crippled. Though possible, it requires explicit agreement (expressed in HTTP headers) from the remote side. Once again, that's safety limitations. + This limitation is, again, for the user's safety. A page from `http://anysite.com` which a user has opened must not be able to access another browser tab with the URL `http://gmail.com`, for example, and steal information from there. +- JavaScript can easily communicate over the net to the server where the current page came from. But its ability to receive data from other sites/domains is crippled. Though possible, it requires explicit agreement (expressed in HTTP headers) from the remote side. Once again, that's a safety limitation. -![](limitations.png) +![](limitations.svg) -Such limits do not exist if JavaScript is used outside of the browser, for example on a server. Modern browsers also allow installing plugin/extensions which may get extended permissions. +Such limitations do not exist if JavaScript is used outside of the browser, for example on a server. Modern browsers also allow plugins/extensions which may ask for extended permissions. ## What makes JavaScript unique? @@ -85,16 +85,14 @@ There are at least *three* great things about JavaScript: ```compare + Full integration with HTML/CSS. -+ Simple things done simply. ++ Simple things are done simply. + Supported by all major browsers and enabled by default. ``` +JavaScript is the only browser technology that combines these three things. -Combined, these three things exist only in JavaScript and no other browser technology. - -That's what makes JavaScript unique. That's why it's the most widespread tool to create browser interfaces. - -While planning to learn a new technology, it's beneficial to check its perspectives. So let's move on to the modern trends that include new languages and browser abilities. +That's what makes JavaScript unique. That's why it's the most widespread tool for creating browser interfaces. +That said, JavaScript can be used to create servers, mobile applications, etc. ## Languages "over" JavaScript @@ -102,20 +100,23 @@ The syntax of JavaScript does not suit everyone's needs. Different people want d That's to be expected, because projects and requirements are different for everyone. -So recently a plethora of new languages appeared, which are *transpiled* (converted) to JavaScript before they run in the browser. +So, recently a plethora of new languages appeared, which are *transpiled* (converted) to JavaScript before they run in the browser. -Modern tools make the transpilation very fast and transparent, actually allowing developers to code in another language and autoconverting it "under the hood". +Modern tools make the transpilation very fast and transparent, actually allowing developers to code in another language and auto-converting it "under the hood". Examples of such languages: -- [CoffeeScript](http://coffeescript.org/) is a "syntactic sugar" for JavaScript, it introduces shorter syntax, allowing to write more precise and clear code. Usually Ruby devs like it. -- [TypeScript](http://www.typescriptlang.org/) is concentrated on adding "strict data typing", to simplify development and support of complex systems. It is developed by Microsoft. -- [Dart](https://www.dartlang.org/) is a standalone language that has its own engine that runs in non-browser environments (like mobile apps). It was initially offered by Google as a replacement for JavaScript, but as of now, browsers require it to be transpiled to JavaScript just like the ones above. +- [CoffeeScript](https://coffeescript.org/) is "syntactic sugar" for JavaScript. It introduces shorter syntax, allowing us to write clearer and more precise code. Usually, Ruby devs like it. +- [TypeScript](https://www.typescriptlang.org/) is concentrated on adding "strict data typing" to simplify the development and support of complex systems. It is developed by Microsoft. +- [Flow](https://flow.org/) also adds data typing, but in a different way. Developed by Facebook. +- [Dart](https://www.dartlang.org/) is a standalone language that has its own engine that runs in non-browser environments (like mobile apps), but also can be transpiled to JavaScript. Developed by Google. +- [Brython](https://brython.info/) is a Python transpiler to JavaScript that enables the writing of applications in pure Python without JavaScript. +- [Kotlin](https://kotlinlang.org/docs/reference/js-overview.html) is a modern, concise and safe programming language that can target the browser or Node. -There are more. Of course even if we use one of those languages, we should also know JavaScript, to really understand what we're doing. +There are more. Of course, even if we use one of these transpiled languages, we should also know JavaScript to really understand what we're doing. ## Summary -- JavaScript was initially created as a browser-only language, but now it is used in many other environments as well. -- At this moment, JavaScript has a unique position as the most widely-adopted browser language with full integration with HTML/CSS. +- JavaScript was initially created as a browser-only language, but it is now used in many other environments as well. +- Today, JavaScript has a unique position as the most widely-adopted browser language, fully integrated with HTML/CSS. - There are many languages that get "transpiled" to JavaScript and provide certain features. It is recommended to take a look at them, at least briefly, after mastering JavaScript. diff --git a/1-js/01-getting-started/1-intro/limitations.png b/1-js/01-getting-started/1-intro/limitations.png deleted file mode 100644 index d5d315c5c1..0000000000 Binary files a/1-js/01-getting-started/1-intro/limitations.png and /dev/null differ diff --git a/1-js/01-getting-started/1-intro/limitations.svg b/1-js/01-getting-started/1-intro/limitations.svg new file mode 100644 index 0000000000..76ea43fd7a --- /dev/null +++ b/1-js/01-getting-started/1-intro/limitations.svg @@ -0,0 +1 @@ +https://javascript.info<script> ... </script>https://gmail.comhttps://javascript.info \ No newline at end of file diff --git a/1-js/01-getting-started/1-intro/limitations@2x.png b/1-js/01-getting-started/1-intro/limitations@2x.png deleted file mode 100644 index 01483997da..0000000000 Binary files a/1-js/01-getting-started/1-intro/limitations@2x.png and /dev/null differ diff --git a/1-js/01-getting-started/2-code-editors/article.md b/1-js/01-getting-started/2-code-editors/article.md deleted file mode 100644 index 0d714cdb27..0000000000 --- a/1-js/01-getting-started/2-code-editors/article.md +++ /dev/null @@ -1,56 +0,0 @@ -# Code editors - -A code editor is the place where programmers spend most of their time. - -There are two archetypes: IDE and lightweight editors. Many people feel comfortable choosing one tool of each type. - -## IDE - -The term [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) (Integrated Development Environment) means a powerful editor with many features that usually operates on a "whole project". As the name suggests, that's not just an editor, but a full-scale "development environment". - -An IDE loads the project (can be many files), allows navigation between files, provides autocompletion based on the whole project (not just the open file), integrates with a version management system (like [git](https://git-scm.com/)), a testing environment and other "project-level" stuff. - -If you haven't considered selecting an IDE yet, look at the following variants: - -- [WebStorm](http://www.jetbrains.com/webstorm/) for frontend development and other editors of the same company if you need additional languages. -- Visual Studio is fine if you're a .NET developer, and a free version is available ([Visual Studio Community](https://www.visualstudio.com/vs/community/)) -- [Netbeans](http://netbeans.org/). - -All of the IDEs except Visual Studio are available on Windows, MacOs and Linux. Visual Studio doesn't work on Linux. - -Most IDEs are paid, but have a trial period. Their cost is usually negligible compared to a qualified developer's salary, so just choose the best one for you. - -## Lightweight editors - -"Lightweight editors" are not as powerful as IDEs, but they're fast, elegant and simple. - -They are mainly used to instantly open and edit a file. - -The main difference between a "lightweight editor" and an "IDE" is that an IDE works on a project-level, so it loads much more data on start, analyzes the project structure if needed and so on. A lightweight editor is much faster if we need only one file. - -In practice, lightweight editors may have a lot of plugins including directory-level syntax analyzers and autocompleters, so there's no strict border between a lightweight editor and an IDE. - -The following options deserve your attention: - -- [Visual Studio Code](https://code.visualstudio.com/) (cross-platform, free). -- [Atom](https://atom.io/) (cross-platform, free). -- [Sublime Text](http://www.sublimetext.com) (cross-platform, shareware). -- [Notepad++](https://notepad-plus-plus.org/) (Windows, free). -- [Vim](http://www.vim.org/) and [Emacs](https://www.gnu.org/software/emacs/) are also cool, if you know how to use them. - -## My favorites - -The personal preference of the author is to have both an IDE for projects and a lightweight editor for quick and easy file editing. - -I'm using: - -- [WebStorm](http://www.jetbrains.com/webstorm/) for JS, and if there is one more language in the project, then I switch to one of the other Jetbrains offerings listed above. -- As a lightweight editor -- [Sublime Text](http://www.sublimetext.com) or [Atom](https://atom.io/). - -## Let's not argue - -The editors in the lists above are those that either I or my friends who I consider good developers have been using for a long time and are happy with. - -There are other great editors in our big world. Please choose the one you like the most. - -The choice of an editor, like any other tool, is individual and depends on your projects, habits, personal preferences. diff --git a/1-js/01-getting-started/2-manuals-specifications/article.md b/1-js/01-getting-started/2-manuals-specifications/article.md new file mode 100644 index 0000000000..3fa2433363 --- /dev/null +++ b/1-js/01-getting-started/2-manuals-specifications/article.md @@ -0,0 +1,37 @@ + +# Manuals and specifications + +This book is a *tutorial*. It aims to help you gradually learn the language. But once you're familiar with the basics, you'll need other resources. + +## Specification + +[The ECMA-262 specification](https://www.ecma-international.org/publications/standards/Ecma-262.htm) contains the most in-depth, detailed and formalized information about JavaScript. It defines the language. + +But being that formalized, it's difficult to understand at first. So if you need the most trustworthy source of information about the language details, the specification is the right place. But it's not for everyday use. + +A new specification version is released every year. Between these releases, the latest specification draft is at . + +To read about new bleeding-edge features, including those that are "almost standard" (so-called "stage 3"), see proposals at . + +Also, if you're developing for the browser, then there are other specifications covered in the [second part](info:browser-environment) of the tutorial. + +## Manuals + +- **MDN (Mozilla) JavaScript Reference** is the main manual with examples and other information. It's great to get in-depth information about individual language functions, methods etc. + + You can find it at . + +Although, it's often best to use an internet search instead. Just use "MDN [term]" in the query, e.g. to search for the `parseInt` function. + +## Compatibility tables + +JavaScript is a developing language, new features get added regularly. + +To see their support among browser-based and other engines, see: + +- - per-feature tables of support, e.g. to see which engines support modern cryptography functions: . +- - a table with language features and engines that support those or don't support. + +All these resources are useful in real-life development, as they contain valuable information about language details, their support, etc. + +Please remember them (or this page) for the cases when you need in-depth information about a particular feature. diff --git a/1-js/01-getting-started/3-code-editors/article.md b/1-js/01-getting-started/3-code-editors/article.md new file mode 100644 index 0000000000..ca61947412 --- /dev/null +++ b/1-js/01-getting-started/3-code-editors/article.md @@ -0,0 +1,49 @@ +# Code editors + +A code editor is the place where programmers spend most of their time. + +There are two main types of code editors: IDEs and lightweight editors. Many people use one tool of each type. + +## IDE + +The term [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) (Integrated Development Environment) refers to a powerful editor with many features that usually operates on a "whole project." As the name suggests, it's not just an editor, but a full-scale "development environment." + +An IDE loads the project (which can be many files), allows navigation between files, provides autocompletion based on the whole project (not just the open file), and integrates with a version management system (like [git](https://git-scm.com/)), a testing environment, and other "project-level" stuff. + +If you haven't selected an IDE yet, consider the following options: + +- [Visual Studio Code](https://code.visualstudio.com/) (cross-platform, free). +- [WebStorm](https://www.jetbrains.com/webstorm/) (cross-platform, paid). + +For Windows, there's also "Visual Studio", not to be confused with "Visual Studio Code". "Visual Studio" is a paid and mighty Windows-only editor, well-suited for the .NET platform. It's also good at JavaScript. There's also a free version [Visual Studio Community](https://www.visualstudio.com/vs/community/). + +Many IDEs are paid, but have a trial period. Their cost is usually negligible compared to a qualified developer's salary, so just choose the best one for you. + +## Lightweight editors + +"Lightweight editors" are not as powerful as IDEs, but they're fast, elegant and simple. + +They are mainly used to open and edit a file instantly. + +The main difference between a "lightweight editor" and an "IDE" is that an IDE works on a project-level, so it loads much more data on start, analyzes the project structure if needed and so on. A lightweight editor is much faster if we need only one file. + +In practice, lightweight editors may have a lot of plugins including directory-level syntax analyzers and autocompleters, so there's no strict border between a lightweight editor and an IDE. + +There are many options, for instance: + +- [Sublime Text](https://www.sublimetext.com/) (cross-platform, shareware). +- [Notepad++](https://notepad-plus-plus.org/) (Windows, free). +- [Vim](https://www.vim.org/) and [Emacs](https://www.gnu.org/software/emacs/) are also cool if you know how to use them. + +## Let's not argue + +The editors in the lists above are those that either I or my friends whom I consider good developers have been using for a long time and are happy with. + +There are other great editors in our big world. Please choose the one you like the most. + +The choice of an editor, like any other tool, is individual and depends on your projects, habits, and personal preferences. + +The author's personal opinion: + +- I'd use [Visual Studio Code](https://code.visualstudio.com/) if I develop mostly frontend. +- Otherwise, if it's mostly another language/platform and partially frontend, then consider other editors, such as XCode (Mac), Visual Studio (Windows) or Jetbrains family (Webstorm, PHPStorm, RubyMine etc, depending on the language). diff --git a/1-js/01-getting-started/3-devtools/article.md b/1-js/01-getting-started/3-devtools/article.md deleted file mode 100644 index f250502df0..0000000000 --- a/1-js/01-getting-started/3-devtools/article.md +++ /dev/null @@ -1,58 +0,0 @@ -# Developer console - -Code is prone to errors. You are quite likely to make errors... Oh, what am I talking about? You are *absolutely* going to make errors, at least if you're a human, not a [robot](https://en.wikipedia.org/wiki/Bender_(Futurama)). - -But in the browser, a user doesn't see the errors by default. So, if something goes wrong in the script, we won't see what's broken and can't fix it. - -To see errors and get a lot of other useful information about scripts, browsers have embedded "developer tools". - -Most often developers lean towards Chrome or Firefox for development, because those browsers have the best developer tools. Other browsers also provide developer tools, sometimes with special features, but are usually playing "catch-up" to Chrome or Firefox. So most people have a "favorite" browser and switch to others if a problem is browser-specific. - -Developer tools are really powerful, there are many features. To start, we'll learn how to open them, look at errors and run JavaScript commands. - -## Google Chrome - -Open the page [bug.html](bug.html). - -There's an error in the JavaScript code on it. It's hidden from a regular visitor's eyes, so let's open developer tools to see it. - -Press `key:F12` or, if you're on Mac, then `key:Cmd+Opt+J`. - -The developer tools will open on the Console tab by default. - -It looks somewhat like this: - -![chrome](chrome.png) - -The exact look of developer tools depends on your version of Chrome. It changes from time to time, but should be similar. - -- Here we can see the red-colored error message. In this case the script contains an unknown "lalala" command. -- On the right, there is a clickable link to the source `bug.html:12` with the line number where the error has occurred. - -Below the error message there is a blue `>` symbol. It marks a "command line" where we can type JavaScript commands and press `key:Enter` to run them (`key:Shift+Enter` to input multi-line commands). - -Now we can see errors and that's enough for the start. We'll be back to developer tools later and cover debugging more in-depth in the chapter . - - -## Firefox, Edge and others - -Most other browsers use `key:F12` to open developer tools. - -The look & feel of them is quite similar. Once you know how to use one of them (you can start with Chrome), you can easily switch to another. - -## Safari - -Safari (Mac browser, not supported by Windows/Linux) is a little bit special here. We need to enable the "Develop menu" first. - -Open Preferences and go to "Advanced" pane. There's a checkbox at the bottom: - -![safari](safari.png) - -Now `key:Cmd+Opt+C` can toggle the console. Also note that the new top menu item named "Develop" has appeared. It has many commands and options. - -## Summary - -- Developer tools allow us to see errors, run commands, examine variables and much more. -- They can be opened with `key:F12` for most browsers under Windows. Chrome for Mac needs `key:Cmd+Opt+J`, Safari: `key:Cmd+Opt+C` (need to enable first). - -Now we have the environment ready. In the next section we'll get down to JavaScript. diff --git a/1-js/01-getting-started/3-devtools/chrome.png b/1-js/01-getting-started/3-devtools/chrome.png deleted file mode 100644 index fd029b747f..0000000000 Binary files a/1-js/01-getting-started/3-devtools/chrome.png and /dev/null differ diff --git a/1-js/01-getting-started/3-devtools/safari.png b/1-js/01-getting-started/3-devtools/safari.png deleted file mode 100644 index 37598a2610..0000000000 Binary files a/1-js/01-getting-started/3-devtools/safari.png and /dev/null differ diff --git a/1-js/01-getting-started/3-devtools/safari@2x.png b/1-js/01-getting-started/3-devtools/safari@2x.png deleted file mode 100644 index c59cebef21..0000000000 Binary files a/1-js/01-getting-started/3-devtools/safari@2x.png and /dev/null differ diff --git a/1-js/01-getting-started/4-devtools/article.md b/1-js/01-getting-started/4-devtools/article.md new file mode 100644 index 0000000000..50926d4f76 --- /dev/null +++ b/1-js/01-getting-started/4-devtools/article.md @@ -0,0 +1,63 @@ +# Developer console + +Code is prone to errors. You will quite likely make errors... Oh, what am I talking about? You are *absolutely* going to make errors, at least if you're a human, not a [robot](https://en.wikipedia.org/wiki/Bender_(Futurama)). + +But in the browser, users don't see errors by default. So, if something goes wrong in the script, we won't see what's broken and can't fix it. + +To see errors and get a lot of other useful information about scripts, "developer tools" have been embedded in browsers. + +Most developers lean towards Chrome or Firefox for development because those browsers have the best developer tools. Other browsers also provide developer tools, sometimes with special features, but are usually playing "catch-up" to Chrome or Firefox. So most developers have a "favorite" browser and switch to others if a problem is browser-specific. + +Developer tools are potent; they have many features. To start, we'll learn how to open them, look at errors, and run JavaScript commands. + +## Google Chrome + +Open the page [bug.html](bug.html). + +There's an error in the JavaScript code on it. It's hidden from a regular visitor's eyes, so let's open developer tools to see it. + +Press `key:F12` or, if you're on Mac, then `key:Cmd+Opt+J`. + +The developer tools will open on the Console tab by default. + +It looks somewhat like this: + +![chrome](chrome.png) + +The exact look of developer tools depends on your version of Chrome. It changes from time to time but should be similar. + +- Here we can see the red-colored error message. In this case, the script contains an unknown "lalala" command. +- On the right, there is a clickable link to the source `bug.html:12` with the line number where the error has occurred. + +Below the error message, there is a blue `>` symbol. It marks a "command line" where we can type JavaScript commands. Press `key:Enter` to run them. + +Now we can see errors, and that's enough for a start. We'll come back to developer tools later and cover debugging more in-depth in the chapter . + +```smart header="Multi-line input" +Usually, when we put a line of code into the console, and then press `key:Enter`, it executes. + +To insert multiple lines, press `key:Shift+Enter`. This way one can enter long fragments of JavaScript code. +``` + +## Firefox, Edge, and others + +Most other browsers use `key:F12` to open developer tools. + +The look & feel of them is quite similar. Once you know how to use one of these tools (you can start with Chrome), you can easily switch to another. + +## Safari + +Safari (Mac browser, not supported by Windows/Linux) is a little bit special here. We need to enable the "Develop menu" first. + +Open Preferences and go to the "Advanced" pane. There's a checkbox at the bottom: + +![safari](safari.png) + +Now `key:Cmd+Opt+C` can toggle the console. Also, note that the new top menu item named "Develop" has appeared. It has many commands and options. + +## Summary + +- Developer tools allow us to see errors, run commands, examine variables, and much more. +- They can be opened with `key:F12` for most browsers on Windows. Chrome for Mac needs `key:Cmd+Opt+J`, Safari: `key:Cmd+Opt+C` (need to enable first). + +Now we have the environment ready. In the next section, we'll get down to JavaScript. diff --git a/1-js/01-getting-started/3-devtools/bug.html b/1-js/01-getting-started/4-devtools/bug.html similarity index 100% rename from 1-js/01-getting-started/3-devtools/bug.html rename to 1-js/01-getting-started/4-devtools/bug.html diff --git a/1-js/01-getting-started/4-devtools/chrome.png b/1-js/01-getting-started/4-devtools/chrome.png new file mode 100644 index 0000000000..4cb3ea2f46 Binary files /dev/null and b/1-js/01-getting-started/4-devtools/chrome.png differ diff --git a/1-js/01-getting-started/3-devtools/chrome@2x.png b/1-js/01-getting-started/4-devtools/chrome@2x.png similarity index 100% rename from 1-js/01-getting-started/3-devtools/chrome@2x.png rename to 1-js/01-getting-started/4-devtools/chrome@2x.png diff --git a/1-js/01-getting-started/4-devtools/safari.png b/1-js/01-getting-started/4-devtools/safari.png new file mode 100644 index 0000000000..64c7a3f6ca Binary files /dev/null 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 new file mode 100644 index 0000000000..27def4d09b Binary files /dev/null and b/1-js/01-getting-started/4-devtools/safari@2x.png differ diff --git a/1-js/02-first-steps/01-hello-world/1-hello-alert/index.html b/1-js/02-first-steps/01-hello-world/1-hello-alert/index.html new file mode 100644 index 0000000000..ff1d871b08 --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/1-hello-alert/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md index e69de29bb2..81552913b9 100644 --- a/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md +++ b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md @@ -0,0 +1,2 @@ + +[html src="index.html"] 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 6bf4a94553..35f82bf5d7 100644 --- a/1-js/02-first-steps/01-hello-world/article.md +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -1,15 +1,15 @@ # Hello, world! -The tutorial that you're reading is about core JavaScript, which is platform-independent. Further on, you will learn Node.JS and other platforms that use it. +This part of the tutorial is about core JavaScript, the language itself. -But, we need a working environment to run our scripts, and, just because this book is online, the browser is a good choice. We'll keep the amount of browser-specific commands (like `alert`) to a minimum, so that you don't spend time on them if you plan to concentrate on another environment like Node.JS. On the other hand, browser details are explained in detail in the [next part](/ui) of the tutorial. +But we need a working environment to run our scripts and, since this book is online, the browser is a good choice. We'll keep the amount of browser-specific commands (like `alert`) to a minimum so that you don't spend time on them if you plan to concentrate on another environment (like Node.js). We'll focus on JavaScript in the browser in the [next part](/ui) of the tutorial. -So first, let's see how to attach a script to a webpage. For server-side environments, you can just execute it with a command like `"node my.js"` for Node.JS. +So first, let's see how we attach a script to a webpage. For server-side environments (like Node.js), you can execute the script with a command like `"node my.js"`. ## The "script" tag -JavaScript programs can be inserted in any part of an HTML document with the help of the ` ``` - These comments were supposed to hide the code from an old browser that didn't know about a ` ``` -Here `/path/to/script.js` is an absolute path to the file with the script (from the site root). - -It is also possible to provide a path relative to the current page. For instance, `src="script.js"` would mean a file `"script.js"` in the current folder. +Here, `/path/to/script.js` is an absolute path to the script from the site root. One can also provide a relative path from the current page. For instance, `src="script.js"`, just like `src="./script.js"`, would mean a file `"script.js"` in the current folder. -We can give a full URL as well, for instance: +We can give a full URL as well. For instance: ```html - + ``` To attach several scripts, use multiple tags: @@ -95,15 +92,15 @@ To attach several scripts, use multiple tags: ```smart As a rule, only the simplest scripts are put into HTML. More complex ones reside in separate files. -The benefit of a separate file is that the browser will download it and then store in its [cache](https://en.wikipedia.org/wiki/Web_cache). +The benefit of a separate file is that the browser will download it and store it in its [cache](https://en.wikipedia.org/wiki/Web_cache). -After this, other pages that want the same script will take it from the cache instead of downloading it. So the file is actually downloaded only once. +Other pages that reference the same script will take it from the cache instead of downloading it, so the file is actually downloaded only once. -That saves traffic and makes pages faster. +That reduces traffic and makes pages faster. ``` ````warn header="If `src` is set, the script content is ignored." -A single ` ``` -We must choose: either it's an external ``. -There is much more to learn about browser scripts and their interaction with the web-page. But let's keep in mind that this part of the tutorial is devoted to the JavaScript language, so we shouldn't distract ourselves from it. We'll be using a browser as a way to run JavaScript, which is very convenient for online reading, but yet one of many. +There is much more to learn about browser scripts and their interaction with the webpage. But let's keep in mind that this part of the tutorial is devoted to the JavaScript language, so we shouldn't distract ourselves with browser-specific implementations of it. We'll be using the browser as a way to run JavaScript, which is very convenient for online reading, but only one of many. diff --git a/1-js/02-first-steps/01-hello-world/hello-world-render.png b/1-js/02-first-steps/01-hello-world/hello-world-render.png deleted file mode 100644 index ffe810697c..0000000000 Binary files a/1-js/02-first-steps/01-hello-world/hello-world-render.png and /dev/null differ diff --git a/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png b/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png deleted file mode 100644 index c4411027ca..0000000000 Binary files a/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md index ebaa375279..e81fd343df 100644 --- a/1-js/02-first-steps/02-structure/article.md +++ b/1-js/02-first-steps/02-structure/article.md @@ -1,22 +1,22 @@ # Code structure -The first thing to study is the building blocks of the code. +The first thing we'll study is the building blocks of code. ## Statements Statements are syntax constructs and commands that perform actions. -We've already seen a statement `alert('Hello, world!')`, which shows the message. +We've already seen a statement, `alert('Hello, world!')`, which shows the message "Hello, world!". -We can have as many statements in the code as we want. Another statement can be separated with a semicolon. +We can have as many statements in our code as we want. Statements can be separated with a semicolon. -For example, here we split the message into two: +For example, here we split "Hello World" into two alerts: ```js run no-beautify alert('Hello'); alert('World'); ``` -Usually each statement is written on a separate line -- thus the code becomes more readable: +Usually, statements are written on separate lines to make the code more readable: ```js run no-beautify alert('Hello'); @@ -34,11 +34,11 @@ alert('Hello') alert('World') ``` -Here JavaScript interprets the line break as an "implicit" semicolon. That's also called an [automatic semicolon insertion](https://tc39.github.io/ecma262/#sec-automatic-semicolon-insertion). +Here, JavaScript interprets the line break as an "implicit" semicolon. This is called an [automatic semicolon insertion](https://tc39.github.io/ecma262/#sec-automatic-semicolon-insertion). -**In most cases a newline implies a semicolon. But "in most cases" does not mean "always"!** +**In most cases, a newline implies a semicolon. But "in most cases" does not mean "always"!** -There are cases when a newline does not mean a semicolon, for example: +There are cases when a newline does not mean a semicolon. For example: ```js run no-beautify alert(3 + @@ -46,7 +46,7 @@ alert(3 + + 2); ``` -The code outputs `6`, because JavaScript does not insert semicolons here. It is intuitively obvious that if the line ends with a plus `"+"`, then it is an "incomplete expression", no semicolon required. And in this case that works as intended. +The code outputs `6` because JavaScript does not insert semicolons here. It is intuitively obvious that if the line ends with a plus `"+"`, then it is an "incomplete expression", so a semicolon there would be incorrect. And in this case, that works as intended. **But there are situations where JavaScript "fails" to assume a semicolon where it is really needed.** @@ -56,49 +56,45 @@ Errors which occur in such cases are quite hard to find and fix. If you're curious to see a concrete example of such an error, check this code out: ```js run -[1, 2].forEach(alert) +alert("Hello"); + +[1, 2].forEach(alert); ``` -No need to think about the meaning of the brackets `[]` and `forEach` yet. We'll study them later, for now it does not matter. Let's just remember the result: it shows `1`, then `2`. +No need to think about the meaning of the brackets `[]` and `forEach` yet. We'll study them later. For now, just remember the result of running the code: it shows `Hello`, then `1`, then `2`. -Now let's add an `alert` before the code and *not* finish it with a semicolon: +Now let's remove the semicolon after the `alert`: ```js run no-beautify -alert("There will be an error") +alert("Hello") -[1, 2].forEach(alert) +[1, 2].forEach(alert); ``` -Now if we run it, only the first `alert` is shown, and then we have an error! - -But everything is fine again if we add a semicolon after `alert`: -```js run -alert("All fine now"); +The difference compared to the code above is only one character: the semicolon at the end of the first line is gone. -[1, 2].forEach(alert) -``` +If we run this code, only the first `Hello` shows (and there's an error, you may need to open the console to see it). There are no numbers any more. -Now we have the "All fine now" message and then `1` and `2`. +That's because JavaScript does not assume a semicolon before square brackets `[...]`. So, the code in the last example is treated as a single statement. - -The error in the no-semicolon variant occurs because JavaScript does not imply a semicolon before square brackets `[...]`. - -So, because the semicolon is not auto-inserted, the code in the first example is treated as a single statement. That's how the engine sees it: +Here's how the engine sees it: ```js run no-beautify -alert("There will be an error")[1, 2].forEach(alert) +alert("Hello")[1, 2].forEach(alert); ``` -But it should be two separate statements, not a single one. Such a merging in this case is just wrong, hence the error. There are other situations when such a thing happens. +Looks weird, right? Such merging in this case is just wrong. We need to put a semicolon after `alert` for the code to work correctly. + +This can happen in other situations also. ```` -It's recommended to put semicolons between statements even if they are separated by newlines. This rule is widely adopted by the community. Let's note once again -- *it is possible* to leave out semicolons most of the time. But it's safer -- especially for a beginner -- to use them. +We recommend putting semicolons between statements even if they are separated by newlines. This rule is widely adopted by the community. Let's note once again -- *it is possible* to leave out semicolons most of the time. But it's safer -- especially for a beginner -- to use them. -## Comments +## Comments [#code-comments] -As time goes on, the program becomes more and more complex. It becomes necessary to add *comments* which describe what happens and why. +As time goes on, programs become more and more complex. It becomes necessary to add *comments* which describe what the code does and why. -Comments can be put into any place of the script. They don't affect the execution, because the engine simply ignores them. +Comments can be put into any place of a script. They don't affect its execution because the engine simply ignores them. **One-line comments start with two forward slash characters `//`.** @@ -124,9 +120,9 @@ alert('Hello'); alert('World'); ``` -The content of comments is ignored, so if we put code inside /* ... */ it won't execute. +The content of comments is ignored, so if we put code inside /* ... */, it won't execute. -Sometimes it comes in handy to temporarily disable a part of code: +Sometimes it can be handy to temporarily disable a part of code: ```js run /* Commenting out the code @@ -136,7 +132,7 @@ alert('World'); ``` ```smart header="Use hotkeys!" -In most editors a line of code can be commented out by `key:Ctrl+/` hotkey for a single-line comment and something like `key:Ctrl+Shift+/` -- for multiline comments (select a piece of code and press the hotkey). For Mac try `key:Cmd` instead of `key:Ctrl`. +In most editors, a line of code can be commented out by pressing the `key:Ctrl+/` hotkey for a single-line comment and something like `key:Ctrl+Shift+/` -- for multiline comments (select a piece of code and press the hotkey). For Mac, try `key:Cmd` instead of `key:Ctrl` and `key:Option` instead of `key:Shift`. ``` ````warn header="Nested comments are not supported!" @@ -154,6 +150,6 @@ alert( 'World' ); Please, don't hesitate to comment your code. -Comments increase the overall code footprint, but that's not a problem at all. There are many tools which minify the code before publishing to the production server. They remove comments, so they don't appear in the working scripts. Therefore comments do not have any negative effects on production at all. +Comments increase the overall code footprint, but that's not a problem at all. There are many tools which minify code before publishing to a production server. They remove comments, so they don't appear in the working scripts. Therefore, comments do not have negative effects on production at all. -Further in the tutorial, there will be a chapter that also explains how to write better comments. +Later in the tutorial there will be a chapter that also explains how to write better comments. 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 35b4ef2b47..9586733cc8 100644 --- a/1-js/02-first-steps/03-strict-mode/article.md +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -1,16 +1,16 @@ # The modern mode, "use strict" -For a long time JavaScript was evolving without compatibility issues. New features were added to the language, but the old functionality did not change. +For a long time, JavaScript evolved without compatibility issues. New features were added to the language while old functionality didn't change. -That had the benefit of never breaking existing code. But the downside was that any mistake or an imperfect decision made by JavaScript creators got stuck in the language forever. +That had the benefit of never breaking existing code. But the downside was that any mistake or an imperfect decision made by JavaScript's creators got stuck in the language forever. -It had been so until 2009 when ECMAScript 5 (ES5) appeared. It added new features to the language and modified some of the existing ones. To keep the old code working, most modifications are off by default. One needs to enable them explicitly with a special directive `"use strict"`. +This was the case until 2009 when ECMAScript 5 (ES5) appeared. It added new features to the language and modified some of the existing ones. To keep the old code working, most such modifications are off by default. You need to explicitly enable them with a special directive: `"use strict"`. ## "use strict" -The directive looks like a string: `"use strict"` or `'use strict'`. When it is located on the top of the script, then the whole script works the "modern" way. +The directive looks like a string: `"use strict"` or `'use strict'`. When it is located at the top of a script, the whole script works the "modern" way. -For example +For example: ```js "use strict"; @@ -19,19 +19,16 @@ For example ... ``` -We will learn functions (a way to group commands) soon. - -Looking ahead let's just note that `"use strict"` can be put at the start of a function (most kinds of functions) instead of the whole script. Then strict mode is enabled in that function only. But usually people use it for the whole script. - +Quite soon we're going to learn functions (a way to group commands), so let's note in advance that `"use strict"` can be put at the beginning of a function. Doing that enables strict mode in that function only. But usually people use it for the whole script. ````warn header="Ensure that \"use strict\" is at the top" -Please make sure that `"use strict"` is on the top of the script, otherwise the strict mode may not be enabled. +Please make sure that `"use strict"` is at the top of your scripts, otherwise strict mode may not be enabled. -There is no strict mode here: +Strict mode isn't enabled here: ```js no-strict alert("some code"); -// "use strict" below is ignored, must be on the top +// "use strict" below is ignored--it must be at the top "use strict"; @@ -42,20 +39,51 @@ Only comments may appear above `"use strict"`. ```` ```warn header="There's no way to cancel `use strict`" -There is no directive `"no use strict"` or alike, that would return the old behavior. +There is no directive like `"no use strict"` that reverts the engine to old behavior. + +Once we enter strict mode, there's no going back. +``` + +## Browser console -Once we enter the strict mode, there's no return. +When you use a [developer console](info:devtools) to run code, please note that it doesn't `use strict` by default. + +Sometimes, when `use strict` makes a difference, you'll get incorrect results. + +So, how to actually `use strict` in the console? + +First, you can try to press `key:Shift+Enter` to input multiple lines, and put `use strict` on top, like this: + +```js +'use strict'; +// ...your code + ``` -## Always "use strict" +It works in most browsers, namely Firefox and Chrome. + +If it doesn't, e.g. in an old browser, there's an ugly, but reliable way to ensure `use strict`. Put it inside this kind of wrapper: + +```js +(function() { + 'use strict'; + + // ...your code here... +})() +``` + +## Should we "use strict"? + +The question may sound obvious, but it's not so. + +One could recommend to start scripts with `"use strict"`... But you know what's cool? + +Modern JavaScript supports "classes" and "modules" - advanced language structures (we'll surely get to them), that enable `use strict` automatically. So we don't need to add the `"use strict"` directive, if we use them. -The differences of `"use strict"` versus the "default" mode are still to be covered. +**So, for now `"use strict";` is a welcome guest at the top of your scripts. Later, when your code is all in classes and modules, you may omit it.** -In the next chapters, as we learn language features, we'll make notes about the differences of the strict mode. Luckily, there are not so many. And they actually make our life better. +As of now, we've got to know about `use strict` in general. -At this point in time it's enough to know about it in general: +In the next chapters, as we learn language features, we'll see the differences between the strict and old modes. Luckily, there aren't many and they actually make our lives better. -1. The `"use strict"` directive switches the engine to the "modern" mode, changing the behavior of some built-in features. We'll see the details as we study. -2. The strict mode is enabled by `"use strict"` at the top. Also there are several language features like "classes" and "modules" that enable strict mode automatically. -3. The strict mode is supported by all modern browsers. -4. It's always recommended to start scripts with `"use strict"`. All examples in this tutorial assume so, unless (very rarely) specified otherwise. +All examples in this tutorial assume strict mode unless (very rarely) specified otherwise. diff --git a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md index 69aa3558d4..392f4e26f1 100644 --- a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md +++ b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md @@ -1,4 +1,4 @@ -First, the variable for the name of our planet. +## The variable for our planet That's simple: @@ -6,9 +6,9 @@ That's simple: let ourPlanetName = "Earth"; ``` -Note, we could use a shorter name `planet`, but it might be not obvious what planet it refers to. It's nice to be more verbose. At least until the variable isNotTooLong. +Note, we could use a shorter name `planet`, but it might not be obvious what planet it refers to. It's nice to be more verbose. At least until the variable isNotTooLong. -Second, the name of the current visitor: +## The name of the current visitor ```js let currentUserName = "John"; @@ -18,4 +18,4 @@ Again, we could shorten that to `userName` if we know for sure that the user is Modern editors and autocomplete make long variable names easy to write. Don't save on them. A name with 3 words in it is fine. -And if your editor does not have proper autocompletion, get [a new one](/editors). +And if your editor does not have proper autocompletion, get [a new one](/code-editors). diff --git a/1-js/02-first-steps/04-variables/2-declare-variables/task.md b/1-js/02-first-steps/04-variables/2-declare-variables/task.md index 21631e2600..f364badf4c 100644 --- a/1-js/02-first-steps/04-variables/2-declare-variables/task.md +++ b/1-js/02-first-steps/04-variables/2-declare-variables/task.md @@ -4,5 +4,5 @@ importance: 3 # Giving the right name -1. Create the variable with the name of our planet. How would you name such a variable? -2. Create the variable to store the name of the current visitor. How would you name that variable? +1. Create a variable with the name of our planet. How would you name such a variable? +2. Create a variable to store the name of a current visitor to a website. How would you name that variable? diff --git a/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md b/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md index f3a96c692b..acd643fded 100644 --- a/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md +++ b/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md @@ -2,4 +2,4 @@ We generally use upper case for constants that are "hard-coded". Or, in other wo In this code, `birthday` is exactly like that. So we could use the upper case for it. -In contrast, `age` is evaluated in run-time. Today we have one age, a year after we'll have another one. It is constant in a sense that it does not change through the code execution. But it is a bit "less of a constant" than `birthday`, it is calculated, so we should keep the lower case for it. \ No newline at end of file +In contrast, `age` is evaluated in run-time. Today we have one age, a year after we'll have another one. It is constant in a sense that it does not change through the code execution. But it is a bit "less of a constant" than `birthday`: it is calculated, so we should keep the lower case for it. diff --git a/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md b/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md index 5fd18f90a8..f3c208a745 100644 --- a/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md +++ b/1-js/02-first-steps/04-variables/3-uppercast-constant/task.md @@ -12,13 +12,14 @@ const birthday = '18.04.1982'; const age = someCode(birthday); ``` -Here we have a constant `birthday` date and the `age` is calculated from `birthday` with the help of some code (it is not provided for shortness, and because details don't matter here). +Here we have a constant `birthday` for the date, and also the `age` constant. + +The `age` is calculated from `birthday` using `someCode()`, which means a function call that we didn't explain yet (we will soon!), but the details don't matter here, the point is that `age` is calculated somehow based on the `birthday`. Would it be right to use upper case for `birthday`? For `age`? Or even for both? ```js -const BIRTHDAY = '18.04.1982'; // make uppercase? +const BIRTHDAY = '18.04.1982'; // make birthday uppercase? -const AGE = someCode(BIRTHDAY); // make uppercase? +const AGE = someCode(BIRTHDAY); // make age uppercase? ``` - diff --git a/1-js/02-first-steps/04-variables/article.md b/1-js/02-first-steps/04-variables/article.md index 370dcd3906..e91d0ea7ea 100644 --- a/1-js/02-first-steps/04-variables/article.md +++ b/1-js/02-first-steps/04-variables/article.md @@ -1,30 +1,30 @@ # Variables -Most of the time, a JavaScript application needs to work with information. Here are 2 examples: -1. An online-shop -- the information might include goods being sold and a shopping cart. +Most of the time, a JavaScript application needs to work with information. Here are two examples: +1. An online shop -- the information might include goods being sold and a shopping cart. 2. A chat application -- the information might include users, messages, and much more. Variables are used to store this information. ## A variable -A [variable](https://en.wikipedia.org/wiki/Variable_(computer_science)) is a "named storage" for data. We can use variables to store goodies, visitors and other data. +A [variable](https://en.wikipedia.org/wiki/Variable_(computer_science)) is a "named storage" for data. We can use variables to store goodies, visitors, and other data. -To create a variable in JavaScript, we need to use the `let` keyword. +To create a variable in JavaScript, use the `let` keyword. -The statement below creates (in other words: *declares* or *defines*) a variable with the name "message": +The statement below creates (in other words: *declares*) a variable with the name "message": ```js let message; ``` -Now we can put some data into it by using the assignment operator `=`: +Now, we can put some data into it by using the assignment operator `=`: ```js let message; *!* -message = 'Hello'; // store the string +message = 'Hello'; // store the string 'Hello' in the variable named message */!* ``` @@ -39,7 +39,7 @@ alert(message); // shows the variable content */!* ``` -To be concise we can merge the variable declaration and assignment into a single line: +To be concise, we can combine the variable declaration and assignment into a single line: ```js run let message = 'Hello!'; // define the variable and assign the value @@ -53,7 +53,7 @@ We can also declare multiple variables in one line: let user = 'John', age = 25, message = 'Hello'; ``` -That might seem shorter, but it's not recommended. For the sake of better readability, please use a single line per variable. +That might seem shorter, but we don't recommend it. For the sake of better readability, please use a single line per variable. The multiline variant is a bit longer, but easier to read: @@ -63,7 +63,8 @@ let age = 25; let message = 'Hello'; ``` -Some people also write many variables like that: +Some people also define multiple variables in this multiline style: + ```js no-beautify let user = 'John', age = 25, @@ -78,32 +79,31 @@ let user = 'John' , message = 'Hello'; ``` -Technically, all these variants do the same. So, it's a matter of personal taste and aesthetics. - +Technically, all these variants do the same thing. So, it's a matter of personal taste and aesthetics. ````smart header="`var` instead of `let`" -In older scripts you may also find another keyword: `var` instead of `let`: +In older scripts, you may also find another keyword: `var` instead of `let`: ```js *!*var*/!* message = 'Hello'; ``` -The `var` keyword is *almost* the same as `let`. It also declares a variable, but in a slightly different, "old-school" fashion. +The `var` keyword is *almost* the same as `let`. It also declares a variable but in a slightly different, "old-school" way. -There are subtle differences between `let` and `var`, but they do not matter for us yet. We'll cover them in detail later, in the chapter . +There are subtle differences between `let` and `var`, but they do not matter to us yet. We'll cover them in detail in the chapter . ```` ## A real-life analogy We can easily grasp the concept of a "variable" if we imagine it as a "box" for data, with a uniquely-named sticker on it. -For instance, the variable `message` can be imagined as a box labeled `"message"` with the value `"Hello!"` in it: +For instance, the variable `message` can be imagined as a box labelled `"message"` with the value `"Hello!"` in it: -![](variable.png) +![](variable.svg) -We can put any value into the box. +We can put any value in the box. -Also we can change it. The value can be changed as many times as needed: +We can also change it as many times as we want: ```js run let message; @@ -117,7 +117,7 @@ alert(message); When the value is changed, the old data is removed from the variable: -![](variable-change.png) +![](variable-change.svg) We can also declare two variables and copy data from one into the other. @@ -136,29 +136,43 @@ alert(hello); // Hello world! alert(message); // Hello world! ``` +````warn header="Declaring twice triggers an error" +A variable should be declared only once. + +A repeated declaration of the same variable is an error: + +```js run +let message = "This"; + +// repeated 'let' leads to an error +let message = "That"; // SyntaxError: 'message' has already been declared +``` +So, we should declare a variable once and then refer to it without `let`. +```` + ```smart header="Functional languages" -It may be interesting to know that there also exist [functional](https://en.wikipedia.org/wiki/Functional_programming) programming languages that forbid changing a variable value. For example, [Scala](http://www.scala-lang.org/) or [Erlang](http://www.erlang.org/). +It's interesting to note that there exist so-called [pure functional](https://en.wikipedia.org/wiki/Purely_functional_programming) programming languages, such as [Haskell](https://en.wikipedia.org/wiki/Haskell), that forbid changing variable values. In such languages, once the value is stored "in the box", it's there forever. If we need to store something else, the language forces us to create a new box (declare a new variable). We can't reuse the old one. -Though it may seem a little bit odd at first sight, these languages are quite capable of serious development. More than that, there are areas like parallel computations where this limitation confers certain benefits. Studying such a language (even if not planning to use it soon) is recommended to broaden the mind. +Though it may seem a little odd at first sight, these languages are quite capable of serious development. More than that, there are areas like parallel computations where this limitation confers certain benefits. ``` ## Variable naming [#variable-naming] -There are two limitations for a variable name in JavaScript: +There are two limitations on variable names in JavaScript: -1. The name must contain only letters, digits, symbols `$` and `_`. +1. The name must contain only letters, digits, or the symbols `$` and `_`. 2. The first character must not be a digit. -Valid names, for instance: +Examples of valid names: ```js let userName; let test123; ``` -When the name contains multiple words, [camelCase](https://en.wikipedia.org/wiki/CamelCase) is commonly used. That is: words go one after another, each word starts with a capital letter: `myVeryLongName`. +When the name contains multiple words, [camelCase](https://en.wikipedia.org/wiki/CamelCase) is commonly used. That is: words go one after another, each word except first starting with a capital letter: `myVeryLongName`. What's interesting -- the dollar sign `'$'` and the underscore `'_'` can also be used in names. They are regular symbols, just like letters, without any special meaning. @@ -176,28 +190,28 @@ Examples of incorrect variable names: ```js no-beautify let 1a; // cannot start with a digit -let my-name; // a hyphen '-' is not allowed in the name +let my-name; // hyphens '-' aren't allowed in the name ``` ```smart header="Case matters" -Variables named `apple` and `AppLE` -- are two different variables. +Variables named `apple` and `APPLE` are two different variables. ``` -````smart header="Non-english letters are allowed, but not recommended" -It is possible to use any language, including cyrillic letters or even hieroglyphs, like this: +````smart header="Non-Latin letters are allowed, but not recommended" +It is possible to use any language, including Cyrillic letters, Chinese logograms and so on, like this: ```js let имя = '...'; let 我 = '...'; ``` -Technically, there is no error here, such names are allowed, but there is an international tradition to use English in variable names. Even if we're writing a small script, it may have a long life ahead. People from other countries may need to read it some time. +Technically, there is no error here. Such names are allowed, but there is an international convention to use English in variable names. Even if we're writing a small script, it may have a long life ahead. People from other countries may need to read it sometime. ```` ````warn header="Reserved names" -There is a list of reserved words, which cannot be used as variable names, because they are used by the language itself. +There is a [list of reserved words](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords), which cannot be used as variable names because they are used by the language itself. -For example, words `let`, `class`, `return`, `function` are reserved. +For example: `let`, `class`, `return`, and `function` are reserved. The code below gives a syntax error: @@ -209,37 +223,36 @@ let return = 5; // also can't name it "return", error! ````warn header="An assignment without `use strict`" -Normally, we need to define a variable before using it. But in the old times, it was technically possible to create a variable by a mere assignment of the value, without `let`. This still works now if we don't put `use strict`. The behavior is kept for compatibility with old scripts. +Normally, we need to define a variable before using it. But in the old times, it was technically possible to create a variable by a mere assignment of the value without using `let`. This still works now if we don't put `use strict` in our scripts to maintain compatibility with old scripts. ```js run no-strict // note: no "use strict" in this example -num = 5; // the variable "num" is created if didn't exist +num = 5; // the variable "num" is created if it didn't exist alert(num); // 5 ``` -That's a bad practice, it gives an error in the strict mode: +This is a bad practice and would cause an error in strict mode: -```js run untrusted +```js "use strict"; *!* num = 5; // error: num is not defined */!* ``` - ```` ## Constants -To declare a constant (unchanging) variable, one can use `const` instead of `let`: +To declare a constant (unchanging) variable, use `const` instead of `let`: ```js const myBirthday = '18.04.1982'; ``` -Variables declared using `const` are called "constants". They cannot be changed. An attempt to do it would cause an error: +Variables declared using `const` are called "constants". They cannot be reassigned. An attempt to do so would cause an error: ```js run const myBirthday = '18.04.1982'; @@ -247,16 +260,15 @@ const myBirthday = '18.04.1982'; myBirthday = '01.01.2001'; // error, can't reassign the constant! ``` -When a programmer is sure that the variable should never change, they can use `const` to guarantee it, and also to clearly show that fact to everyone. - +When a programmer is sure that a variable will never change, they can declare it with `const` to guarantee and communicate that fact to everyone. ### Uppercase constants -There is a widespread practice to use constants as aliases for difficult-to-remember values that are known prior to execution. +There is a widespread practice to use constants as aliases for difficult-to-remember values that are known before execution. Such constants are named using capital letters and underscores. -Like this: +For instance, let's make constants for colors in so-called "web" (hexadecimal) format: ```js run const COLOR_RED = "#F00"; @@ -272,61 +284,62 @@ alert(color); // #FF7F00 Benefits: - `COLOR_ORANGE` is much easier to remember than `"#FF7F00"`. -- It is much easier to mistype in `"#FF7F00"` than in `COLOR_ORANGE`. +- It is much easier to mistype `"#FF7F00"` than `COLOR_ORANGE`. - When reading the code, `COLOR_ORANGE` is much more meaningful than `#FF7F00`. -When should we use capitals for a constant, and when should we name them normally? Let's make that clear. +When should we use capitals for a constant and when should we name it normally? Let's make that clear. -Being a "constant" just means that the value never changes. But there are constants that are known prior to execution (like a hexadecimal value for red), and there are those that are *calculated* in run-time, during the execution, but do not change after the assignment. +Being a "constant" just means that a variable's value never changes. But some constants are known before execution (like a hexadecimal value for red) and some constants are *calculated* in run-time, during the execution, but do not change after their initial assignment. For instance: + ```js const pageLoadTime = /* time taken by a webpage to load */; ``` -The value of `pageLoadTime` is not known prior to the page load, so it's named normally. But it's still a constant, because it doesn't change after assignment. +The value of `pageLoadTime` is not known before the page load, so it's named normally. But it's still a constant because it doesn't change after the assignment. -In other words, capital-named constants are only used as aliases for "hard-coded" values. +In other words, capital-named constants are only used as aliases for "hard-coded" values. ## Name things right Talking about variables, there's one more extremely important thing. -Please name the variables sensibly. Take time to think if needed. +A variable name should have a clean, obvious meaning, describing the data that it stores. -Variable naming is one of the most important and complex skills in programming. A quick glance at variable names can reveal which code is written by a beginner and which by an experienced developer. +Variable naming is one of the most important and complex skills in programming. A glance at variable names can reveal which code was written by a beginner versus an experienced developer. -In a real project, most of the time is spent on modifying and extending the existing code base, rather than writing something completely separate from scratch. And when we return to the code after some time of doing something else, it's much easier to find information that is well-labeled. Or, in other words, when the variables have good names. +In a real project, most of the time is spent modifying and extending an existing code base rather than writing something completely separate from scratch. When we return to some code after doing something else for a while, it's much easier to find information that is well-labelled. Or, in other words, when the variables have good names. -Please spend some time thinking about the right name for a variable before declaring it. This will repay you a lot. +Please spend time thinking about the right name for a variable before declaring it. Doing so will repay you handsomely. Some good-to-follow rules are: - Use human-readable names like `userName` or `shoppingCart`. -- Stay away from abbreviations or short names like `a`, `b`, `c`, unless you really know what you're doing. -- Make the name maximally descriptive and concise. Examples of bad names are `data` and `value`. Such a name says nothing. It is only ok to use them if it's exceptionally obvious from the context which data or value is meant. -- Agree on terms within your team and in your own mind. If a site visitor is called a "user" then we should name related variables like `currentUser` or `newUser`, but not `currentVisitor` or a `newManInTown`. +- Stay away from abbreviations or short names like `a`, `b`, and `c`, unless you know what you're doing. +- Make names maximally descriptive and concise. Examples of bad names are `data` and `value`. Such names say nothing. It's only okay to use them if the context of the code makes it exceptionally obvious which data or value the variable is referencing. +- Agree on terms within your team and in your mind. If a site visitor is called a "user" then we should name related variables `currentUser` or `newUser` instead of `currentVisitor` or `newManInTown`. -Sounds simple? Indeed it is, but creating good descriptive-and-concise names in practice is not. Go for it. +Sounds simple? Indeed it is, but creating descriptive and concise variable names in practice is not. Go for it. ```smart header="Reuse or create?" -And the last note. There are some lazy programmers who, instead of declaring a new variable, tend to reuse the existing ones. +And the last note. There are some lazy programmers who, instead of declaring new variables, tend to reuse existing ones. -As a result, the variable is like a box where people throw different things without changing the sticker. What is inside it now? Who knows... We need to come closer and check. +As a result, their variables are like boxes into which people throw different things without changing their stickers. What's inside the box now? Who knows? We need to come closer and check. -Such a programmer saves a little bit on variable declaration, but loses ten times more on debugging the code. +Such programmers save a little bit on variable declaration but lose ten times more on debugging. An extra variable is good, not evil. -Modern JavaScript minifiers and browsers optimize code well enough, so it won't create performance issues. Using different variables for different values can even help the engine to optimize. +Modern JavaScript minifiers and browsers optimize code well enough, so it won't create performance issues. Using different variables for different values can even help the engine optimize your code. ``` ## Summary -We can declare variables to store data. That can be done using `var` or `let` or `const`. +We can declare variables to store data by using the `var`, `let`, or `const` keywords. -- `let` -- is a modern variable declaration. The code must be in strict mode to use `let` in Chrome (V8). +- `let` -- is a modern variable declaration. - `var` -- is an old-school variable declaration. Normally we don't use it at all, but we'll cover subtle differences from `let` in the chapter , just in case you need them. - `const` -- is like `let`, but the value of the variable can't be changed. -Variables should be named in a way that allows us to easily understand what's inside. +Variables should be named in a way that allows us to easily understand what's inside them. diff --git a/1-js/02-first-steps/04-variables/variable-change.png b/1-js/02-first-steps/04-variables/variable-change.png deleted file mode 100644 index 6dd3803d97..0000000000 Binary files a/1-js/02-first-steps/04-variables/variable-change.png and /dev/null differ diff --git a/1-js/02-first-steps/04-variables/variable-change.svg b/1-js/02-first-steps/04-variables/variable-change.svg new file mode 100644 index 0000000000..1b26792380 --- /dev/null +++ b/1-js/02-first-steps/04-variables/variable-change.svg @@ -0,0 +1 @@ +"World!""Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/variable-change@2x.png b/1-js/02-first-steps/04-variables/variable-change@2x.png deleted file mode 100644 index f57b04ab16..0000000000 Binary files a/1-js/02-first-steps/04-variables/variable-change@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/04-variables/variable.png b/1-js/02-first-steps/04-variables/variable.png deleted file mode 100644 index ab532d91d3..0000000000 Binary files a/1-js/02-first-steps/04-variables/variable.png and /dev/null differ diff --git a/1-js/02-first-steps/04-variables/variable.svg b/1-js/02-first-steps/04-variables/variable.svg new file mode 100644 index 0000000000..1c3d8b0cbe --- /dev/null +++ b/1-js/02-first-steps/04-variables/variable.svg @@ -0,0 +1 @@ +"Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/variable@2x.png b/1-js/02-first-steps/04-variables/variable@2x.png deleted file mode 100644 index c9c37f034e..0000000000 Binary files a/1-js/02-first-steps/04-variables/variable@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md index 9ed9a2d3b0..04e8b2450c 100644 --- a/1-js/02-first-steps/05-types/article.md +++ b/1-js/02-first-steps/05-types/article.md @@ -1,6 +1,10 @@ # Data types -A variable in JavaScript can contain any data. A variable can at one moment be a string and later receive a numeric value: +A value in JavaScript is always of a certain type. For example, a string or a number. + +There are eight basic data types in JavaScript. Here, we'll cover them in general and in the next chapters we'll talk about each of them in detail. + +We can put any type in a variable. For example, a variable can at one moment be a string and then store a number: ```js // no error @@ -8,22 +12,20 @@ let message = "hello"; message = 123456; ``` -Programming languages that allow such things are called "dynamically typed", meaning that there are data types, but variables are not bound to any of them. +Programming languages that allow such things, such as JavaScript, are called "dynamically typed", meaning that there exist data types, but variables are not bound to any of them. -There are seven basic data types in JavaScript. Here we'll study the basics, and in the next chapters we'll talk about each of them in detail. - -## A number +## Number ```js let n = 123; n = 12.345; ``` -The *number* type serves both for integer and floating point numbers. +The *number* type represents both integer and floating point numbers. -There are many operations for numbers, e.g. multiplication `*`, division `/`, addition `+`, subtraction `-` and so on. +There are many operations for numbers, e.g. multiplication `*`, division `/`, addition `+`, subtraction `-`, and so on. -Besides regular numbers, there are so-called "special numeric values" which also belong to that type: `Infinity`, `-Infinity` and `NaN`. +Besides regular numbers, there are so-called "special numeric values" which also belong to this data type: `Infinity`, `-Infinity` and `NaN`. - `Infinity` represents the mathematical [Infinity](https://en.wikipedia.org/wiki/Infinity) ∞. It is a special value that's greater than any number. @@ -33,7 +35,7 @@ Besides regular numbers, there are so-called "special numeric values" which also alert( 1 / 0 ); // Infinity ``` - Or just mention it in the code directly: + Or just reference it directly: ```js run alert( Infinity ); // Infinity @@ -44,32 +46,62 @@ Besides regular numbers, there are so-called "special numeric values" which also alert( "not a number" / 2 ); // NaN, such division is erroneous ``` - `NaN` is sticky. Any further operation on `NaN` would give `NaN`: + `NaN` is sticky. Any further mathematical operation on `NaN` returns `NaN`: ```js run - alert( "not a number" / 2 + 5 ); // NaN + alert( NaN + 1 ); // NaN + alert( 3 * NaN ); // NaN + alert( "not a number" / 2 - 1 ); // NaN ``` - So, if there's `NaN` somewhere in a mathematical expression, it propagates to the whole result. + So, if there's a `NaN` somewhere in a mathematical expression, it propagates to the whole result (there's only one exception to that: `NaN ** 0` is `1`). ```smart header="Mathematical operations are safe" -Doing maths is safe in JavaScript. We can do anything: divide by zero, treat non-numeric strings as numbers, etc. +Doing maths is "safe" in JavaScript. We can do anything: divide by zero, treat non-numeric strings as numbers, etc. -The script will never stop with a fatal error ("die"). At worst we'll get `NaN` as the result. +The script will never stop with a fatal error ("die"). At worst, we'll get `NaN` as the result. ``` -Special numeric values formally belong to the "number" type. Of course they are not numbers in a common sense of this word. +Special numeric values formally belong to the "number" type. Of course they are not numbers in the common sense of this word. We'll see more about working with numbers in the chapter . -## A string +## BigInt [#bigint-type] + +In JavaScript, the "number" type cannot safely represent integer values larger than (253-1) (that's `9007199254740991`), or less than -(253-1) for negatives. + +To be really precise, the "number" type can store larger integers (up to 1.7976931348623157 * 10308), but outside of the safe integer range ±(253-1) there'll be a precision error, because not all digits fit into the fixed 64-bit storage. So an "approximate" value may be stored. + +For example, these two numbers (right above the safe range) are the same: + +```js +console.log(9007199254740991 + 1); // 9007199254740992 +console.log(9007199254740991 + 2); // 9007199254740992 +``` + +So to say, all odd integers greater than (253-1) can't be stored at all in the "number" type. + +For most purposes ±(253-1) range is quite enough, but sometimes we need the entire range of really big integers, e.g. for cryptography or microsecond-precision timestamps. -A string in JavaScript must be quoted. +`BigInt` type was recently added to the language to represent integers of arbitrary length. + +A `BigInt` value is created by appending `n` to the end of an integer: + +```js +// the "n" at the end means it's a BigInt +const bigInt = 1234567890123456789012345678901234567890n; +``` + +As `BigInt` numbers are rarely needed, we don't cover them here, but devoted them a separate chapter . Read it when you need such big numbers. + +## String + +A string in JavaScript must be surrounded by quotes. ```js let str = "Hello"; let str2 = 'Single quotes are ok too'; -let phrase = `can embed ${str}`; +let phrase = `can embed another ${str}`; ``` In JavaScript, there are 3 types of quotes. @@ -78,7 +110,7 @@ In JavaScript, there are 3 types of quotes. 2. Single quotes: `'Hello'`. 3. Backticks: `Hello`. -Double and single quotes are "simple" quotes. There's no difference between them in JavaScript. +Double and single quotes are "simple" quotes. There's practically no difference between them in JavaScript. Backticks are "extended functionality" quotes. They allow us to embed variables and expressions into a string by wrapping them in `${…}`, for example: @@ -92,9 +124,9 @@ alert( `Hello, *!*${name}*/!*!` ); // Hello, John! alert( `the result is *!*${1 + 2}*/!*` ); // the result is 3 ``` -The expression inside `${…}` is evaluated and the result becomes a part of the string. We can put anything there: a variable like `name` or an arithmetical expression like `1 + 2` or something more complex. +The expression inside `${…}` is evaluated and the result becomes a part of the string. We can put anything in there: a variable like `name` or an arithmetical expression like `1 + 2` or something more complex. -Please note that this can only be done in backticks. Other quotes do not allow such embedding! +Please note that this can only be done in backticks. Other quotes don't have this embedding functionality! ```js run alert( "the result is ${1 + 2}" ); // the result is ${1 + 2} (double quotes do nothing) ``` @@ -102,12 +134,12 @@ alert( "the result is ${1 + 2}" ); // the result is ${1 + 2} (double quotes do n We'll cover strings more thoroughly in the chapter . ```smart header="There is no *character* type." -In some languages, there is a special "character" type for a single character. For example, in the C language and in Java it is `char`. +In some languages, there is a special "character" type for a single character. For example, in the C language and in Java it is called "char". -In JavaScript, there is no such type. There's only one type: `string`. A string may consist of only one character or many of them. +In JavaScript, there is no such type. There's only one type: `string`. A string may consist of zero characters (be empty), one character or many of them. ``` -## A boolean (logical type) +## Boolean (logical type) The boolean type has only two values: `true` and `false`. @@ -128,76 +160,74 @@ let isGreater = 4 > 1; alert( isGreater ); // true (the comparison result is "yes") ``` -We'll cover booleans more deeply later in the chapter . +We'll cover booleans more deeply in the chapter . ## The "null" value -The special `null` value does not belong to any type of those described above. +The special `null` value does not belong to any of the types described above. -It forms a separate type of its own, which contains only the `null` value: +It forms a separate type of its own which contains only the `null` value: ```js let age = null; ``` -In JavaScript `null` is not a "reference to a non-existing object" or a "null pointer" like in some other languages. +In JavaScript, `null` is not a "reference to a non-existing object" or a "null pointer" like in some other languages. -It's just a special value which has the sense of "nothing", "empty" or "value unknown". +It's just a special value which represents "nothing", "empty" or "value unknown". -The code above states that the `age` is unknown or empty for some reason. +The code above states that `age` is unknown. ## The "undefined" value -The special value `undefined` stands apart. It makes a type of its own, just like `null`. +The special value `undefined` also stands apart. It makes a type of its own, just like `null`. The meaning of `undefined` is "value is not assigned". -If a variable is declared, but not assigned, then its value is exactly `undefined`: +If a variable is declared, but not assigned, then its value is `undefined`: ```js run -let x; +let age; -alert(x); // shows "undefined" +alert(age); // shows "undefined" ``` -Technically, it is possible to assign `undefined` to any variable: +Technically, it is possible to explicitly assign `undefined` to a variable: ```js run -let x = 123; +let age = 100; -x = undefined; +// change the value to undefined +age = undefined; -alert(x); // "undefined" +alert(age); // "undefined" ``` -...But it's not recommended to do that. Normally, we use `null` to write an "empty" or an "unknown" value into the variable, and `undefined` is only used for checks, to see if the variable is assigned or similar. +...But we don't recommend doing that. Normally, one uses `null` to assign an "empty" or "unknown" value to a variable, while `undefined` is reserved as a default initial value for unassigned things. ## Objects and Symbols The `object` type is special. -All other types are called "primitive", because their values can contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store collections of data and more complex entities. We'll deal with them later in the chapter after we know enough about primitives. - -The `symbol` type is used to create unique identifiers for objects. We have to mention it here for completeness, but it's better to study them after objects. - -## The typeof operator [#type-typeof] +All other types are called "primitive" because their values can contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store collections of data and more complex entities. -The `typeof` operator returns the type of the argument. It's useful when we want to process values of different types differently, or just want to make a quick check. +Being that important, objects deserve a special treatment. We'll deal with them later in the chapter , after we learn more about primitives. -It supports two forms of syntax: +The `symbol` type is used to create unique identifiers for objects. We have to mention it here for the sake of completeness, but also postpone the details till we know objects. -1. As an operator: `typeof x`. -2. Function style: `typeof(x)`. +## The typeof operator [#type-typeof] -In other words, it works both with parentheses or without them. The result is the same. +The `typeof` operator returns the type of the operand. It's useful when we want to process values of different types differently or just want to do a quick check. -The call to `typeof x` returns a string with the type name: +A call to `typeof x` returns a string with the type name: ```js typeof undefined // "undefined" typeof 0 // "number" +typeof 10n // "bigint" + typeof true // "boolean" typeof "foo" // "string" @@ -217,29 +247,41 @@ typeof alert // "function" (3) */!* ``` -The last three lines may need additional explanations: +The last three lines may need additional explanation: + +1. `Math` is a built-in object that provides mathematical operations. We will learn it in the chapter . Here, it serves just as an example of an object. +2. The result of `typeof null` is `"object"`. That's an officially recognized error in `typeof`, coming from very early days of JavaScript and kept for compatibility. Definitely, `null` is not an object. It is a special value with a separate type of its own. The behavior of `typeof` is wrong here. +3. The result of `typeof alert` is `"function"`, because `alert` is a function. We'll study functions in the next chapters where we'll also see that there's no special "function" type in JavaScript. Functions belong to the object type. But `typeof` treats them differently, returning `"function"`. That also comes from the early days of JavaScript. Technically, such behavior isn't correct, but can be convenient in practice. -1. `Math` is a built-in object that provides mathematical operations. We will learn it in the chapter . Here it serves just as an example of an object. -2. The result of `typeof null` is `"object"`. That's wrong. It is an officially recognized error in `typeof`, kept for compatibility. Of course, `null` is not an object. It is a special value with a separate type of its own. So, again, that's an error in the language. -3. The result of `typeof alert` is `"function"`, because `alert` is a function of the language. We'll study functions in the next chapters, and we'll see that there's no special "function" type in the language. Functions belong to the object type. But `typeof` treats them differently. Formally, it's incorrect, but very convenient in practice. +```smart header="The `typeof(x)` syntax" +You may also come across another syntax: `typeof(x)`. It's the same as `typeof x`. +To put it clear: `typeof` is an operator, not a function. The parentheses here aren't a part of `typeof`. It's the kind of parentheses used for mathematical grouping. + +Usually, such parentheses contain a mathematical expression, such as `(2 + 2)`, but here they contain only one argument `(x)`. Syntactically, they allow to avoid a space between the `typeof` operator and its argument, and some people like it. + +Some people prefer `typeof(x)`, although the `typeof x` syntax is much more common. +``` ## Summary -There are 7 basic types in JavaScript. +There are 8 basic data types in JavaScript. -- `number` for numbers of any kind: integer or floating-point. -- `string` for strings. A string may have one or more characters, there's no separate single-character type. -- `boolean` for `true`/`false`. -- `null` for unknown values -- a standalone type that has a single value `null`. -- `undefined` for unassigned values -- a standalone type that has a single value `undefined`. -- `object` for more complex data structures. -- `symbol` for unique identifiers. +- Seven primitive data types: + - `number` for numbers of any kind: integer or floating-point, integers are limited by ±(253-1). + - `bigint` for integer numbers of arbitrary length. + - `string` for strings. A string may have zero or more characters, there's no separate single-character type. + - `boolean` for `true`/`false`. + - `null` for unknown values -- a standalone type that has a single value `null`. + - `undefined` for unassigned values -- a standalone type that has a single value `undefined`. + - `symbol` for unique identifiers. +- And one non-primitive data type: + - `object` for more complex data structures. -The `typeof` operator allows us to see which type is stored in the variable. +The `typeof` operator allows us to see which type is stored in a variable. -- Two forms: `typeof x` or `typeof(x)`. +- Usually used as `typeof x`, but `typeof(x)` is also possible. - Returns a string with the name of the type, like `"string"`. -- For `null` returns `"object"` -- that's an error in the language, it's not an object in fact. +- For `null` returns `"object"` -- this is an error in the language, it's not actually an object. -In the next chapters we'll concentrate on primitive values and once we're familiar with them, then we'll move on to objects. +In the next chapters, we'll concentrate on primitive values and once we're familiar with them, we'll move on to objects. diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md diff --git a/1-js/02-first-steps/06-alert-prompt-confirm/article.md b/1-js/02-first-steps/06-alert-prompt-confirm/article.md new file mode 100644 index 0000000000..ef0f333cb5 --- /dev/null +++ b/1-js/02-first-steps/06-alert-prompt-confirm/article.md @@ -0,0 +1,105 @@ +# Interaction: alert, prompt, confirm + +As we'll be using the browser as our demo environment, let's see a couple of functions to interact with the user: `alert`, `prompt` and `confirm`. + +## alert + +This one we've seen already. It shows a message and waits for the user to press "OK". + +For example: + +```js run +alert("Hello"); +``` + +The mini-window with the message is called a *modal window*. The word "modal" means that the visitor can't interact with the rest of the page, press other buttons, etc, until they have dealt with the window. In this case -- until they press "OK". + +## prompt + +The function `prompt` accepts two arguments: + +```js no-beautify +result = prompt(title, [default]); +``` + +It shows a modal window with a text message, an input field for the visitor, and the buttons OK/Cancel. + +`title` +: The text to show the visitor. + +`default` +: An optional second parameter, the initial value for the input field. + +```smart header="The square brackets in syntax `[...]`" +The square brackets around `default` in the syntax above denote that the parameter is optional, not required. +``` + +The visitor can type something in the prompt input field and press OK. Then we get that text in the `result`. Or they can cancel the input by pressing Cancel or hitting the `key:Esc` key, then we get `null` as the `result`. + +The call to `prompt` returns the text from the input field or `null` if the input was canceled. + +For instance: + +```js run +let age = prompt('How old are you?', 100); + +alert(`You are ${age} years old!`); // You are 100 years old! +``` + +````warn header="In IE: always supply a `default`" +The second parameter is optional, but if we don't supply it, Internet Explorer will insert the text `"undefined"` into the prompt. + +Run this code in Internet Explorer to see: + +```js run +let test = prompt("Test"); +``` + +So, for prompts to look good in IE, we recommend always providing the second argument: + +```js run +let test = prompt("Test", ''); // <-- for IE +``` +```` + +## confirm + +The syntax: + +```js +result = confirm(question); +``` + +The function `confirm` shows a modal window with a `question` and two buttons: OK and Cancel. + +The result is `true` if OK is pressed and `false` otherwise. + +For example: + +```js run +let isBoss = confirm("Are you the boss?"); + +alert( isBoss ); // true if OK is pressed +``` + +## Summary + +We covered 3 browser-specific functions to interact with visitors: + +`alert` +: shows a message. + +`prompt` +: shows a message asking the user to input text. It returns the text or, if Cancel button or `key:Esc` is clicked, `null`. + +`confirm` +: shows a message and waits for the user to press "OK" or "Cancel". It returns `true` for OK and `false` for Cancel/`key:Esc`. + +All these methods are modal: they pause script execution and don't allow the visitor to interact with the rest of the page until the window has been dismissed. + +There are two limitations shared by all the methods above: + +1. The exact location of the modal window is determined by the browser. Usually, it's in the center. +2. The exact look of the window also depends on the browser. We can't modify it. + +That is the price for simplicity. There are other ways to show nicer windows and richer interaction with the visitor, but if "bells and whistles" do not matter much, these methods work just fine. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md deleted file mode 100644 index ce48cbff11..0000000000 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md +++ /dev/null @@ -1,22 +0,0 @@ - -```js no-beautify -"" + 1 + 0 = "10" // (1) -"" - 1 + 0 = -1 // (2) -true + false = 1 -6 / "3" = 2 -"2" * "3" = 6 -4 + 5 + "px" = "9px" -"$" + 4 + 5 = "$45" -"4" - 2 = 2 -"4px" - 2 = NaN -7 / 0 = Infinity -" -9\n" + 5 = " -9\n5" -" -9\n" - 5 = -14 -null + 1 = 1 // (3) -undefined + 1 = NaN // (4) -``` - -1. The addition with a string `"" + 1` converts `1` to a string: `"" + 1 = "1"`, and then we have `"1" + 0`, the same rule is applied. -2. The subtraction `-` (like most math operations) only works with numbers, it converts an empty string `""` to `0`. -3. `null` becomes `0` after the numeric conversion. -4. `undefined` becomes `NaN` after the numeric conversion. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md deleted file mode 100644 index 83b295f927..0000000000 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md +++ /dev/null @@ -1,26 +0,0 @@ -importance: 5 - ---- - -# Type conversions - -What are results of these expressions? - -```js no-beautify -"" + 1 + 0 -"" - 1 + 0 -true + false -6 / "3" -"2" * "3" -4 + 5 + "px" -"$" + 4 + 5 -"4" - 2 -"4px" - 2 -7 / 0 -" -9\n" + 5 -" -9\n" - 5 -null + 1 -undefined + 1 -``` - -Think well, write down and then compare with the answer. diff --git a/1-js/02-first-steps/06-type-conversions/article.md b/1-js/02-first-steps/06-type-conversions/article.md deleted file mode 100644 index 90a418e351..0000000000 --- a/1-js/02-first-steps/06-type-conversions/article.md +++ /dev/null @@ -1,160 +0,0 @@ -# Type Conversions - -Most of the time, operators and functions automatically convert a value to the right type. That's called "type conversion". - -For example, `alert` automatically converts any value to a string to show it. Mathematical operations convert values to numbers. - -There are also cases when we need to explicitly convert a value to put things right. - -```smart header="Not talking about objects yet" -In this chapter we don't cover objects yet. Here we study primitives first. Later, after we learn objects, we'll see how object conversion works in the chapter . -``` - -## ToString - -String conversion happens when we need the string form of a value. - -For example, `alert(value)` does it to show the value. - -We can also use a call `String(value)` function for that: - -```js run -let value = true; -alert(typeof value); // boolean - -*!* -value = String(value); // now value is a string "true" -alert(typeof value); // string -*/!* -``` - -String conversion is mostly obvious. A `false` becomes `"false"`, `null` becomes `"null"` etc. - -## ToNumber - -Numeric conversion happens in mathematical functions and expressions automatically. - -For example, when division `/` is applied to non-numbers: - -```js run -alert( "6" / "2" ); // 3, strings are converted to numbers -``` - -We can use a `Number(value)` function to explicitly convert a `value`: - -```js run -let str = "123"; -alert(typeof str); // string - -let num = Number(str); // becomes a number 123 - -alert(typeof num); // number -``` - -Explicit conversion is usually required when we read a value from a string-based source like a text form, but we expect a number to be entered. - -If the string is not a valid number, the result of such conversion is `NaN`, for instance: - -```js run -let age = Number("an arbitrary string instead of a number"); - -alert(age); // NaN, conversion failed -``` - -Numeric conversion rules: - -| Value | Becomes... | -|-------|-------------| -|`undefined`|`NaN`| -|`null`|`0`| -|true and false | `1` and `0` | -| `string` | Whitespaces from the start and the end are removed. Then, if the remaining string is empty, the result is `0`. Otherwise, the number is "read" from the string. An error gives `NaN`. | - -Examples: - -```js run -alert( Number(" 123 ") ); // 123 -alert( Number("123z") ); // NaN (error reading a number at "z") -alert( Number(true) ); // 1 -alert( Number(false) ); // 0 -``` - -Please note that `null` and `undefined` behave differently here: `null` becomes a zero, while `undefined` becomes `NaN`. - -````smart header="Addition '+' concatenates strings" -Almost all mathematical operations convert values to numbers. With a notable exception of the addition `+`. If one of the added values is a string, then another one is also converted to a string. - -Then it concatenates (joins) them: - -```js run -alert( 1 + '2' ); // '12' (string to the right) -alert( '1' + 2 ); // '12' (string to the left) -``` - -That only happens when one of the arguments is a string. Otherwise, values are converted to numbers. -```` - -## ToBoolean - -Boolean conversion is the simplest one. - -It happens in logical operations (later we'll meet condition tests and other kinds of them), but also can be performed manually with the call of `Boolean(value)`. - -The conversion rule: - -- Values that are intuitively "empty", like `0`, an empty string, `null`, `undefined` and `NaN` become `false`. -- Other values become `true`. - -For instance: - -```js run -alert( Boolean(1) ); // true -alert( Boolean(0) ); // false - -alert( Boolean("hello") ); // true -alert( Boolean("") ); // false -``` - -````warn header="Please note: the string with zero `\"0\"` is `true`" -Some languages (namely PHP) treat `"0"` as `false`. But in JavaScript a non-empty string is always `true`. - -```js run -alert( Boolean("0") ); // true -alert( Boolean(" ") ); // spaces, also true (any non-empty string is true) -``` -```` - - -## Summary - -There are three most widely used type conversions: to string, to number and to boolean. - -**`ToString`** -- Occurs when we output something, can be performed with `String(value)`. The conversion to string is usually obvious for primitive values. - -**`ToNumber`** -- Occurs in math operations, can be performed with `Number(value)`. - -The conversion follows the rules: - -| Value | Becomes... | -|-------|-------------| -|`undefined`|`NaN`| -|`null`|`0`| -|true / false | `1 / 0` | -| `string` | The string is read "as is", whitespaces from both sides are ignored. An empty string becomes `0`. An error gives `NaN`. | - -**`ToBoolean`** -- Occurs in logical operations, or can be performed with `Boolean(value)`. - -Follows the rules: - -| Value | Becomes... | -|-------|-------------| -|`0`, `null`, `undefined`, `NaN`, `""` |`false`| -|any other value| `true` | - - -Most of these rules are easy to understand and memorize. The notable exceptions where people usually make mistakes are: - -- `undefined` is `NaN` as a number, not `0`. -- `"0"` and space-only strings like `" "` are true as a boolean. - -Objects are not covered here, we'll return to them later in the chapter that is devoted exclusively to objects, after we learn more basic things about JavaScript. diff --git a/1-js/02-first-steps/07-operators/article.md b/1-js/02-first-steps/07-operators/article.md deleted file mode 100644 index 6a2121fba1..0000000000 --- a/1-js/02-first-steps/07-operators/article.md +++ /dev/null @@ -1,444 +0,0 @@ -# Operators - -Many operators are known to us from school. They are addition `+`, a multiplication `*`, a subtraction `-` and so on. - -In this chapter we concentrate on aspects that are not covered by school arithmetic. - -## Terms: "unary", "binary", "operand" - -Before we move on, let's grasp the common terminology. - -- *An operand* -- is what operators are applied to. For instance in multiplication `5 * 2` there are two operands: the left operand is `5`, and the right operand is `2`. Sometimes people say "arguments" instead of "operands". -- An operator is *unary* if it has a single operand. For example, the unary negation `-` reverses the sign of the number: - - ```js run - let x = 1; - - *!* - x = -x; - */!* - alert( x ); // -1, unary negation was applied - ``` -- An operator is *binary* if it has two operands. The same minus exists in the binary form as well: - - ```js run no-beautify - let x = 1, y = 3; - alert( y - x ); // 2, binary minus subtracts values - ``` - - Formally, we're talking about two different operators here: the unary negation (single operand, reverses the sign) and the binary subtraction (two operands, subtracts). - -## Strings concatenation, binary + - -Now let's see special features of JavaScript operators that are beyond school arithmetics. - -Usually the plus operator `+` sums numbers. - -But if the binary `+` is applied to strings, it merges (concatenates) them: - -```js -let s = "my" + "string"; -alert(s); // mystring -``` - -Note that if any of the operands is a string, then the other one is converted to a string too. - -For example: - -```js run -alert( '1' + 2 ); // "12" -alert( 2 + '1' ); // "21" -``` - -See, it doesn't matter whether the first operand is a string or the second one. The rule is simple: if either operand is a string, then convert the other one into a string as well. - -However, note that operations run from left to right. If there are two numbers followed by a string, the numbers will be added before being converted to a string: - - -```js run -alert(2 + 2 + '1' ); // "41" and not "221" -``` - -String concatenation and conversion is a special feature of the binary plus `+`. Other arithmetic operators work only with numbers. They always convert their operands to numbers. - -For instance, subtraction and division: - -```js run -alert( 2 - '1' ); // 1 -alert( '6' / '2' ); // 3 -``` - -## Numeric conversion, unary + - -The plus `+` exists in two forms. The binary form that we used above and the unary form. - -The unary plus or, in other words, the plus operator `+` applied to a single value, doesn't do anything with numbers, but if the operand is not a number, then it is converted into it. - -For example: - -```js run -// No effect on numbers -let x = 1; -alert( +x ); // 1 - -let y = -2; -alert( +y ); // -2 - -*!* -// Converts non-numbers -alert( +true ); // 1 -alert( +"" ); // 0 -*/!* -``` - -It actually does the same as `Number(...)`, but is shorter. - -A need to convert strings to numbers arises very often. For example, if we are getting values from HTML form fields, then they are usually strings. - -What if we want to sum them? - -The binary plus would add them as strings: - -```js run -let apples = "2"; -let oranges = "3"; - -alert( apples + oranges ); // "23", the binary plus concatenates strings -``` - -If we want to treat them as numbers, then we can convert and then sum: - -```js run -let apples = "2"; -let oranges = "3"; - -*!* -// both values converted to numbers before the binary plus -alert( +apples + +oranges ); // 5 -*/!* - -// the longer variant -// alert( Number(apples) + Number(oranges) ); // 5 -``` - -From a mathematician's standpoint the abundance of pluses may seem strange. But from a programmer's standpoint, there's nothing special: unary pluses are applied first, they convert strings to numbers, and then the binary plus sums them up. - -Why are unary pluses applied to values before the binary one? As we're going to see, that's because of their *higher precedence*. - -## Operators precedence - -If an expression has more than one operator, the execution order is defined by their *precedence*, or, in other words, there's an implicit priority order among the operators. - -From school we all know that the multiplication in the expression `1 + 2 * 2` should be calculated before the addition. That's exactly the precedence thing. The multiplication is said to have *a higher precedence* than the addition. - -Parentheses override any precedence, so if we're not satisfied with the order, we can use them, like: `(1 + 2) * 2`. - -There are many operators in JavaScript. Every operator has a corresponding precedence number. The one with the bigger number executes first. If the precedence is the same, the execution order is from left to right. - -An extract from the [precedence table](https://developer.mozilla.org/en/JavaScript/Reference/operators/operator_precedence) (you don't need to remember this, but note that unary operators are higher than corresponding binary ones): - -| Precedence | Name | Sign | -|------------|------|------| -| ... | ... | ... | -| 16 | unary plus | `+` | -| 16 | unary negation | `-` | -| 14 | multiplication | `*` | -| 14 | division | `/` | -| 13 | addition | `+` | -| 13 | subtraction | `-` | -| ... | ... | ... | -| 3 | assignment | `=` | -| ... | ... | ... | - -As we can see, the "unary plus" has a priority of `16`, which is higher than `13` for the "addition" (binary plus). That's why in the expression `"+apples + +oranges"` unary pluses work first, and then the addition. - -## Assignment - -Let's note that an assignment `=` is also an operator. It is listed in the precedence table with the very low priority of `3`. - -That's why when we assign a variable, like `x = 2 * 2 + 1`, then the calculations are done first, and afterwards the `=` is evaluated, storing the result in `x`. - -```js -let x = 2 * 2 + 1; - -alert( x ); // 5 -``` - -It is possible to chain assignments: - -```js run -let a, b, c; - -*!* -a = b = c = 2 + 2; -*/!* - -alert( a ); // 4 -alert( b ); // 4 -alert( c ); // 4 -``` - -Chained assignments evaluate from right to left. First the rightmost expression `2 + 2` is evaluated then assigned to the variables on the left: `c`, `b` and `a`. At the end, all variables share a single value. - -````smart header="The assignment operator `\"=\"` returns a value" -An operator always returns a value. That's obvious for most of them like an addition `+` or a multiplication `*`. But the assignment operator follows that rule too. - -The call `x = value` writes the `value` into `x` *and then returns it*. - -Here's the demo that uses an assignment as part of a more complex expression: - -```js run -let a = 1; -let b = 2; - -*!* -let c = 3 - (a = b + 1); -*/!* - -alert( a ); // 3 -alert( c ); // 0 -``` - -In the example above, the result of `(a = b + 1)` is the value which is assigned to `a` (that is `3`). It is then used to subtract from `3`. - -Funny code, isn't it? We should understand how it works, because sometimes we can see it in 3rd-party libraries, but shouldn't write anything like that ourselves. Such tricks definitely don't make the code clearer and readable. -```` - -## Remainder % - -The remainder operator `%` despite its look does not have a relation to percents. - -The result of `a % b` is the remainder of the integer division of `a` by `b`. - -For instance: - -```js run -alert( 5 % 2 ); // 1 is a remainder of 5 divided by 2 -alert( 8 % 3 ); // 2 is a remainder of 8 divided by 3 -alert( 6 % 3 ); // 0 is a remainder of 6 divided by 3 -``` - -## Exponentiation ** - -The exponentiation operator `**` is a recent addition to the language. - -For a natural number `b`, the result of `a ** b` is `a` multiplied by itself `b` times. - -For instance: - -```js run -alert( 2 ** 2 ); // 4 (2 * 2) -alert( 2 ** 3 ); // 8 (2 * 2 * 2) -alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2) -``` - -The operator works for non-integer numbers of `a` and `b` as well, for instance: - -```js run -alert( 4 ** (1/2) ); // 2 (power of 1/2 is the same as a square root, that's maths) -alert( 8 ** (1/3) ); // 2 (power of 1/3 is the same as a cubic root) -``` - -## Increment/decrement - - - -Increasing or decreasing a number by one is among the most common numerical operations. - -So, there are special operators for that: - -- **Increment** `++` increases a variable by 1: - - ```js run no-beautify - let counter = 2; - counter++; // works the same as counter = counter + 1, but is shorter - alert( counter ); // 3 - ``` -- **Decrement** `--` decreases a variable by 1: - - ```js run no-beautify - let counter = 2; - counter--; // works the same as counter = counter - 1, but is shorter - alert( counter ); // 1 - ``` - -```warn -Increment/decrement can be applied only to a variable. An attempt to use it on a value like `5++` will give an error. -``` - -Operators `++` and `--` can be placed both after and before the variable. - -- When the operator goes after the variable, it is called a "postfix form": `counter++`. -- The "prefix form" is when the operator stands before the variable: `++counter`. - -Both of these records do the same: increase `counter` by `1`. - -Is there any difference? Yes, but we can only see it if we use the returned value of `++/--`. - -Let's clarify. As we know, all operators return a value. Increment/decrement is not an exception here. The prefix form returns the new value, while the postfix form returns the old value (prior to increment/decrement). - -To see the difference, here's the example: - -```js run -let counter = 1; -let a = ++counter; // (*) - -alert(a); // *!*2*/!* -``` - -Here in the line `(*)` the prefix call `++counter` increments `counter` and returns the new value that is `2`. So the `alert` shows `2`. - -Now let's use the postfix form: - -```js run -let counter = 1; -let a = counter++; // (*) changed ++counter to counter++ - -alert(a); // *!*1*/!* -``` - -In the line `(*)` the *postfix* form `counter++` also increments `counter`, but returns the *old* value (prior to increment). So the `alert` shows `1`. - -To summarize: - -- If the result of increment/decrement is not used, then there is no difference in which form to use: - - ```js run - let counter = 0; - counter++; - ++counter; - alert( counter ); // 2, the lines above did the same - ``` -- If we'd like to increase the value *and* use the result of the operator right now, then we need the prefix form: - - ```js run - let counter = 0; - alert( ++counter ); // 1 - ``` -- If we'd like to increment, but use the previous value, then we need the postfix form: - - ```js run - let counter = 0; - alert( counter++ ); // 0 - ``` - -````smart header="Increment/decrement among other operators" -Operators `++/--` can be used inside an expression as well. Their precedence is higher than most other arithmetical operations. - -For instance: - -```js run -let counter = 1; -alert( 2 * ++counter ); // 4 -``` - -Compare with: - -```js run -let counter = 1; -alert( 2 * counter++ ); // 2, because counter++ returns the "old" value -``` - -Though technically allowable, such notation usually makes the code less readable. One line does multiple things -- not good. - -While reading the code, a fast "vertical" eye-scan can easily miss such `counter++`, and it won't be obvious that the variable increases. - -The "one line -- one action" style is advised: - -```js run -let counter = 1; -alert( 2 * counter ); -counter++; -``` -```` - -## Bitwise operators - -Bitwise operators treat arguments as 32-bit integer numbers and work on the level of their binary representation. - -These operators are not JavaScript-specific. They are supported in most programming languages. - -The list of operators: - -- AND ( `&` ) -- OR ( `|` ) -- XOR ( `^` ) -- NOT ( `~` ) -- LEFT SHIFT ( `<<` ) -- RIGHT SHIFT ( `>>` ) -- ZERO-FILL RIGHT SHIFT ( `>>>` ) - -These operators are used very rarely. To understand them, we should delve into low-level number representation, and it would not be optimal to do that right now. Especially because we won't need them any time soon. If you're curious, you can read the [Bitwise Operators](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) article in MDN. It would be more practical to do that when a real need arises. - -## Modify-in-place - -We often need to apply an operator to a variable and store the new result in it. - -For example: - -```js -let n = 2; -n = n + 5; -n = n * 2; -``` - -This notation can be shortened using operators `+=` and `*=`: - -```js run -let n = 2; -n += 5; // now n = 7 (same as n = n + 5) -n *= 2; // now n = 14 (same as n = n * 2) - -alert( n ); // 14 -``` - -Short "modify-and-assign" operators exist for all arithmetical and bitwise operators: `/=`, `-=` etc. - -Such operators have the same precedence as a normal assignment, so they run after most other calculations: - -```js run -let n = 2; - -n *= 3 + 5; - -alert( n ); // 16 (right part evaluated first, same as n *= 8) -``` - -## Comma - -The comma operator `,` is one of most rare and unusual operators. Sometimes it's used to write shorter code, so we need to know it in order to understand what's going on. - -The comma operator allows us to evaluate several expressions, dividing them with a comma `,`. Each of them is evaluated, but the result of only the last one is returned. - -For example: - -```js run -*!* -let a = (1 + 2, 3 + 4); -*/!* - -alert( a ); // 7 (the result of 3 + 4) -``` - -Here, the first expression `1 + 2` is evaluated, and its result is thrown away, then `3 + 4` is evaluated and returned as the result. - -```smart header="Comma has a very low precedence" -Please note that the comma operator has very low precedence, lower than `=`, so parentheses are important in the example above. - -Without them: `a = 1 + 2, 3 + 4` evaluates `+` first, summing the numbers into `a = 3, 7`, then the assignment operator `=` assigns `a = 3`, and then the number after the comma `7` is not processed anyhow, so it's ignored. -``` - -Why do we need such an operator which throws away everything except the last part? - -Sometimes people use it in more complex constructs to put several actions in one line. - -For example: - -```js -// three operations in one line -for (*!*a = 1, b = 3, c = a * b*/!*; a < 10; a++) { - ... -} -``` - -Such tricks are used in many JavaScript frameworks, that's why we mention them. But usually they don't improve the code readability, so we should think well before writing like that. diff --git a/1-js/02-first-steps/07-type-conversions/article.md b/1-js/02-first-steps/07-type-conversions/article.md new file mode 100644 index 0000000000..3295561419 --- /dev/null +++ b/1-js/02-first-steps/07-type-conversions/article.md @@ -0,0 +1,150 @@ +# Type Conversions + +Most of the time, operators and functions automatically convert the values given to them to the right type. + +For example, `alert` automatically converts any value to a string to show it. Mathematical operations convert values to numbers. + +There are also cases when we need to explicitly convert a value to the expected type. + +```smart header="Not talking about objects yet" +In this chapter, we won't cover objects. For now, we'll just be talking about primitives. + +Later, after we learn about objects, in the chapter we'll see how objects fit in. +``` + +## String Conversion + +String conversion happens when we need the string form of a value. + +For example, `alert(value)` does it to show the value. + +We can also call the `String(value)` function to convert a value to a string: + +```js run +let value = true; +alert(typeof value); // boolean + +*!* +value = String(value); // now value is a string "true" +alert(typeof value); // string +*/!* +``` + +String conversion is mostly obvious. A `false` becomes `"false"`, `null` becomes `"null"`, etc. + +## Numeric Conversion + +Numeric conversion in mathematical functions and expressions happens automatically. + +For example, when division `/` is applied to non-numbers: + +```js run +alert( "6" / "2" ); // 3, strings are converted to numbers +``` + +We can use the `Number(value)` function to explicitly convert a `value` to a number: + +```js run +let str = "123"; +alert(typeof str); // string + +let num = Number(str); // becomes a number 123 + +alert(typeof num); // number +``` + +Explicit conversion is usually required when we read a value from a string-based source like a text form but expect a number to be entered. + +If the string is not a valid number, the result of such a conversion is `NaN`. For instance: + +```js run +let age = Number("an arbitrary string instead of a number"); + +alert(age); // NaN, conversion failed +``` + +Numeric conversion rules: + +| Value | Becomes... | +|-------|-------------| +|`undefined`|`NaN`| +|`null`|`0`| +|true and false | `1` and `0` | +| `string` | Whitespaces (includes spaces, tabs `\t`, newlines `\n` etc.) from the start and end are removed. If the remaining string is empty, the result is `0`. Otherwise, the number is "read" from the string. An error gives `NaN`. | + +Examples: + +```js run +alert( Number(" 123 ") ); // 123 +alert( Number("123z") ); // NaN (error reading a number at "z") +alert( Number(true) ); // 1 +alert( Number(false) ); // 0 +``` + +Please note that `null` and `undefined` behave differently here: `null` becomes zero while `undefined` becomes `NaN`. + +Most mathematical operators also perform such conversion, we'll see that in the next chapter. + +## Boolean Conversion + +Boolean conversion is the simplest one. + +It happens in logical operations (later we'll meet condition tests and other similar things) but can also be performed explicitly with a call to `Boolean(value)`. + +The conversion rule: + +- Values that are intuitively "empty", like `0`, an empty string, `null`, `undefined`, and `NaN`, become `false`. +- Other values become `true`. + +For instance: + +```js run +alert( Boolean(1) ); // true +alert( Boolean(0) ); // false + +alert( Boolean("hello") ); // true +alert( Boolean("") ); // false +``` + +````warn header="Please note: the string with zero `\"0\"` is `true`" +Some languages (namely PHP) treat `"0"` as `false`. But in JavaScript, a non-empty string is always `true`. + +```js run +alert( Boolean("0") ); // true +alert( Boolean(" ") ); // spaces, also true (any non-empty string is true) +``` +```` + +## Summary + +The three most widely used type conversions are to string, to number, and to boolean. + +**`String Conversion`** -- Occurs when we output something. Can be performed with `String(value)`. The conversion to string is usually obvious for primitive values. + +**`Numeric Conversion`** -- Occurs in math operations. Can be performed with `Number(value)`. + +The conversion follows the rules: + +| Value | Becomes... | +|-------|-------------| +|`undefined`|`NaN`| +|`null`|`0`| +|true / false | `1 / 0` | +| `string` | The string is read "as is", whitespaces (includes spaces, tabs `\t`, newlines `\n` etc.) from both sides are ignored. An empty string becomes `0`. An error gives `NaN`. | + +**`Boolean Conversion`** -- Occurs in logical operations. Can be performed with `Boolean(value)`. + +Follows the rules: + +| Value | Becomes... | +|-------|-------------| +|`0`, `null`, `undefined`, `NaN`, `""` |`false`| +|any other value| `true` | + + +Most of these rules are easy to understand and memorize. The notable exceptions where people usually make mistakes are: + +- `undefined` is `NaN` as a number, not `0`. +- `"0"` and space-only strings like `" "` are true as a boolean. + +Objects aren't covered here. We'll return to them later in the chapter that is devoted exclusively to objects after we learn more basic things about JavaScript. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md deleted file mode 100644 index 5c8bd2bc47..0000000000 --- a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md +++ /dev/null @@ -1,21 +0,0 @@ - - -```js no-beautify -5 > 4 → true -"apple" > "pineapple" → false -"2" > "12" → true -undefined == null → true -undefined === null → false -null == "\n0\n" → false -null === +"\n0\n" → false -``` - -Some of the reasons: - -1. Obviously, true. -2. Dictionary comparison, hence false. -3. Again, dictionary comparison, first char of `"2"` is greater than the first char of `"1"`. -4. Values `null` and `undefined` equal each other only. -5. Strict equality is strict. Different types from both sides lead to false. -6. See (4). -7. Strict equality of different types. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md b/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md deleted file mode 100644 index 891e703d0b..0000000000 --- a/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md +++ /dev/null @@ -1,18 +0,0 @@ -importance: 5 - ---- - -# Comparisons - -What will be the result for expressions? - -```js no-beautify -5 > 4 -"apple" > "pineapple" -"2" > "12" -undefined == null -undefined === null -null == "\n0\n" -null === +"\n0\n" -``` - diff --git a/1-js/02-first-steps/08-comparison/article.md b/1-js/02-first-steps/08-comparison/article.md deleted file mode 100644 index 00ae53f11b..0000000000 --- a/1-js/02-first-steps/08-comparison/article.md +++ /dev/null @@ -1,212 +0,0 @@ -# Comparisons - -Many comparison operators we know from maths: - -- Greater/less than: a > b, a < b. -- Greater/less than or equals: a >= b, a <= b. -- Equality check is written as `a == b` (please note the double equation sign `=`. A single symbol `a = b` would mean an assignment). -- Not equals. In maths the notation is , in JavaScript it's written as an assignment with an exclamation sign before it: a != b. - -## Boolean is the result - -Just as all other operators, a comparison returns a value. The value is of the boolean type. - -- `true` -- means "yes", "correct" or "the truth". -- `false` -- means "no", "wrong" or "a lie". - -For example: - -```js run -alert( 2 > 1 ); // true (correct) -alert( 2 == 1 ); // false (wrong) -alert( 2 != 1 ); // true (correct) -``` - -A comparison result can be assigned to a variable, just like any value: - -```js run -let result = 5 > 4; // assign the result of the comparison -alert( result ); // true -``` - -## String comparison - -To see which string is greater than the other, the so-called "dictionary" or "lexicographical" order is used. - -In other words, strings are compared letter-by-letter. - -For example: - -```js run -alert( 'Z' > 'A' ); // true -alert( 'Glow' > 'Glee' ); // true -alert( 'Bee' > 'Be' ); // true -``` - -The algorithm to compare two strings is simple: - -1. Compare first characters of both strings. -2. If the first one is greater(or less), then the first string is greater(or less) than the second. We're done. -3. Otherwise if first characters are equal, compare the second characters the same way. -4. Repeat until the end of any string. -5. If both strings ended simultaneously, then they are equal. Otherwise the longer string is greater. - -In the example above, the comparison `'Z' > 'A'` gets the result at the first step. - -Strings `"Glow"` and `"Glee"` are compared character-by-character: - -1. `G` is the same as `G`. -2. `l` is the same as `l`. -3. `o` is greater than `e`. Stop here. The first string is greater. - -```smart header="Not a real dictionary, but Unicode order" -The comparison algorithm given above is roughly equivalent to the one used in book dictionaries or phone books. But it's not exactly the same. - -For instance, case matters. A capital letter `"A"` is not equal to the lowercase `"a"`. Which one is greater? Actually, the lowercase `"a"` is. Why? Because the lowercase character has a greater index in the internal encoding table (Unicode). We'll get back to specific details and consequences in the chapter . -``` - -## Comparison of different types - -When compared values belong to different types, they are converted to numbers. - -For example: - -```js run -alert( '2' > 1 ); // true, string '2' becomes a number 2 -alert( '01' == 1 ); // true, string '01' becomes a number 1 -``` - -For boolean values, `true` becomes `1` and `false` becomes `0`, that's why: - -```js run -alert( true == 1 ); // true -alert( false == 0 ); // true -``` - -````smart header="A funny consequence" -It is possible that at the same time: - -- Two values are equal. -- One of them is `true` as a boolean and the other one is `false` as a boolean. - -For example: - -```js run -let a = 0; -alert( Boolean(a) ); // false - -let b = "0"; -alert( Boolean(b) ); // true - -alert(a == b); // true! -``` - -From JavaScript's standpoint that's quite normal. An equality check converts using the numeric conversion (hence `"0"` becomes `0`), while `Boolean` conversion uses another set of rules. -```` - -## Strict equality - -A regular equality check `==` has a problem. It cannot differ `0` from `false`: - -```js run -alert( 0 == false ); // true -``` - -The same thing with an empty string: - -```js run -alert( '' == false ); // true -``` - -That's because operands of different types are converted to a number by the equality operator `==`. An empty string, just like `false`, becomes a zero. - -What to do if we'd like to differentiate `0` from `false`? - -**A strict equality operator `===` checks the equality without type conversion.** - -In other words, if `a` and `b` are of different types, then `a === b` immediately returns `false` without an attempt to convert them. - -Let's try it: - -```js run -alert( 0 === false ); // false, because the types are different -``` - -There also exists a "strict non-equality" operator `!==`, as an analogy for `!=`. - -The strict equality check operator is a bit longer to write, but makes it obvious what's going on and leaves less space for errors. - -## Comparison with null and undefined - -Let's see more edge cases. - -There's a non-intuitive behavior when `null` or `undefined` are compared with other values. - - -For a strict equality check `===` -: These values are different, because each of them belongs to a separate type of its own. - - ```js run - alert( null === undefined ); // false - ``` - -For a non-strict check `==` -: There's a special rule. These two are a "sweet couple": they equal each other (in the sense of `==`), but not any other value. - - ```js run - alert( null == undefined ); // true - ``` - -For maths and other comparisons `< > <= >=` -: Values `null/undefined` are converted to a number: `null` becomes `0`, while `undefined` becomes `NaN`. - -Now let's see funny things that happen when we apply those rules. And, what's more important, how to not fall into a trap with these features. - -### Strange result: null vs 0 - -Let's compare `null` with a zero: - -```js run -alert( null > 0 ); // (1) false -alert( null == 0 ); // (2) false -alert( null >= 0 ); // (3) *!*true*/!* -``` - -Yeah, mathematically that's strange. The last result states that "`null` is greater than or equal to zero". Then one of the comparisons above must be correct, but they are both false. - -The reason is that an equality check `==` and comparisons `> < >= <=` work differently. Comparisons convert `null` to a number, hence treat it as `0`. That's why (3) `null >= 0` is true and (1) `null > 0` is false. - -On the other hand, the equality check `==` for `undefined` and `null` works by the rule, without any conversions. They equal each other and don't equal anything else. That's why (2) `null == 0` is false. - -### An incomparable undefined - -The value `undefined` shouldn't participate in comparisons at all: - -```js run -alert( undefined > 0 ); // false (1) -alert( undefined < 0 ); // false (2) -alert( undefined == 0 ); // false (3) -``` - -Why does it dislike a zero so much? Always false! - -We've got these results because: - -- Comparisons `(1)` and `(2)` return `false` because `undefined` gets converted to `NaN`. And `NaN` is a special numeric value which returns `false` for all comparisons. -- The equality check `(3)` returns `false`, because `undefined` only equals `null` and no other value. - -### Evade problems - -Why did we observe these examples? Should we remember these peculiarities all the time? Well, not really. Actually, these tricky things will gradually become familiar over time, but there's a solid way to evade any problems with them. - -Just treat any comparison with `undefined/null` except the strict equality `===` with exceptional care. - -Don't use comparisons `>= > < <=` with a variable which may be `null/undefined`, unless you are really sure what you're doing. If a variable can have such values, then check for them separately. - -## Summary - -- Comparison operators return a logical value. -- Strings are compared letter-by-letter in the "dictionary" order. -- When values of different types are compared, they get converted to numbers (with the exclusion of a strict equality check). -- Values `null` and `undefined` equal `==` each other and do not equal any other value. -- Be careful when using comparisons like `>` or `<` with variables that can occasionally be `null/undefined`. Making a separate check for `null/undefined` is a good idea. diff --git a/1-js/02-first-steps/07-operators/1-increment-order/solution.md b/1-js/02-first-steps/08-operators/1-increment-order/solution.md similarity index 100% rename from 1-js/02-first-steps/07-operators/1-increment-order/solution.md rename to 1-js/02-first-steps/08-operators/1-increment-order/solution.md diff --git a/1-js/02-first-steps/07-operators/1-increment-order/task.md b/1-js/02-first-steps/08-operators/1-increment-order/task.md similarity index 100% rename from 1-js/02-first-steps/07-operators/1-increment-order/task.md rename to 1-js/02-first-steps/08-operators/1-increment-order/task.md diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/solution.md b/1-js/02-first-steps/08-operators/2-assignment-result/solution.md similarity index 100% rename from 1-js/02-first-steps/07-operators/2-assignment-result/solution.md rename to 1-js/02-first-steps/08-operators/2-assignment-result/solution.md diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/task.md b/1-js/02-first-steps/08-operators/2-assignment-result/task.md similarity index 100% rename from 1-js/02-first-steps/07-operators/2-assignment-result/task.md rename to 1-js/02-first-steps/08-operators/2-assignment-result/task.md 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 new file mode 100644 index 0000000000..7370b66aff --- /dev/null +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md @@ -0,0 +1,25 @@ + +```js no-beautify +"" + 1 + 0 = "10" // (1) +"" - 1 + 0 = -1 // (2) +true + false = 1 +6 / "3" = 2 +"2" * "3" = 6 +4 + 5 + "px" = "9px" +"$" + 4 + 5 = "$45" +"4" - 2 = 2 +"4px" - 2 = NaN +" -9 " + 5 = " -9 5" // (3) +" -9 " - 5 = -14 // (4) +null + 1 = 1 // (5) +undefined + 1 = NaN // (6) +" \t \n" - 2 = -2 // (7) +``` + +1. The addition with a string `"" + 1` converts `1` to a string: `"" + 1 = "1"`, and then we have `"1" + 0`, the same rule is applied. +2. The subtraction `-` (like most math operations) only works with numbers, it converts an empty string `""` to `0`. +3. The addition with a string appends the number `5` to the string. +4. The subtraction always converts to numbers, so it makes `" -9 "` a number `-9` (ignoring spaces around it). +5. `null` becomes `0` after the numeric conversion. +6. `undefined` becomes `NaN` after the numeric conversion. +7. Space characters are trimmed off string start and end when a string is converted to a number. Here the whole string consists of space characters, such as `\t`, `\n` and a "regular" space between them. So, similarly to an empty string, it becomes `0`. 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 new file mode 100644 index 0000000000..068420c7d3 --- /dev/null +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md @@ -0,0 +1,26 @@ +importance: 5 + +--- + +# Type conversions + +What are results of these expressions? + +```js no-beautify +"" + 1 + 0 +"" - 1 + 0 +true + false +6 / "3" +"2" * "3" +4 + 5 + "px" +"$" + 4 + 5 +"4" - 2 +"4px" - 2 +" -9 " + 5 +" -9 " - 5 +null + 1 +undefined + 1 +" \t \n" - 2 +``` + +Think well, write down and then compare with the answer. diff --git a/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md b/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md new file mode 100644 index 0000000000..209a0702c4 --- /dev/null +++ b/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md @@ -0,0 +1,32 @@ +The reason is that prompt returns user input as a string. + +So variables have values `"1"` and `"2"` respectively. + +```js run +let a = "1"; // prompt("First number?", 1); +let b = "2"; // prompt("Second number?", 2); + +alert(a + b); // 12 +``` + +What we should do is to convert strings to numbers before `+`. For example, using `Number()` or prepending them with `+`. + +For example, right before `prompt`: + +```js run +let a = +prompt("First number?", 1); +let b = +prompt("Second number?", 2); + +alert(a + b); // 3 +``` + +Or in the `alert`: + +```js run +let a = prompt("First number?", 1); +let b = prompt("Second number?", 2); + +alert(+a + +b); // 3 +``` + +Using both unary and binary `+` in the latest code. Looks funny, doesn't it? diff --git a/1-js/02-first-steps/08-operators/4-fix-prompt/task.md b/1-js/02-first-steps/08-operators/4-fix-prompt/task.md new file mode 100644 index 0000000000..b3ea4a3a3c --- /dev/null +++ b/1-js/02-first-steps/08-operators/4-fix-prompt/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Fix the addition + +Here's a code that asks the user for two numbers and shows their sum. + +It works incorrectly. The output in the example below is `12` (for default prompt values). + +Why? Fix it. The result should be `3`. + +```js run +let a = prompt("First number?", 1); +let b = prompt("Second number?", 2); + +alert(a + b); // 12 +``` diff --git a/1-js/02-first-steps/08-operators/article.md b/1-js/02-first-steps/08-operators/article.md new file mode 100644 index 0000000000..d52c37a172 --- /dev/null +++ b/1-js/02-first-steps/08-operators/article.md @@ -0,0 +1,480 @@ +# Basic operators, maths + +We know many operators from school. They are things like addition `+`, multiplication `*`, subtraction `-`, and so on. + +In this chapter, we’ll start with simple operators, then concentrate on JavaScript-specific aspects, not covered by school arithmetic. + +## Terms: "unary", "binary", "operand" + +Before we move on, let's grasp some common terminology. + +- *An operand* -- is what operators are applied to. For instance, in the multiplication of `5 * 2` there are two operands: the left operand is `5` and the right operand is `2`. Sometimes, people call these "arguments" instead of "operands". +- An operator is *unary* if it has a single operand. For example, the unary negation `-` reverses the sign of a number: + + ```js run + let x = 1; + + *!* + x = -x; + */!* + alert( x ); // -1, unary negation was applied + ``` +- An operator is *binary* if it has two operands. The same minus exists in binary form as well: + + ```js run no-beautify + let x = 1, y = 3; + alert( y - x ); // 2, binary minus subtracts values + ``` + + Formally, in the examples above we have two different operators that share the same symbol: the negation operator, a unary operator that reverses the sign, and the subtraction operator, a binary operator that subtracts one number from another. + +## Maths + +The following math operations are supported: + +- Addition `+`, +- Subtraction `-`, +- Multiplication `*`, +- Division `/`, +- Remainder `%`, +- Exponentiation `**`. + +The first four are straightforward, while `%` and `**` need a few words about them. + +### Remainder % + +The remainder operator `%`, despite its appearance, is not related to percents. + +The result of `a % b` is the [remainder](https://en.wikipedia.org/wiki/Remainder) of the integer division of `a` by `b`. + +For instance: + +```js run +alert( 5 % 2 ); // 1, the remainder of 5 divided by 2 +alert( 8 % 3 ); // 2, the remainder of 8 divided by 3 +alert( 8 % 4 ); // 0, the remainder of 8 divided by 4 +``` + +### Exponentiation ** + +The exponentiation operator `a ** b` raises `a` to the power of `b`. + +In school maths, we write that as ab. + +For instance: + +```js run +alert( 2 ** 2 ); // 2² = 4 +alert( 2 ** 3 ); // 2³ = 8 +alert( 2 ** 4 ); // 2⁴ = 16 +``` + +Just like in maths, the exponentiation operator is defined for non-integer numbers as well. + +For example, a square root is an exponentiation by ½: + +```js run +alert( 4 ** (1/2) ); // 2 (power of 1/2 is the same as a square root) +alert( 8 ** (1/3) ); // 2 (power of 1/3 is the same as a cubic root) +``` + + +## String concatenation with binary + + +Let's meet the features of JavaScript operators that are beyond school arithmetics. + +Usually, the plus operator `+` sums numbers. + +But, if the binary `+` is applied to strings, it merges (concatenates) them: + +```js +let s = "my" + "string"; +alert(s); // mystring +``` + +Note that if any of the operands is a string, then the other one is converted to a string too. + +For example: + +```js run +alert( '1' + 2 ); // "12" +alert( 2 + '1' ); // "21" +``` + +See, it doesn't matter whether the first operand is a string or the second one. + +Here's a more complex example: + +```js run +alert(2 + 2 + '1' ); // "41" and not "221" +``` + +Here, operators work one after another. The first `+` sums two numbers, so it returns `4`, then the next `+` adds the string `1` to it, so it's like `4 + '1' = '41'`. + +```js run +alert('1' + 2 + 2); // "122" and not "14" +``` +Here, the first operand is a string, the compiler treats the other two operands as strings too. The `2` gets concatenated to `'1'`, so it's like `'1' + 2 = "12"` and `"12" + 2 = "122"`. + +The binary `+` is the only operator that supports strings in such a way. Other arithmetic operators work only with numbers and always convert their operands to numbers. + +Here's the demo for subtraction and division: + +```js run +alert( 6 - '2' ); // 4, converts '2' to a number +alert( '6' / '2' ); // 3, converts both operands to numbers +``` + +## Numeric conversion, unary + + +The plus `+` exists in two forms: the binary form that we used above and the unary form. + +The unary plus or, in other words, the plus operator `+` applied to a single value, doesn't do anything to numbers. But if the operand is not a number, the unary plus converts it into a number. + +For example: + +```js run +// No effect on numbers +let x = 1; +alert( +x ); // 1 + +let y = -2; +alert( +y ); // -2 + +*!* +// Converts non-numbers +alert( +true ); // 1 +alert( +"" ); // 0 +*/!* +``` + +It actually does the same thing as `Number(...)`, but is shorter. + +The need to convert strings to numbers arises very often. For example, if we are getting values from HTML form fields, they are usually strings. What if we want to sum them? + +The binary plus would add them as strings: + +```js run +let apples = "2"; +let oranges = "3"; + +alert( apples + oranges ); // "23", the binary plus concatenates strings +``` + +If we want to treat them as numbers, we need to convert and then sum them: + +```js run +let apples = "2"; +let oranges = "3"; + +*!* +// both values converted to numbers before the binary plus +alert( +apples + +oranges ); // 5 +*/!* + +// the longer variant +// alert( Number(apples) + Number(oranges) ); // 5 +``` + +From a mathematician's standpoint, the abundance of pluses may seem strange. But from a programmer's standpoint, there's nothing special: unary pluses are applied first, they convert strings to numbers, and then the binary plus sums them up. + +Why are unary pluses applied to values before the binary ones? As we're going to see, that's because of their *higher precedence*. + +## Operator precedence + +If an expression has more than one operator, the execution order is defined by their *precedence*, or, in other words, the default priority order of operators. + +From school, we all know that the multiplication in the expression `1 + 2 * 2` should be calculated before the addition. That's exactly the precedence thing. The multiplication is said to have *a higher precedence* than the addition. + +Parentheses override any precedence, so if we're not satisfied with the default order, we can use them to change it. For example, write `(1 + 2) * 2`. + +There are many operators in JavaScript. Every operator has a corresponding precedence number. The one with the larger number executes first. If the precedence is the same, the execution order is from left to right. + +Here's an extract from the [precedence table](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence) (you don't need to remember this, but note that unary operators are higher than corresponding binary ones): + +| Precedence | Name | Sign | +|------------|------|------| +| ... | ... | ... | +| 14 | unary plus | `+` | +| 14 | unary negation | `-` | +| 13 | exponentiation | `**` | +| 12 | multiplication | `*` | +| 12 | division | `/` | +| 11 | addition | `+` | +| 11 | subtraction | `-` | +| ... | ... | ... | +| 2 | assignment | `=` | +| ... | ... | ... | + +As we can see, the "unary plus" has a priority of `14` which is higher than the `11` of "addition" (binary plus). That's why, in the expression `"+apples + +oranges"`, unary pluses work before the addition. + +## Assignment + +Let's note that an assignment `=` is also an operator. It is listed in the precedence table with the very low priority of `2`. + +That's why, when we assign a variable, like `x = 2 * 2 + 1`, the calculations are done first and then the `=` is evaluated, storing the result in `x`. + +```js +let x = 2 * 2 + 1; + +alert( x ); // 5 +``` + +### Assignment = returns a value + +The fact of `=` being an operator, not a "magical" language construct has an interesting implication. + +All operators in JavaScript return a value. That's obvious for `+` and `-`, but also true for `=`. + +The call `x = value` writes the `value` into `x` *and then returns it*. + +Here's a demo that uses an assignment as part of a more complex expression: + +```js run +let a = 1; +let b = 2; + +*!* +let c = 3 - (a = b + 1); +*/!* + +alert( a ); // 3 +alert( c ); // 0 +``` + +In the example above, the result of expression `(a = b + 1)` is the value which was assigned to `a` (that is `3`). It is then used for further evaluations. + +Funny code, isn't it? We should understand how it works, because sometimes we see it in JavaScript libraries. + +Although, please don't write the code like that. Such tricks definitely don't make code clearer or readable. + +### Chaining assignments + +Another interesting feature is the ability to chain assignments: + +```js run +let a, b, c; + +*!* +a = b = c = 2 + 2; +*/!* + +alert( a ); // 4 +alert( b ); // 4 +alert( c ); // 4 +``` + +Chained assignments evaluate from right to left. First, the rightmost expression `2 + 2` is evaluated and then assigned to the variables on the left: `c`, `b` and `a`. At the end, all the variables share a single value. + +Once again, for the purposes of readability it's better to split such code into few lines: + +```js +c = 2 + 2; +b = c; +a = c; +``` +That's easier to read, especially when eye-scanning the code fast. + +## Modify-in-place + +We often need to apply an operator to a variable and store the new result in that same variable. + +For example: + +```js +let n = 2; +n = n + 5; +n = n * 2; +``` + +This notation can be shortened using the operators `+=` and `*=`: + +```js run +let n = 2; +n += 5; // now n = 7 (same as n = n + 5) +n *= 2; // now n = 14 (same as n = n * 2) + +alert( n ); // 14 +``` + +Short "modify-and-assign" operators exist for all arithmetical and bitwise operators: `/=`, `-=`, etc. + +Such operators have the same precedence as a normal assignment, so they run after most other calculations: + +```js run +let n = 2; + +n *= 3 + 5; // right part evaluated first, same as n *= 8 + +alert( n ); // 16 +``` + +## Increment/decrement + + + +Increasing or decreasing a number by one is among the most common numerical operations. + +So, there are special operators for it: + +- **Increment** `++` increases a variable by 1: + + ```js run no-beautify + let counter = 2; + counter++; // works the same as counter = counter + 1, but is shorter + alert( counter ); // 3 + ``` +- **Decrement** `--` decreases a variable by 1: + + ```js run no-beautify + let counter = 2; + counter--; // works the same as counter = counter - 1, but is shorter + alert( counter ); // 1 + ``` + +```warn +Increment/decrement can only be applied to variables. Trying to use it on a value like `5++` will give an error. +``` + +The operators `++` and `--` can be placed either before or after a variable. + +- When the operator goes after the variable, it is in "postfix form": `counter++`. +- The "prefix form" is when the operator goes before the variable: `++counter`. + +Both of these statements do the same thing: increase `counter` by `1`. + +Is there any difference? Yes, but we can only see it if we use the returned value of `++/--`. + +Let's clarify. As we know, all operators return a value. Increment/decrement is no exception. The prefix form returns the new value while the postfix form returns the old value (prior to increment/decrement). + +To see the difference, here's an example: + +```js run +let counter = 1; +let a = ++counter; // (*) + +alert(a); // *!*2*/!* +``` + +In the line `(*)`, the *prefix* form `++counter` increments `counter` and returns the new value, `2`. So, the `alert` shows `2`. + +Now, let's use the postfix form: + +```js run +let counter = 1; +let a = counter++; // (*) changed ++counter to counter++ + +alert(a); // *!*1*/!* +``` + +In the line `(*)`, the *postfix* form `counter++` also increments `counter` but returns the *old* value (prior to increment). So, the `alert` shows `1`. + +To summarize: + +- If the result of increment/decrement is not used, there is no difference in which form to use: + + ```js run + let counter = 0; + counter++; + ++counter; + alert( counter ); // 2, the lines above did the same + ``` +- If we'd like to increase a value *and* immediately use the result of the operator, we need the prefix form: + + ```js run + let counter = 0; + alert( ++counter ); // 1 + ``` +- If we'd like to increment a value but use its previous value, we need the postfix form: + + ```js run + let counter = 0; + alert( counter++ ); // 0 + ``` + +````smart header="Increment/decrement among other operators" +The operators `++/--` can be used inside expressions as well. Their precedence is higher than most other arithmetical operations. + +For instance: + +```js run +let counter = 1; +alert( 2 * ++counter ); // 4 +``` + +Compare with: + +```js run +let counter = 1; +alert( 2 * counter++ ); // 2, because counter++ returns the "old" value +``` + +Though technically okay, such notation usually makes code less readable. One line does multiple things -- not good. + +While reading code, a fast "vertical" eye-scan can easily miss something like `counter++` and it won't be obvious that the variable increased. + +We advise a style of "one line -- one action": + +```js run +let counter = 1; +alert( 2 * counter ); +counter++; +``` +```` + +## Bitwise operators + +Bitwise operators treat arguments as 32-bit integer numbers and work on the level of their binary representation. + +These operators are not JavaScript-specific. They are supported in most programming languages. + +The list of operators: + +- AND ( `&` ) +- OR ( `|` ) +- XOR ( `^` ) +- NOT ( `~` ) +- LEFT SHIFT ( `<<` ) +- RIGHT SHIFT ( `>>` ) +- ZERO-FILL RIGHT SHIFT ( `>>>` ) + +These operators are used very rarely, when we need to fiddle with numbers on the very lowest (bitwise) level. We won't need these operators any time soon, as web development has little use of them, but in some special areas, such as cryptography, they are useful. You can read the [Bitwise Operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#bitwise_operators) chapter on MDN when a need arises. + +## Comma + +The comma operator `,` is one of the rarest and most unusual operators. Sometimes, it's used to write shorter code, so we need to know it in order to understand what's going on. + +The comma operator allows us to evaluate several expressions, dividing them with a comma `,`. Each of them is evaluated but only the result of the last one is returned. + +For example: + +```js run +*!* +let a = (1 + 2, 3 + 4); +*/!* + +alert( a ); // 7 (the result of 3 + 4) +``` + +Here, the first expression `1 + 2` is evaluated and its result is thrown away. Then, `3 + 4` is evaluated and returned as the result. + +```smart header="Comma has a very low precedence" +Please note that the comma operator has very low precedence, lower than `=`, so parentheses are important in the example above. + +Without them: `a = 1 + 2, 3 + 4` evaluates `+` first, summing the numbers into `a = 3, 7`, then the assignment operator `=` assigns `a = 3`, and the rest is ignored. It's like `(a = 1 + 2), 3 + 4`. +``` + +Why do we need an operator that throws away everything except the last expression? + +Sometimes, people use it in more complex constructs to put several actions in one line. + +For example: + +```js +// three operations in one line +for (*!*a = 1, b = 3, c = a * b*/!*; a < 10; a++) { + ... +} +``` + +Such tricks are used in many JavaScript frameworks. That's why we're mentioning them. But usually they don't improve code readability so we should think well before using them. diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/article.md b/1-js/02-first-steps/09-alert-prompt-confirm/article.md deleted file mode 100644 index 65a4255774..0000000000 --- a/1-js/02-first-steps/09-alert-prompt-confirm/article.md +++ /dev/null @@ -1,109 +0,0 @@ -# Interaction: alert, prompt, confirm - -This part of the tutorial aims to cover JavaScript "as is", without environment-specific tweaks. - -But still we use a browser as the demo environment. So we should know at least a few user-interface functions. In this chapter we'll get familiar with the browser functions `alert`, `prompt` and `confirm`. - -## alert - -Syntax: - -```js -alert(message); -``` - -This shows a message and pauses the script execution until the user presses "OK". - -For example: - -```js run -alert("Hello"); -``` - -The mini-window with the message is called a *modal window*. The word "modal" means that the visitor can't interact with the rest of the page, press other buttons etc, until they have dealt with the window. In this case -- until they press "OK". - -## prompt - -Function `prompt` accepts two arguments: - -```js no-beautify -result = prompt(title[, default]); -``` - -It shows a modal window with a text message, an input field for the visitor and buttons OK/CANCEL. - -`title` -: The text to show to the visitor. - -`default` -: An optional second parameter, the initial value for the input field. - -The visitor may type something in the prompt input field and press OK. Or they can cancel the input by pressing the CANCEL button or hitting the `key:Esc` key. - -The call to `prompt` returns the text from the field or `null` if the input was canceled. - -For instance: - -```js run -let age = prompt('How old are you?', 100); - -alert(`You are ${age} years old!`); // You are 100 years old! -``` - -````warn header="IE: always supply a `default`" -The second parameter is optional. But if we don't supply it, Internet Explorer would insert the text `"undefined"` into the prompt. - -Run this code in Internet Explorer to see that: - -```js run -let test = prompt("Test"); -``` - -So, to look good in IE, it's recommended to always provide the second argument: - -```js run -let test = prompt("Test", ''); // <-- for IE -``` -```` - -## confirm - -The syntax: - -```js -result = confirm(question); -``` - -Function `confirm` shows a modal window with a `question` and two buttons: OK and CANCEL. - -The result is `true` if OK is pressed and `false` otherwise. - -For example: - -```js run -let isBoss = confirm("Are you the boss?"); - -alert( isBoss ); // true if OK is pressed -``` - -## Summary - -We covered 3 browser-specific functions to interact with the visitor: - -`alert` -: shows a message. - -`prompt` -: shows a message asking the user to input text. It returns the text or, if CANCEL or `key:Esc` is clicked, all browsers return `null`. - -`confirm` -: shows a message and waits for the user to press "OK" or "CANCEL". It returns `true` for OK and `false` for CANCEL/`key:Esc`. - -All these methods are modal: they pause the script execution and don't allow the visitor to interact with the rest of the page until the message has been dismissed. - -There are two limitations shared by all the methods above: - -1. The exact location of the modal window is determined by the browser. Usually it's in the center. -2. The exact look of the window also depends on the browser. We can't modify it. - -That is the price for simplicity. There are other ways to show nicer windows and richer interaction with the visitor, but if "bells and whistles" do not matter much, these methods work just fine. diff --git a/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md new file mode 100644 index 0000000000..632b1cf4ea --- /dev/null +++ b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md @@ -0,0 +1,21 @@ + + +```js no-beautify +5 > 4 → true +"apple" > "pineapple" → false +"2" > "12" → true +undefined == null → true +undefined === null → false +null == "\n0\n" → false +null === +"\n0\n" → false +``` + +Some of the reasons: + +1. Obviously, true. +2. Dictionary comparison, hence false. `"a"` is smaller than `"p"`. +3. Again, dictionary comparison, first char `"2"` is greater than the first char `"1"`. +4. Values `null` and `undefined` equal each other only. +5. Strict equality is strict. Different types from both sides lead to false. +6. Similar to `(4)`, `null` only equals `undefined`. +7. Strict equality of different types. diff --git a/1-js/02-first-steps/09-comparison/1-comparison-questions/task.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/task.md new file mode 100644 index 0000000000..be7f75ddd9 --- /dev/null +++ b/1-js/02-first-steps/09-comparison/1-comparison-questions/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Comparisons + +What will be the result for these expressions? + +```js no-beautify +5 > 4 +"apple" > "pineapple" +"2" > "12" +undefined == null +undefined === null +null == "\n0\n" +null === +"\n0\n" +``` + diff --git a/1-js/02-first-steps/09-comparison/article.md b/1-js/02-first-steps/09-comparison/article.md new file mode 100644 index 0000000000..a69317feea --- /dev/null +++ b/1-js/02-first-steps/09-comparison/article.md @@ -0,0 +1,216 @@ +# Comparisons + +We know many comparison operators from maths. + +In JavaScript they are written like this: + +- Greater/less than: a > b, a < b. +- Greater/less than or equals: a >= b, a <= b. +- Equals: `a == b`, please note the double equality sign `==` means the equality test, while a single one `a = b` means an assignment. +- Not equals: In maths the notation is , but in JavaScript it's written as a != b. + +In this article we'll learn more about different types of comparisons, how JavaScript makes them, including important peculiarities. + +At the end you'll find a good recipe to avoid "JavaScript quirks"-related issues. + +## Boolean is the result + +All comparison operators return a boolean value: + +- `true` -- means "yes", "correct" or "the truth". +- `false` -- means "no", "wrong" or "not the truth". + +For example: + +```js run +alert( 2 > 1 ); // true (correct) +alert( 2 == 1 ); // false (wrong) +alert( 2 != 1 ); // true (correct) +``` + +A comparison result can be assigned to a variable, just like any value: + +```js run +let result = 5 > 4; // assign the result of the comparison +alert( result ); // true +``` + +## String comparison + +To see whether a string is greater than another, JavaScript uses the so-called "dictionary" or "lexicographical" order. + +In other words, strings are compared letter-by-letter. + +For example: + +```js run +alert( 'Z' > 'A' ); // true +alert( 'Glow' > 'Glee' ); // true +alert( 'Bee' > 'Be' ); // true +``` + +The algorithm to compare two strings is simple: + +1. Compare the first character of both strings. +2. If the first character from the first string is greater (or less) than the other string's, then the first string is greater (or less) than the second. We're done. +3. Otherwise, if both strings' first characters are the same, compare the second characters the same way. +4. Repeat until the end of either string. +5. If both strings end at the same length, then they are equal. Otherwise, the longer string is greater. + +In the first example above, the comparison `'Z' > 'A'` gets to a result at the first step. + +The second comparison `'Glow'` and `'Glee'` needs more steps as strings are compared character-by-character: + +1. `G` is the same as `G`. +2. `l` is the same as `l`. +3. `o` is greater than `e`. Stop here. The first string is greater. + +```smart header="Not a real dictionary, but Unicode order" +The comparison algorithm given above is roughly equivalent to the one used in dictionaries or phone books, but it's not exactly the same. + +For instance, case matters. A capital letter `"A"` is not equal to the lowercase `"a"`. Which one is greater? The lowercase `"a"`. Why? Because the lowercase character has a greater index in the internal encoding table JavaScript uses (Unicode). We'll get back to specific details and consequences of this in the chapter . +``` + +## Comparison of different types + +When comparing values of different types, JavaScript converts the values to numbers. + +For example: + +```js run +alert( '2' > 1 ); // true, string '2' becomes a number 2 +alert( '01' == 1 ); // true, string '01' becomes a number 1 +``` + +For boolean values, `true` becomes `1` and `false` becomes `0`. + +For example: + +```js run +alert( true == 1 ); // true +alert( false == 0 ); // true +``` + +````smart header="A funny consequence" +It is possible that at the same time: + +- Two values are equal. +- One of them is `true` as a boolean and the other one is `false` as a boolean. + +For example: + +```js run +let a = 0; +alert( Boolean(a) ); // false + +let b = "0"; +alert( Boolean(b) ); // true + +alert(a == b); // true! +``` + +From JavaScript's standpoint, this result is quite normal. An equality check converts values using the numeric conversion (hence `"0"` becomes `0`), while the explicit `Boolean` conversion uses another set of rules. +```` + +## Strict equality + +A regular equality check `==` has a problem. It cannot differentiate `0` from `false`: + +```js run +alert( 0 == false ); // true +``` + +The same thing happens with an empty string: + +```js run +alert( '' == false ); // true +``` + +This happens because operands of different types are converted to numbers by the equality operator `==`. An empty string, just like `false`, becomes a zero. + +What to do if we'd like to differentiate `0` from `false`? + +**A strict equality operator `===` checks the equality without type conversion.** + +In other words, if `a` and `b` are of different types, then `a === b` immediately returns `false` without an attempt to convert them. + +Let's try it: + +```js run +alert( 0 === false ); // false, because the types are different +``` + +There is also a "strict non-equality" operator `!==` analogous to `!=`. + +The strict equality operator is a bit longer to write, but makes it obvious what's going on and leaves less room for errors. + +## Comparison with null and undefined + +There's a non-intuitive behavior when `null` or `undefined` are compared to other values. + +For a strict equality check `===` +: These values are different, because each of them is a different type. + + ```js run + alert( null === undefined ); // false + ``` + +For a non-strict check `==` +: There's a special rule. These two are a "sweet couple": they equal each other (in the sense of `==`), but not any other value. + + ```js run + alert( null == undefined ); // true + ``` + +For maths and other comparisons `< > <= >=` +: `null/undefined` are converted to numbers: `null` becomes `0`, while `undefined` becomes `NaN`. + +Now let's see some funny things that happen when we apply these rules. And, what's more important, how to not fall into a trap with them. + +### Strange result: null vs 0 + +Let's compare `null` with a zero: + +```js run +alert( null > 0 ); // (1) false +alert( null == 0 ); // (2) false +alert( null >= 0 ); // (3) *!*true*/!* +``` + +Mathematically, that's strange. The last result states that "`null` is greater than or equal to zero", so in one of the comparisons above it must be `true`, but they are both false. + +The reason is that an equality check `==` and comparisons `> < >= <=` work differently. Comparisons convert `null` to a number, treating it as `0`. That's why (3) `null >= 0` is true and (1) `null > 0` is false. + +On the other hand, the equality check `==` for `undefined` and `null` is defined such that, without any conversions, they equal each other and don't equal anything else. That's why (2) `null == 0` is false. + +### An incomparable undefined + +The value `undefined` shouldn't be compared to other values: + +```js run +alert( undefined > 0 ); // false (1) +alert( undefined < 0 ); // false (2) +alert( undefined == 0 ); // false (3) +``` + +Why does it dislike zero so much? Always false! + +We get these results because: + +- Comparisons `(1)` and `(2)` return `false` because `undefined` gets converted to `NaN` and `NaN` is a special numeric value which returns `false` for all comparisons. +- The equality check `(3)` returns `false` because `undefined` only equals `null`, `undefined`, and no other value. + +### Avoid problems + +Why did we go over these examples? Should we remember these peculiarities all the time? Well, not really. Actually, these tricky things will gradually become familiar over time, but there's a solid way to avoid problems with them: + +- Treat any comparison with `undefined/null` except the strict equality `===` with exceptional care. +- Don't use comparisons `>= > < <=` with a variable which may be `null/undefined`, unless you're really sure of what you're doing. If a variable can have these values, check for them separately. + +## Summary + +- Comparison operators return a boolean value. +- Strings are compared letter-by-letter in the "dictionary" order. +- When values of different types are compared, they get converted to numbers (with the exclusion of a strict equality check). +- The values `null` and `undefined` equal `==` each other and do not equal any other value. +- Be careful when using comparisons like `>` or `<` with variables that can occasionally be `null/undefined`. Checking for `null/undefined` separately is a good idea. diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png deleted file mode 100644 index 8c57b18850..0000000000 Binary files a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png and /dev/null differ 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 new file mode 100644 index 0000000000..47b020aab1 --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg @@ -0,0 +1 @@ +BeginYou don't know? “ECMAScript”!Right!What's the “official” name of JavaScript?OtherECMAScript \ No newline at end of file diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png deleted file mode 100644 index cbb2c611a0..0000000000 Binary files a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/task.md b/1-js/02-first-steps/10-ifelse/2-check-standard/task.md index 46fe05dc9b..4305584fa6 100644 --- a/1-js/02-first-steps/10-ifelse/2-check-standard/task.md +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/task.md @@ -6,9 +6,8 @@ importance: 2 Using the `if..else` construct, write the code which asks: 'What is the "official" name of JavaScript?' -If the visitor enters "ECMAScript", then output "Right!", otherwise -- output: "Didn't know? ECMAScript!" +If the visitor enters "ECMAScript", then output "Right!", otherwise -- output: "You don't know? ECMAScript!" -![](ifelse_task2.png) +![](ifelse_task2.svg) [demo src="ifelse_task2"] - diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task.png b/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task.png deleted file mode 100644 index 8b54dc83d5..0000000000 Binary files a/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task.png and /dev/null differ diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task@2x.png b/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task@2x.png deleted file mode 100644 index 92001dfe86..0000000000 Binary files a/1-js/02-first-steps/10-ifelse/4-check-login/ifelse_task@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/solution.md b/1-js/02-first-steps/10-ifelse/4-check-login/solution.md deleted file mode 100644 index b535650ec6..0000000000 --- a/1-js/02-first-steps/10-ifelse/4-check-login/solution.md +++ /dev/null @@ -1,25 +0,0 @@ - - -```js run demo -let userName = prompt("Who's there?", ''); - -if (userName == 'Admin') { - - let pass = prompt('Password?', ''); - - if (pass == 'TheMaster') { - alert( 'Welcome!' ); - } else if (pass == '' || pass == null) { - alert( 'Canceled.' ); - } else { - alert( 'Wrong password' ); - } - -} else if (userName == '' || userName == null) { - alert( 'Canceled' ); -} else { - alert( "I don't know you" ); -} -``` - -Note the vertical indents inside the `if` blocks. They are technically not required, but make the code more readable. diff --git a/1-js/02-first-steps/10-ifelse/4-check-login/task.md b/1-js/02-first-steps/10-ifelse/4-check-login/task.md deleted file mode 100644 index 1d12cb0963..0000000000 --- a/1-js/02-first-steps/10-ifelse/4-check-login/task.md +++ /dev/null @@ -1,25 +0,0 @@ -importance: 3 - ---- - -# Check the login - -Write the code which asks for a login with `prompt`. - -If the visitor enters `"Admin"`, then `prompt` for a password, if the input is an empty line or `key:Esc` -- show "Canceled.", if it's another string -- then show "I don't know you". - -The password is checked as follows: - -- If it equals "TheMaster", then show "Welcome!", -- Another string -- show "Wrong password", -- For an empty string or cancelled input, show "Canceled." - -The schema: - -![](ifelse_task.png) - -Please use nested `if` blocks. Mind the overall readability of the code. - -Hint: passing an empty input to a prompt returns an empty string `''`. Pressing `key:ESC` during a prompt returns `null`. - -[demo] diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md index 638ce81f13..ff32354fae 100644 --- a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md +++ b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md @@ -1,6 +1,6 @@ ```js -result = (a + b < 4) ? 'Below' : 'Over'; +let result = (a + b < 4) ? 'Below' : 'Over'; ``` diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md index 684e239f2f..6bdf8453ea 100644 --- a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md +++ b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md @@ -4,13 +4,14 @@ importance: 5 # Rewrite 'if' into '?' -Rewrite this `if` using the ternary operator `'?'`: +Rewrite this `if` using the conditional operator `'?'`: ```js +let result; + if (a + b < 4) { result = 'Below'; } else { result = 'Over'; } ``` - diff --git a/1-js/02-first-steps/10-ifelse/article.md b/1-js/02-first-steps/10-ifelse/article.md index b3ab599728..82e8800b9c 100644 --- a/1-js/02-first-steps/10-ifelse/article.md +++ b/1-js/02-first-steps/10-ifelse/article.md @@ -1,12 +1,12 @@ -# Conditional operators: if, '?' +# Conditional branching: if, '?' -Sometimes we need to perform different actions based on a condition. +Sometimes, we need to perform different actions based on different conditions. -There is the `if` statement for that and also the conditional (ternary) operator for conditional evaluation which we will be referring as the “question mark” operator `?` for simplicity. +To do that, we can use the `if` statement and the conditional operator `?`, that's also called a "question mark" operator. ## The "if" statement -The `if` statement gets a condition, evaluates it and, if the result is `true`, executes the code. +The `if(...)` statement evaluates a condition in parentheses and, if the result is `true`, executes a block of code. For example: @@ -18,9 +18,9 @@ if (year == 2015) alert( 'You are right!' ); */!* ``` -In the example above, the condition is a simple equality check: `year == 2015`, but it can be much more complex. +In the example above, the condition is a simple equality check (`year == 2015`), but it can be much more complex. -If there is more than one statement to be executed, we have to wrap our code block inside curly braces: +If we want to execute more than one statement, we have to wrap our code block inside curly braces: ```js if (year == 2015) { @@ -29,15 +29,15 @@ if (year == 2015) { } ``` -It is recommended to wrap your code block with curly braces `{}` every time with `if`, even if there is only one statement. That improves readability. +We recommend wrapping your code block with curly braces `{}` every time you use an `if` statement, even if there is only one statement to execute. Doing so improves readability. ## Boolean conversion -The `if (…)` statement evaluates the expression in parentheses and converts it to the boolean type. +The `if (…)` statement evaluates the expression in its parentheses and converts the result to a boolean. Let's recall the conversion rules from the chapter : -- A number `0`, an empty string `""`, `null`, `undefined` and `NaN` become `false`. Because of that they are called "falsy" values. +- A number `0`, an empty string `""`, `null`, `undefined`, and `NaN` all become `false`. Because of that they are called "falsy" values. - Other values become `true`, so they are called "truthy". So, the code under this condition would never execute: @@ -48,7 +48,7 @@ if (0) { // 0 is falsy } ``` -...And inside this condition -- always works: +...and inside this condition -- it always will: ```js if (1) { // 1 is truthy @@ -56,7 +56,7 @@ if (1) { // 1 is truthy } ``` -We can also pass a pre-evaluated boolean value to `if`, like here: +We can also pass a pre-evaluated boolean value to `if`, like this: ```js let cond = (year == 2015); // equality evaluates to true or false @@ -68,11 +68,11 @@ if (cond) { ## The "else" clause -The `if` statement may contain an optional "else" block. It executes when the condition is wrong. +The `if` statement may contain an optional `else` block. It executes when the condition is falsy. For example: ```js run -let year = prompt('In which year was ECMAScript-2015 specification published?', ''); +let year = prompt('In which year was the ECMAScript-2015 specification published?', ''); if (year == 2015) { alert( 'You guessed it right!' ); @@ -83,12 +83,12 @@ if (year == 2015) { ## Several conditions: "else if" -Sometimes we'd like to test several variants of a condition. There is an `else if` clause for that. +Sometimes, we'd like to test several variants of a condition. The `else if` clause lets us do that. For example: ```js run -let year = prompt('In which year was ECMAScript-2015 specification published?', ''); +let year = prompt('In which year was the ECMAScript-2015 specification published?', ''); if (year < 2015) { alert( 'Too early...' ); @@ -99,13 +99,13 @@ if (year < 2015) { } ``` -In the code above JavaScript first checks `year < 2015`. If it is falsy it then goes to the next condition `year > 2015`, and otherwise shows the last `alert`. +In the code above, JavaScript first checks `year < 2015`. If that is falsy, it goes to the next condition `year > 2015`. If that is also falsy, it shows the last `alert`. -There can be more `else if` blocks. The ending `else` is optional. +There can be more `else if` blocks. The final `else` is optional. -## Ternary operator '?' +## Conditional operator '?' -Sometimes we need to assign a variable depending on a condition. +Sometimes, we need to assign a variable depending on a condition. For instance: @@ -124,16 +124,16 @@ if (age > 18) { alert(accessAllowed); ``` -The so-called "ternary" or "question mark" operator lets us do that shorter and simpler. +The so-called "conditional" or "question mark" operator lets us do that in a shorter and simpler way. -The operator is represented by a question mark `?`. The formal term "ternary" means that the operator has three operands. It is actually the one and only operator in JavaScript which has that many. +The operator is represented by a question mark `?`. Sometimes it's called "ternary", because the operator has three operands. It is actually the one and only operator in JavaScript which has that many. The syntax is: ```js -let result = condition ? value1 : value2 +let result = condition ? value1 : value2; ``` -The `condition` is evaluated, if it's truthy then `value1` is returned, otherwise -- `value2`. +The `condition` is evaluated: if it's truthy then `value1` is returned, otherwise -- `value2`. For example: @@ -141,7 +141,9 @@ For example: let accessAllowed = (age > 18) ? true : false; ``` -Technically, we can omit parentheses around `age > 18`. The question mark operator has a low precedence. It executes after the comparison `>`, so that'll do the same: +Technically, we can omit the parentheses around `age > 18`. The question mark operator has a low precedence, so it executes after the comparison `>`. + +This example will do the same thing as the previous one: ```js // the comparison operator "age > 18" executes first anyway @@ -149,10 +151,10 @@ Technically, we can omit parentheses around `age > 18`. The question mark operat let accessAllowed = age > 18 ? true : false; ``` -But parentheses make the code more readable, so it's recommended to use them. +But parentheses make the code more readable, so we recommend using them. ````smart -In the example above it's possible to evade the question mark operator, because the comparison by itself returns `true/false`: +In the example above, you can avoid using the question mark operator because the comparison itself returns `true/false`: ```js // the same @@ -162,7 +164,7 @@ let accessAllowed = age > 18; ## Multiple '?' -A sequence of question mark `?` operators allows returning a value that depends on more than one condition. +A sequence of question mark operators `?` can return a value that depends on more than one condition. For instance: ```js run @@ -176,14 +178,14 @@ let message = (age < 3) ? 'Hi, baby!' : alert( message ); ``` -It may be difficult at first to grasp what's going on. But after a closer look we can see that it's just an ordinary sequence of tests. +It may be difficult at first to grasp what's going on. But after a closer look, we can see that it's just an ordinary sequence of tests: 1. The first question mark checks whether `age < 3`. -2. If true -- returns `'Hi, baby!'`, otherwise -- goes after the colon `":"` and checks for `age < 18`. -3. If that's true -- returns `'Hello!'`, otherwise -- goes after the next colon `":"` and checks for `age < 100`. -4. If that's true -- returns `'Greetings!'`, otherwise -- goes after the last colon `":"` and returns `'What an unusual age!'`. +2. If true -- it returns `'Hi, baby!'`. Otherwise, it continues to the expression after the colon ":", checking `age < 18`. +3. If that's true -- it returns `'Hello!'`. Otherwise, it continues to the expression after the next colon ":", checking `age < 100`. +4. If that's true -- it returns `'Greetings!'`. Otherwise, it continues to the expression after the last colon ":", returning `'What an unusual age!'`. -The same logic using `if..else`: +Here's how this looks using `if..else`: ```js if (age < 3) { @@ -210,15 +212,15 @@ let company = prompt('Which company created JavaScript?', ''); */!* ``` -Depending on the condition `company == 'Netscape'`, either the first or the second part after `?` gets executed and shows the alert. +Depending on the condition `company == 'Netscape'`, either the first or the second expression after the `?` gets executed and shows an alert. -We don't assign a result to a variable here. The idea is to execute different code depending on the condition. +We don't assign a result to a variable here. Instead, we execute different code depending on the condition. -**It is not recommended to use the question mark operator in this way.** +**It's not recommended to use the question mark operator in this way.** -The notation seems to be shorter than `if`, which appeals to some programmers. But it is less readable. +The notation is shorter than the equivalent `if` statement, which appeals to some programmers. But it is less readable. -Here is the same code with `if` for comparison: +Here is the same code using `if` for comparison: ```js run no-beautify let company = prompt('Which company created JavaScript?', ''); @@ -232,6 +234,6 @@ if (company == 'Netscape') { */!* ``` -Our eyes scan the code vertically. The constructs which span several lines are easier to understand than a long horizontal instruction set. +Our eyes scan the code vertically. Code blocks which span several lines are easier to understand than a long, horizontal instruction set. -The idea of a question mark `?` is to return one or another value depending on the condition. Please use it for exactly that. There is `if` to execute different branches of the code. +The purpose of the question mark operator `?` is to return one value or another depending on its condition. Please use it for exactly that. Use `if` when you need to execute different branches of code. diff --git a/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md b/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md index eda8c90589..a7c9addfc7 100644 --- a/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md +++ b/1-js/02-first-steps/11-logical-operators/1-alert-null-2-undefined/task.md @@ -4,7 +4,7 @@ importance: 5 # What's the result of OR? -What the code below is going to output? +What is the code below going to output? ```js alert( null || 2 || undefined ); diff --git a/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md b/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md index 8f4d664e87..f85b563662 100644 --- a/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md +++ b/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md @@ -6,7 +6,7 @@ alert( alert(1) || 2 || alert(3) ); The call to `alert` does not return a value. Or, in other words, it returns `undefined`. -1. The first OR `||` evaluates it's left operand `alert(1)`. That shows the first message with `1`. +1. The first OR `||` evaluates its left operand `alert(1)`. That shows the first message with `1`. 2. The `alert` returns `undefined`, so OR goes on to the second operand searching for a truthy value. 3. The second operand `2` is truthy, so the execution is halted, `2` is returned and then shown by the outer alert. diff --git a/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md b/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md index bc622abfc2..3908fa2ece 100644 --- a/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md +++ b/1-js/02-first-steps/11-logical-operators/2-alert-or/task.md @@ -4,7 +4,7 @@ importance: 3 # What's the result of OR'ed alerts? -What the code below will output? +What will the code below output? ```js alert( alert(1) || 2 || alert(3) ); diff --git a/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md index 5c2455ef48..368b594094 100644 --- a/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md +++ b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md @@ -1,6 +1,6 @@ The answer: `null`, because it's the first falsy value from the list. ```js run -alert( 1 && null && 2 ); +alert(1 && null && 2); ``` diff --git a/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md index 53ec7874ac..043d431e41 100644 --- a/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md +++ b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/task.md @@ -4,7 +4,7 @@ importance: 5 # What is the result of AND? -What this code is going to show? +What is this code going to show? ```js alert( 1 && null && 2 ); diff --git a/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md index 32a8ccf257..25e3568f8b 100644 --- a/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md +++ b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/solution.md @@ -12,5 +12,5 @@ The result of `2 && 3 = 3`, so the expression becomes: null || 3 || 4 ``` -Now the result if the first truthy value: `3`. +Now the result is the first truthy value: `3`. diff --git a/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md index 4b2ad046e5..b18bb9c514 100644 --- a/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md +++ b/1-js/02-first-steps/11-logical-operators/5-alert-and-or/task.md @@ -4,7 +4,7 @@ importance: 5 # The result of OR AND OR -What will be the result? +What will the result be? ```js alert( null || 2 && 3 || 4 ); 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/6-check-if-in-range/task.md index cc00ca9fcd..fc9e336c15 100644 --- 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/6-check-if-in-range/task.md @@ -4,6 +4,6 @@ importance: 3 # Check the range between -Write an "if" condition to check that `age` is between `14` and `90` inclusively. +Write an `if` condition to check that `age` is between `14` and `90` inclusively. "Inclusively" means that `age` can reach the edges `14` or `90`. 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/7-check-if-out-range/task.md index 7c22d6ad1d..9b947d00f8 100644 --- 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/7-check-if-out-range/task.md @@ -4,6 +4,6 @@ importance: 3 # Check the range outside -Write an `if` condition to check that `age` is NOT between 14 and 90 inclusively. +Write an `if` condition to check that `age` is NOT between `14` and `90` inclusively. Create two variants: the first one using NOT `!`, the second one -- without it. 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/8-if-question/task.md index f824779bf6..55987121b0 100644 --- a/1-js/02-first-steps/11-logical-operators/8-if-question/task.md +++ b/1-js/02-first-steps/11-logical-operators/8-if-question/task.md @@ -6,7 +6,7 @@ importance: 5 Which of these `alert`s are going to execute? -What will be the results of the expressions inside `if(...)`? +What will the results of the expressions be inside `if(...)`? ```js if (-1 || 0) alert( 'first' ); diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg new file mode 100644 index 0000000000..d22b518a91 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg @@ -0,0 +1 @@ +BeginCanceledCanceledWelcome!I don't know youWrong passwordWho's there?Password?CancelCancelAdminTheMasterOtherOther \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/solution.md b/1-js/02-first-steps/11-logical-operators/9-check-login/solution.md new file mode 100644 index 0000000000..604606259f --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/9-check-login/solution.md @@ -0,0 +1,25 @@ + + +```js run demo +let userName = prompt("Who's there?", ''); + +if (userName === 'Admin') { + + let pass = prompt('Password?', ''); + + if (pass === 'TheMaster') { + alert( 'Welcome!' ); + } else if (pass === '' || pass === null) { + alert( 'Canceled' ); + } else { + alert( 'Wrong password' ); + } + +} else if (userName === '' || userName === null) { + alert( 'Canceled' ); +} else { + alert( "I don't know you" ); +} +``` + +Note the vertical indents inside the `if` blocks. They are technically not required, but make the code more readable. 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/9-check-login/task.md new file mode 100644 index 0000000000..290a52642f --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/9-check-login/task.md @@ -0,0 +1,25 @@ +importance: 3 + +--- + +# Check the login + +Write the code which asks for a login with `prompt`. + +If the visitor enters `"Admin"`, then `prompt` for a password, if the input is an empty line or `key:Esc` -- show "Canceled", if it's another string -- then show "I don't know you". + +The password is checked as follows: + +- If it equals "TheMaster", then show "Welcome!", +- Another string -- show "Wrong password", +- For an empty string or cancelled input, show "Canceled" + +The schema: + +![](ifelse_task.svg) + +Please use nested `if` blocks. Mind the overall readability of the code. + +Hint: passing an empty input to a prompt returns an empty string `''`. Pressing `key:ESC` during a prompt returns `null`. + +[demo] 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 d2431494ec..78c4fd2f1b 100644 --- a/1-js/02-first-steps/11-logical-operators/article.md +++ b/1-js/02-first-steps/11-logical-operators/article.md @@ -1,8 +1,8 @@ # Logical operators -There are three logical operators in JavaScript: `||` (OR), `&&` (AND), `!` (NOT). +There are four logical operators in JavaScript: `||` (OR), `&&` (AND), `!` (NOT), `??` (Nullish Coalescing). Here we cover the first three, the `??` operator is in the next article. -Although they are called "logical", they can be applied to values of any type, not only boolean. The result can also be of any type. +Although they are called "logical", they can be applied to values of any type, not only boolean. Their result can also be of any type. Let's see the details. @@ -14,9 +14,9 @@ The "OR" operator is represented with two vertical line symbols: result = a || b; ``` -In classical programming, logical OR is meant to manipulate boolean values only. If any of its arguments are `true`, then it returns `true`, otherwise it returns `false`. +In classical programming, the logical OR is meant to manipulate boolean values only. If any of its arguments are `true`, it returns `true`, otherwise it returns `false`. -In JavaScript the operator is a little bit more tricky and powerful. But first let's see what happens with boolean values. +In JavaScript, the operator is a little bit trickier and more powerful. But first, let's see what happens with boolean values. There are four possible logical combinations: @@ -29,9 +29,9 @@ alert( false || false ); // false As we can see, the result is always `true` except for the case when both operands are `false`. -If an operand is not boolean, then it's converted to boolean for the evaluation. +If an operand is not a boolean, it's converted to a boolean for the evaluation. -For instance, a number `1` is treated as `true`, a number `0` -- as `false`: +For instance, the number `1` is treated as `true`, the number `0` as `false`: ```js run if (1 || 0) { // works just like if( true || false ) @@ -39,7 +39,7 @@ if (1 || 0) { // works just like if( true || false ) } ``` -Most of the time, OR `||` is used in an `if` statement to test if *any* of the given conditions is correct. +Most of the time, OR `||` is used in an `if` statement to test if *any* of the given conditions is `true`. For example: @@ -64,9 +64,9 @@ if (hour < 10 || hour > 18 || isWeekend) { } ``` -## OR seeks the first truthy value +## OR "||" finds the first truthy value [#or-finds-the-first-truthy-value] -The logic described above is somewhat classical. Now let's bring in the "extra" features of JavaScript. +The logic described above is somewhat classical. Now, let's bring in the "extra" features of JavaScript. The extended algorithm works as follows. @@ -78,76 +78,63 @@ result = value1 || value2 || value3; The OR `||` operator does the following: -- Evaluate operands from left to right. -- For each operand, convert it to boolean. If the result is `true`, then stop and return the original value of that operand. -- If all other operands have been assessed (i.e. all were `false`), return the last operand. +- Evaluates operands from left to right. +- For each operand, converts it to boolean. If the result is `true`, stops and returns the original value of that operand. +- If all operands have been evaluated (i.e. all were `false`), returns the last operand. A value is returned in its original form, without the conversion. -In other words, a chain of OR `"||"` returns the first truthy value or the last one if no such value is found. +In other words, a chain of OR `||` returns the first truthy value or the last one if no truthy value is found. For instance: ```js run alert( 1 || 0 ); // 1 (1 is truthy) -alert( true || 'no matter what' ); // (true is truthy) alert( null || 1 ); // 1 (1 is the first truthy value) alert( null || 0 || 1 ); // 1 (the first truthy value) + alert( undefined || null || 0 ); // 0 (all falsy, returns the last value) ``` -That leads to some interesting usages compared to a "pure, classical, boolean-only OR". +This leads to some interesting usage compared to a "pure, classical, boolean-only OR". -1. **Getting the first truthy value from the list of variables or expressions.** +1. **Getting the first truthy value from a list of variables or expressions.** - Imagine we have several variables, which can either contain the data or be `null/undefined`. And we need to choose the first one with data. + For instance, we have `firstName`, `lastName` and `nickName` variables, all optional (i.e. can be undefined or have falsy values). - We can use OR `||` for that: + Let's use OR `||` to choose the one that has the data and show it (or `"Anonymous"` if nothing set): ```js run - let currentUser = null; - let defaultUser = "John"; + let firstName = ""; + let lastName = ""; + let nickName = "SuperCoder"; *!* - let name = currentUser || defaultUser || "unnamed"; + alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder */!* - - alert( name ); // selects "John" – the first truthy value ``` - If both `currentUser` and `defaultUser` were falsy then `"unnamed"` would be the result. -2. **Short-circuit evaluation.** - - Operands can be not only values, but arbitrary expressions. OR evaluates and tests them from left to right. The evaluation stops when a truthy value is reached, and the value is returned. The process is called "a short-circuit evaluation", because it goes as short as possible from left to right. + If all variables were falsy, `"Anonymous"` would show up. - This is clearly seen when the expression given as the second argument has a side effect. Like a variable assignment. +2. **Short-circuit evaluation.** - If we run the example below, `x` would not get assigned: + Another feature of OR `||` operator is the so-called "short-circuit" evaluation. - ```js run no-beautify - let x; + It means that `||` processes its arguments until the first truthy value is reached, and then the value is returned immediately, without even touching the other argument. - *!*true*/!* || (x = 1); + The importance of this feature becomes obvious if an operand isn't just a value, but an expression with a side effect, such as a variable assignment or a function call. - alert(x); // undefined, because (x = 1) not evaluated - ``` - - ...And if the first argument is `false`, then `OR` goes on and evaluates the second one thus running the assignment: + In the example below, only the second message is printed: ```js run no-beautify - let x; - - *!*false*/!* || (x = 1); - - alert(x); // 1 + *!*true*/!* || alert("not printed"); + *!*false*/!* || alert("printed"); ``` - An assignment is a simple case, other side effects can be involved. + In the first line, the OR `||` operator stops the evaluation immediately upon seeing `true`, so the `alert` isn't run. - As we can see, such a use case is a "shorter way to do `if`". The first operand is converted to boolean and if it's false then the second one is evaluated. - - Most of time it's better to use a "regular" `if` to keep the code easy to understand, but sometimes that can be handy. + Sometimes, people use this feature to execute commands only if the condition on the left part is falsy. ## && (AND) @@ -157,7 +144,7 @@ The AND operator is represented with two ampersands `&&`: result = a && b; ``` -In classical programming AND returns `true` if both operands are truthy and `false` otherwise: +In classical programming, AND returns `true` if both operands are truthy and `false` otherwise: ```js run alert( true && true ); // true @@ -173,11 +160,11 @@ let hour = 12; let minute = 30; if (hour == 12 && minute == 30) { - alert( 'Time is 12:30' ); + alert( 'The time is 12:30' ); } ``` -Just as for OR, any value is allowed as an operand of AND: +Just as with OR, any value is allowed as an operand of AND: ```js run if (1 && 0) { // evaluated as true && false @@ -186,7 +173,7 @@ if (1 && 0) { // evaluated as true && false ``` -## AND seeks the first falsy value +## AND "&&" finds the first falsy value Given multiple AND'ed values: @@ -196,9 +183,9 @@ result = value1 && value2 && value3; The AND `&&` operator does the following: -- Evaluate operands from left to right. -- For each operand, convert it to a boolean. If the result is `false`, stop and return the original value of that operand. -- If all other operands have been assessed (i.e. all were truthy), return the last operand. +- Evaluates operands from left to right. +- For each operand, converts it to a boolean. If the result is `false`, stops and returns the original value of that operand. +- If all operands have been evaluated (i.e. all were truthy), returns the last operand. In other words, AND returns the first falsy value or the last value if none were found. @@ -233,10 +220,11 @@ alert( 1 && 2 && 3 ); // 3, the last one ````smart header="Precedence of AND `&&` is higher than OR `||`" The precedence of AND `&&` operator is higher than OR `||`. -So the code `a && b || c && d` is essentially the same as if `&&` were in parentheses: `(a && b) || (c && d)`. +So the code `a && b || c && d` is essentially the same as if the `&&` expressions were in parentheses: `(a && b) || (c && d)`. ```` -Just like OR, the AND `&&` operator can sometimes replace `if`. +````warn header="Don't replace `if` with `||` or `&&`" +Sometimes, people use the AND `&&` operator as a "shorter way to write `if`". For instance: @@ -246,21 +234,19 @@ let x = 1; (x > 0) && alert( 'Greater than zero!' ); ``` -The action in the right part of `&&` would execute only if the evaluation reaches it. That is: only if `(x > 0)` is true. +The action in the right part of `&&` would execute only if the evaluation reaches it. That is, only if `(x > 0)` is true. So we basically have an analogue for: ```js run let x = 1; -if (x > 0) { - alert( 'Greater than zero!' ); -} +if (x > 0) alert( 'Greater than zero!' ); ``` -The variant with `&&` appears to be shorter. But `if` is more obvious and tends to be a little bit more readable. +Although, the variant with `&&` appears shorter, `if` is more obvious and tends to be a little bit more readable. So we recommend using every construct for its purpose: use `if` if we want `if` and use `&&` if we want AND. +```` -So it is recommended to use every construct for its purpose. Use `if` if we want if. And use `&&` if we want AND. ## ! (NOT) @@ -275,7 +261,7 @@ result = !value; The operator accepts a single argument and does the following: 1. Converts the operand to boolean type: `true/false`. -2. Returns an inverse value. +2. Returns the inverse value. For instance: @@ -291,7 +277,7 @@ alert( !!"non-empty string" ); // true alert( !!null ); // false ``` -That is, the first NOT converts the value to boolean and returns the inverse, and the second NOT inverses it again. At the end we have a plain value-to-boolean conversion. +That is, the first NOT converts the value to boolean and returns the inverse, and the second NOT inverses it again. In the end, we have a plain value-to-boolean conversion. There's a little more verbose way to do the same thing -- a built-in `Boolean` function: @@ -300,4 +286,4 @@ alert( Boolean("non-empty string") ); // true alert( Boolean(null) ); // false ``` -The precedence of NOT `!` is the highest of all bitwise operators, so it always executes first, before any `&&`, `||`. +The precedence of NOT `!` is the highest of all logical operators, so it always executes first, before `&&` or `||`. 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 new file mode 100644 index 0000000000..0b2f092ab7 --- /dev/null +++ b/1-js/02-first-steps/12-nullish-coalescing-operator/article.md @@ -0,0 +1,169 @@ +# Nullish coalescing operator '??' + +[recent browser="new"] + +The nullish coalescing operator is written as two question marks `??`. + +As it treats `null` and `undefined` similarly, we'll use a special term here, in this article. For brevity, we'll say that a value is "defined" when it's neither `null` nor `undefined`. + +The result of `a ?? b` is: +- if `a` is defined, then `a`, +- if `a` isn't defined, then `b`. + +In other words, `??` returns the first argument if it's not `null/undefined`. Otherwise, the second one. + +The nullish coalescing operator isn't anything completely new. It's just a nice syntax to get the first "defined" value of the two. + +We can rewrite `result = a ?? b` using the operators that we already know, like this: + +```js +result = (a !== null && a !== undefined) ? a : b; +``` + +Now it should be absolutely clear what `??` does. Let's see where it helps. + +The common use case for `??` is to provide a default value. + +For example, here we show `user` if its value isn't `null/undefined`, otherwise `Anonymous`: + +```js run +let user; + +alert(user ?? "Anonymous"); // Anonymous (user is undefined) +``` + +Here's the example with `user` assigned to a name: + +```js run +let user = "John"; + +alert(user ?? "Anonymous"); // John (user is not null/undefined) +``` + +We can also use a sequence of `??` to select the first value from a list that isn't `null/undefined`. + +Let's say we have a user's data in variables `firstName`, `lastName` or `nickName`. All of them may be not defined, if the user decided not to fill in the corresponding values. + +We'd like to display the user name using one of these variables, or show "Anonymous" if all of them are `null/undefined`. + +Let's use the `??` operator for that: + +```js run +let firstName = null; +let lastName = null; +let nickName = "Supercoder"; + +// shows the first defined value: +*!* +alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder +*/!* +``` + +## Comparison with || + +The OR `||` operator can be used in the same way as `??`, as it was described in the [previous chapter](info:logical-operators#or-finds-the-first-truthy-value). + +For example, in the code above we could replace `??` with `||` and still get the same result: + +```js run +let firstName = null; +let lastName = null; +let nickName = "Supercoder"; + +// shows the first truthy value: +*!* +alert(firstName || lastName || nickName || "Anonymous"); // Supercoder +*/!* +``` + +Historically, the OR `||` operator was there first. It's been there since the beginning of JavaScript, so developers were using it for such purposes for a long time. + +On the other hand, the nullish coalescing operator `??` was added to JavaScript only recently, and the reason for that was that people weren't quite happy with `||`. + +The important difference between them is that: +- `||` returns the first *truthy* value. +- `??` returns the first *defined* value. + +In other words, `||` doesn't distinguish between `false`, `0`, an empty string `""` and `null/undefined`. They are all the same -- falsy values. If any of these is the first argument of `||`, then we'll get the second argument as the result. + +In practice though, we may want to use default value only when the variable is `null/undefined`. That is, when the value is really unknown/not set. + +For example, consider this: + +```js run +let height = 0; + +alert(height || 100); // 100 +alert(height ?? 100); // 0 +``` + +- The `height || 100` checks `height` for being a falsy value, and it's `0`, falsy indeed. + - so the result of `||` is the second argument, `100`. +- The `height ?? 100` checks `height` for being `null/undefined`, and it's not, + - so the result is `height` "as is", that is `0`. + +In practice, the zero height is often a valid value, that shouldn't be replaced with the default. So `??` does just the right thing. + +## Precedence + +The precedence of the `??` operator is the same as `||`. They both equal `3` in the [MDN table](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table). + +That means that, just like `||`, the nullish coalescing operator `??` is evaluated before `=` and `?`, but after most other operations, such as `+`, `*`. + +So we may need to add parentheses in expressions like this: + +```js run +let height = null; +let width = null; + +// important: use parentheses +let area = (height ?? 100) * (width ?? 50); + +alert(area); // 5000 +``` + +Otherwise, if we omit parentheses, then as `*` has the higher precedence than `??`, it would execute first, leading to incorrect results. + +```js +// without parentheses +let area = height ?? 100 * width ?? 50; + +// ...works this way (not what we want): +let area = height ?? (100 * width) ?? 50; +``` + +### Using ?? with && or || + +Due to safety reasons, JavaScript forbids using `??` together with `&&` and `||` operators, unless the precedence is explicitly specified with parentheses. + +The code below triggers a syntax error: + +```js run +let x = 1 && 2 ?? 3; // Syntax error +``` + +The limitation is surely debatable, it was added to the language specification with the purpose to avoid programming mistakes, when people start to switch from `||` to `??`. + +Use explicit parentheses to work around it: + +```js run +*!* +let x = (1 && 2) ?? 3; // Works +*/!* + +alert(x); // 2 +``` + +## Summary + +- The nullish coalescing operator `??` provides a short way to choose the first "defined" value from a list. + + It's used to assign default values to variables: + + ```js + // set height=100, if height is null or undefined + height = height ?? 100; + ``` + +- The operator `??` has a very low precedence, only a bit higher than `?` and `=`, so consider adding parentheses when using it in an expression. +- It's forbidden to use it with `||` or `&&` without explicit parentheses. diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md deleted file mode 100644 index add79c8734..0000000000 --- a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md +++ /dev/null @@ -1,30 +0,0 @@ -The task demonstrates how postfix/prefix forms can lead to different results when used in comparisons. - -1. **From 1 to 4** - - ```js run - let i = 0; - while (++i < 5) alert( i ); - ``` - - The first value is `i=1`, because `++i` first increments `i` and then returns the new value. So the first comparison is `1 < 5` and the `alert` shows `1`. - - Then follow `2,3,4…` -- the values show up one after another. The comparison always uses the incremented value, because `++` is before the variable. - - Finally, `i=4` is incremented to `5`, the comparison `while(5 < 5)` fails, and the loop stops. So `5` is not shown. -2. **From 1 to 5** - - ```js run - let i = 0; - while (i++ < 5) alert( i ); - ``` - - The first value is again `i=1`. The postfix form of `i++` increments `i` and then returns the *old* value, so the comparison `i++ < 5` will use `i=0` (contrary to `++i < 5`). - - But the `alert` call is separate. It's another statement which executes after the increment and the comparison. So it gets the current `i=1`. - - Then follow `2,3,4…` - - Let's stop on `i=4`. The prefix form `++i` would increment it and use `5` in the comparison. But here we have the postfix form `i++`. So it increments `i` to `5`, but returns the old value. Hence the comparison is actually `while(4 < 5)` -- true, and the control goes on to `alert`. - - The value `i=5` is the last one, because on the next step `while(5 < 5)` is false. diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/task.md b/1-js/02-first-steps/12-while-for/2-which-value-while/task.md deleted file mode 100644 index 4576db350f..0000000000 --- a/1-js/02-first-steps/12-while-for/2-which-value-while/task.md +++ /dev/null @@ -1,22 +0,0 @@ -importance: 4 - ---- - -# Which values shows the while? - -For every loop, write down which values it shows, in your opinion. And then compare with the answer. - -Both loops `alert` same values or not? - -1. The prefix form `++i`: - - ```js - let i = 0; - while (++i < 5) alert( i ); - ``` -2. The postfix form `i++` - - ```js - let i = 0; - while (i++ < 5) alert( i ); - ``` diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md b/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md deleted file mode 100644 index 2e04a78c43..0000000000 --- a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md +++ /dev/null @@ -1,15 +0,0 @@ - -```js run demo -let num; - -do { - num = prompt("Enter a number greater than 100?", 0); -} while (num <= 100 && num); -``` - -The loop `do..while` repeats while both checks are truthy: - -1. The check for `num <= 100` -- that is, the entered value is still not greater than `100`. -2. The check `&& num` is false when `num` is `null` or a empty string. Then the `while` loop stops too. - -P.S. If `num` is `null` then `num <= 100` is `true`, so without the 2nd check the loop wouldn't stop if the user clicks CANCEL. Both checks are required. diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/solution.md b/1-js/02-first-steps/12-while-for/7-list-primes/solution.md deleted file mode 100644 index ec9063173a..0000000000 --- a/1-js/02-first-steps/12-while-for/7-list-primes/solution.md +++ /dev/null @@ -1,29 +0,0 @@ -There are many algorithms for this task. - -Let's use a nested loop: - -```js -For each i in the interval { - check if i has a divisor from 1..i - if yes => the value is not a prime - if no => the value is a prime, show it -} -``` - -The code using a label: - -```js run -let n = 10; - -nextPrime: -for (let i = 2; i <= n; i++) { // for each i... - - for (let j = 2; j < i; j++) { // look for a divisor.. - if (i % j == 0) continue nextPrime; // not a prime, go next i - } - - alert( i ); // a prime -} -``` - -There's a lot of space to opimize it. For instance, we could look for the divisors from `2` to square root of `i`. But anyway, if we want to be really efficient for large intervals, we need change the approach and rely on advanced maths and complex algorithms like [Quadratic sieve](https://en.wikipedia.org/wiki/Quadratic_sieve), [General number field sieve](https://en.wikipedia.org/wiki/General_number_field_sieve) etc. diff --git a/1-js/02-first-steps/12-while-for/article.md b/1-js/02-first-steps/12-while-for/article.md deleted file mode 100644 index e600b8c4b9..0000000000 --- a/1-js/02-first-steps/12-while-for/article.md +++ /dev/null @@ -1,388 +0,0 @@ -# Loops: while and for - -We often have a need to perform similar actions many times in a row. - -For example, when we need to output goods from a list one after another. Or just run the same code for each number from 1 to 10. - -*Loops* are a way to repeat the same part of code multiple times. - -## The "while" loop - -The `while` loop has the following syntax: - -```js -while (condition) { - // code - // so-called "loop body" -} -``` - -While the `condition` is `true`, the `code` from the loop body is executed. - -For instance, the loop below outputs `i` while `i < 3`: - -```js run -let i = 0; -while (i < 3) { // shows 0, then 1, then 2 - alert( i ); - i++; -} -``` - -A single execution of the loop body is called *an iteration*. The loop in the example above makes three iterations. - -If there were no `i++` in the example above, the loop would repeat (in theory) forever. In practice, the browser provides ways to stop such loops, and for server-side JavaScript we can kill the process. - -Any expression or a variable can be a loop condition, not just a comparison. They are evaluated and converted to a boolean by `while`. - -For instance, the shorter way to write `while (i != 0)` could be `while (i)`: - -```js run -let i = 3; -*!* -while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops -*/!* - alert( i ); - i--; -} -``` - -````smart header="Brackets are not required for a single-line body" -If the loop body has a single statement, we can omit the brackets `{…}`: - -```js run -let i = 3; -*!* -while (i) alert(i--); -*/!* -``` -```` - -## The "do..while" loop - -The condition check can be moved *below* the loop body using the `do..while` syntax: - -```js -do { - // loop body -} while (condition); -``` - -The loop will first execute the body, then check the condition and, while it's truthy, execute it again and again. - -For example: - -```js run -let i = 0; -do { - alert( i ); - i++; -} while (i < 3); -``` - -This form of syntax is rarely used except when you want the body of the loop to execute **at least once** regardless of the condition being truthy. Usually, the other form is preferred: `while(…) {…}`. - -## The "for" loop - -The `for` loop is the most often used one. - -It looks like this: - -```js -for (begin; condition; step) { - // ... loop body ... -} -``` - -Let's learn the meaning of these parts by example. The loop below runs `alert(i)` for `i` from `0` up to (but not including) `3`: - -```js run -for (let i = 0; i < 3; i++) { // shows 0, then 1, then 2 - alert(i); -} -``` - -Let's examine the `for` statement part by part: - -| part | | | -|-------|----------|----------------------------------------------------------------------------| -| begin | `i = 0` | Executes once upon entering the loop. | -| condition | `i < 3`| Checked before every loop iteration, if fails the loop stops. | -| step| `i++` | Executes after the body on each iteration, but before the condition check. | -| body | `alert(i)`| Runs again and again while the condition is truthy | - - -The general loop algorithm works like this: -``` -Run begin -→ (if condition → run body and run step) -→ (if condition → run body and run step) -→ (if condition → run body and run step) -→ ... -``` - -If you are new to loops, then maybe it would help if you go back to the example and reproduce how it runs step-by-step on a piece of paper. - -Here's what exactly happens in our case: - -```js -// for (let i = 0; i < 3; i++) alert(i) - -// run begin -let i = 0 -// if condition → run body and run step -if (i < 3) { alert(i); i++ } -// if condition → run body and run step -if (i < 3) { alert(i); i++ } -// if condition → run body and run step -if (i < 3) { alert(i); i++ } -// ...finish, because now i == 3 -``` - -````smart header="Inline variable declaration" -Here the "counter" variable `i` is declared right in the loop. That's called an "inline" variable declaration. Such variables are visible only inside the loop. - -```js run -for (*!*let*/!* i = 0; i < 3; i++) { - alert(i); // 0, 1, 2 -} -alert(i); // error, no such variable -``` - -Instead of defining a variable, we can use an existing one: - -```js run -let i = 0; - -for (i = 0; i < 3; i++) { // use an existing variable - alert(i); // 0, 1, 2 -} - -alert(i); // 3, visible, because declared outside of the loop -``` - -```` - - -### Skipping parts - -Any part of `for` can be skipped. - -For example, we can omit `begin` if we don't need to do anything at the loop start. - -Like here: - -```js run -let i = 0; // we have i already declared and assigned - -for (; i < 3; i++) { // no need for "begin" - alert( i ); // 0, 1, 2 -} -``` - -We can also remove the `step` part: - -```js run -let i = 0; - -for (; i < 3;) { - alert( i++ ); -} -``` - -The loop became identical to `while (i < 3)`. - -We can actually remove everything, thus creating an infinite loop: - -```js -for (;;) { - // repeats without limits -} -``` - -Please note that the two `for` semicolons `;` must be present, otherwise it would be a syntax error. - -## Breaking the loop - -Normally the loop exits when the condition becomes falsy. - -But we can force the exit at any moment. There's a special `break` directive for that. - -For example, the loop below asks the user for a series of numbers, but "breaks" when no number is entered: - -```js -let sum = 0; - -while (true) { - - let value = +prompt("Enter a number", ''); - -*!* - if (!value) break; // (*) -*/!* - - sum += value; - -} -alert( 'Sum: ' + sum ); -``` - -The `break` directive is activated at the line `(*)` if the user enters an empty line or cancels the input. It stops the loop immediately, passing the control to the first line after the loop. Namely, `alert`. - -The combination "infinite loop + `break` as needed" is great for situations when the condition must be checked not in the beginning/end of the loop, but in the middle, or even in several places of the body. - -## Continue to the next iteration [#continue] - -The `continue` directive is a "lighter version" of `break`. It doesn't stop the whole loop. Instead it stops the current iteration and forces the loop to start a new one (if the condition allows). - -We can use it if we're done on the current iteration and would like to move on to the next. - -The loop below uses `continue` to output only odd values: - -```js run no-beautify -for (let i = 0; i < 10; i++) { - - // if true, skip the remaining part of the body - *!*if (i % 2 == 0) continue;*/!* - - alert(i); // 1, then 3, 5, 7, 9 -} -``` - -For even values of `i` the `continue` directive stops body execution, passing the control to the next iteration of `for` (with the next number). So the `alert` is only called for odd values. - -````smart header="The directive `continue` helps to decrease nesting level" -A loop that shows odd values could look like this: - -```js -for (let i = 0; i < 10; i++) { - - if (i % 2) { - alert( i ); - } - -} -``` - -From a technical point of view it's identical to the example above. Surely, we can just wrap the code in the `if` block instead of `continue`. - -But as a side-effect we got one more nesting level (the `alert` call inside the curly braces). If the code inside `if` is longer than a few lines, that may decrease the overall readability. -```` - -````warn header="No `break/continue` to the right side of '?'" -Please note that syntax constructs that are not expressions cannot be used with the ternary operator `?`. In particular, directives such as `break/continue` are disallowed there. - -For example, if we take this code: - -```js -if (i > 5) { - alert(i); -} else { - continue; -} -``` - -...And rewrite it using a question mark: - - -```js no-beautify -(i > 5) ? alert(i) : *!*continue*/!*; // continue not allowed here -``` - -...Then it stops working. The code like this will give a syntax error: - - -That's just another reason not to use a question mark operator `?` instead of `if`. -```` - -## Labels for break/continue - -Sometimes we need to break out from multiple nested loops at once. - -For example, in the code below we loop over `i` and `j` prompting for coordinates `(i, j)` from `(0,0)` to `(3,3)`: - -```js run no-beautify -for (let i = 0; i < 3; i++) { - - for (let j = 0; j < 3; j++) { - - let input = prompt(`Value at coords (${i},${j})`, ''); - - // what if I want to exit from here to Done (below)? - - } -} - -alert('Done!'); -``` - -We need a way to stop the process if the user cancels the input. - -The ordinary `break` after `input` would only break the inner loop. That's not sufficient. Labels come to the rescue. - -A *label* is an identifier with a colon before a loop: -```js -labelName: for (...) { - ... -} -``` - -The `break ` statement in the loop breaks out to the label. - -Like here: - -```js run no-beautify -*!*outer:*/!* for (let i = 0; i < 3; i++) { - - for (let j = 0; j < 3; j++) { - - let input = prompt(`Value at coords (${i},${j})`, ''); - - // if an empty string or canceled, then break out of both loops - if (!input) *!*break outer*/!*; // (*) - - // do something with the value... - } -} -alert('Done!'); -``` - -In the code above `break outer` looks upwards for the label named `outer` and breaks out of that loop. - -So the control goes straight from `(*)` to `alert('Done!')`. - -We can also move the label onto a separate line: - -```js no-beautify -outer: -for (let i = 0; i < 3; i++) { ... } -``` - -The `continue` directive can also be used with a label. In this case the execution jumps to the next iteration of the labeled loop. - -````warn header="Labels are not a \"goto\"" -Labels do not allow us to jump into an arbitrary place of code. - -For example, it is impossible to do this: -```js -break label; // jumps to label? No. - -label: for (...) -``` - -The call to a `break/continue` is only possible from inside the loop, and the label must be somewhere upwards from the directive. -```` - -## Summary - -We covered 3 types of loops: - -- `while` -- The condition is checked before each iteration. -- `do..while` -- The condition is checked after each iteration. -- `for (;;)` -- The condition is checked before each iteration, additional settings available. - -To make an "infinite" loop, usually the `while(true)` construct is used. Such a loop, just like any other, can be stopped with the `break` directive. - -If we don't want to do anything on the current iteration and would like to forward to the next one, the `continue` directive does it. - -`break/continue` support labels before the loop. A label is the only way for `break/continue` to escape the nesting and go to the outer loop. diff --git a/1-js/02-first-steps/13-switch/article.md b/1-js/02-first-steps/13-switch/article.md deleted file mode 100644 index ae11492309..0000000000 --- a/1-js/02-first-steps/13-switch/article.md +++ /dev/null @@ -1,172 +0,0 @@ -# The "switch" statement - -A `switch` statement can replace multiple `if` checks. - -It gives a more descriptive way to compare a value with multiple variants. - -## The syntax - -The `switch` has one or more `case` blocks and an optional default. - -It looks like this: - -```js no-beautify -switch(x) { - case 'value1': // if (x === 'value1') - ... - [break] - - case 'value2': // if (x === 'value2') - ... - [break] - - default: - ... - [break] -} -``` - -- The value of `x` is checked for a strict equality to the value from the first `case` (that is, `value1`) then to the second (`value2`) and so on. -- If the equality is found, `switch` starts to execute the code starting from the corresponding `case`, until the nearest `break` (or until the end of `switch`). -- If no case is matched then the `default` code is executed (if it exists). - -## An example - -An example of `switch` (the executed code is highlighted): - -```js run -let a = 2 + 2; - -switch (a) { - case 3: - alert( 'Too small' ); - break; -*!* - case 4: - alert( 'Exactly!' ); - break; -*/!* - case 5: - alert( 'Too large' ); - break; - default: - alert( "I don't know such values" ); -} -``` - -Here the `switch` starts to compare `a` from the first `case` variant that is `3`. The match fails. - -Then `4`. That's a match, so the execution starts from `case 4` until the nearest `break`. - -**If there is no `break` then the execution continues with the next `case` without any checks.** - -An example without `break`: - -```js run -let a = 2 + 2; - -switch (a) { - case 3: - alert( 'Too small' ); -*!* - case 4: - alert( 'Exactly!' ); - case 5: - alert( 'Too big' ); - default: - alert( "I don't know such values" ); -*/!* -} -``` - -In the example above we'll see sequential execution of three `alert`s: - -```js -alert( 'Exactly!' ); -alert( 'Too big' ); -alert( "I don't know such values" ); -``` - -````smart header="Any expression can be a `switch/case` argument" -Both `switch` and `case` allow arbitrary expressions. - -For example: - -```js run -let a = "1"; -let b = 0; - -switch (+a) { -*!* - case b + 1: - alert("this runs, because +a is 1, exactly equals b+1"); - break; -*/!* - - default: - alert("this doesn't run"); -} -``` -Here `+a` gives `1`, that's compared with `b + 1` in `case`, and the corresponding code is executed. -```` - -## Grouping of "case" - -Several variants of `case` which share the same code can be grouped. - -For example, if we want the same code to run for `case 3` and `case 5`: - -```js run no-beautify -let a = 2 + 2; - -switch (a) { - case 4: - alert('Right!'); - break; - -*!* - case 3: // (*) grouped two cases - case 5: - alert('Wrong!'); - alert("Why don't you take a math class?"); - break; -*/!* - - default: - alert('The result is strange. Really.'); -} -``` - -Now both `3` and `5` show the same message. - -The ability to "group" cases is a side-effect of how `switch/case` works without `break`. Here the execution of `case 3` starts from the line `(*)` and goes through `case 5`, because there's no `break`. - -## Type matters - -Let's emphasize that the equality check is always strict. The values must be of the same type to match. - -For example, let's consider the code: - -```js run -let arg = prompt("Enter a value?") -switch (arg) { - case '0': - case '1': - alert( 'One or zero' ); - break; - - case '2': - alert( 'Two' ); - break; - - case 3: - alert( 'Never executes!' ); - break; - default: - alert( 'An unknown value' ) -} -``` - -1. For `0`, `1`, the first `alert` runs. -2. For `2` the second `alert` runs. -3. But for `3`, the result of the `prompt` is a string `"3"`, which is not strictly equal `===` to the number `3`. So we've got a dead code in `case 3`! The `default` variant will execute. diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/task.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/task.md diff --git a/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md new file mode 100644 index 0000000000..4953598769 --- /dev/null +++ b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md @@ -0,0 +1,30 @@ +The task demonstrates how postfix/prefix forms can lead to different results when used in comparisons. + +1. **From 1 to 4** + + ```js run + let i = 0; + while (++i < 5) alert( i ); + ``` + + The first value is `i = 1`, because `++i` first increments `i` and then returns the new value. So the first comparison is `1 < 5` and the `alert` shows `1`. + + Then follow `2, 3, 4…` -- the values show up one after another. The comparison always uses the incremented value, because `++` is before the variable. + + Finally, `i = 4` is incremented to `5`, the comparison `while(5 < 5)` fails, and the loop stops. So `5` is not shown. +2. **From 1 to 5** + + ```js run + let i = 0; + while (i++ < 5) alert( i ); + ``` + + The first value is again `i = 1`. The postfix form of `i++` increments `i` and then returns the *old* value, so the comparison `i++ < 5` will use `i = 0` (contrary to `++i < 5`). + + But the `alert` call is separate. It's another statement which executes after the increment and the comparison. So it gets the current `i = 1`. + + Then follow `2, 3, 4…` + + Let's stop on `i = 4`. The prefix form `++i` would increment it and use `5` in the comparison. But here we have the postfix form `i++`. So it increments `i` to `5`, but returns the old value. Hence the comparison is actually `while(4 < 5)` -- true, and the control goes on to `alert`. + + The value `i = 5` is the last one, because on the next step `while(5 < 5)` is false. diff --git a/1-js/02-first-steps/13-while-for/2-which-value-while/task.md b/1-js/02-first-steps/13-while-for/2-which-value-while/task.md new file mode 100644 index 0000000000..298213237b --- /dev/null +++ b/1-js/02-first-steps/13-while-for/2-which-value-while/task.md @@ -0,0 +1,22 @@ +importance: 4 + +--- + +# Which values does the while loop show? + +For every loop iteration, write down which value it outputs and then compare it with the solution. + +Both loops `alert` the same values, or not? + +1. The prefix form `++i`: + + ```js + let i = 0; + while (++i < 5) alert( i ); + ``` +2. The postfix form `i++` + + ```js + let i = 0; + while (i++ < 5) alert( i ); + ``` diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md b/1-js/02-first-steps/13-while-for/3-which-value-for/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/solution.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/solution.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/task.md b/1-js/02-first-steps/13-while-for/3-which-value-for/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/task.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/task.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/solution.md b/1-js/02-first-steps/13-while-for/4-for-even/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/solution.md rename to 1-js/02-first-steps/13-while-for/4-for-even/solution.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/task.md b/1-js/02-first-steps/13-while-for/4-for-even/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/task.md rename to 1-js/02-first-steps/13-while-for/4-for-even/task.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/task.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/task.md diff --git a/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md new file mode 100644 index 0000000000..c7de5f09b0 --- /dev/null +++ b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md @@ -0,0 +1,15 @@ + +```js run demo +let num; + +do { + num = prompt("Enter a number greater than 100?", 0); +} while (num <= 100 && num); +``` + +The loop `do..while` repeats while both checks are truthy: + +1. The check for `num <= 100` -- that is, the entered value is still not greater than `100`. +2. The check `&& num` is false when `num` is `null` or an empty string. Then the `while` loop stops too. + +P.S. If `num` is `null` then `num <= 100` is `true`, so without the 2nd check the loop wouldn't stop if the user clicks CANCEL. Both checks are required. diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md rename to 1-js/02-first-steps/13-while-for/6-repeat-until-correct/task.md diff --git a/1-js/02-first-steps/13-while-for/7-list-primes/solution.md b/1-js/02-first-steps/13-while-for/7-list-primes/solution.md new file mode 100644 index 0000000000..b4b64b6faa --- /dev/null +++ b/1-js/02-first-steps/13-while-for/7-list-primes/solution.md @@ -0,0 +1,29 @@ +There are many algorithms for this task. + +Let's use a nested loop: + +```js +For each i in the interval { + check if i has a divisor from 1..i + if yes => the value is not a prime + if no => the value is a prime, show it +} +``` + +The code using a label: + +```js run +let n = 10; + +nextPrime: +for (let i = 2; i <= n; i++) { // for each i... + + for (let j = 2; j < i; j++) { // look for a divisor.. + if (i % j == 0) continue nextPrime; // not a prime, go next i + } + + alert( i ); // a prime +} +``` + +There's a lot of space to optimize it. For instance, we could look for the divisors from `2` to square root of `i`. But anyway, if we want to be really efficient for large intervals, we need to change the approach and rely on advanced maths and complex algorithms like [Quadratic sieve](https://en.wikipedia.org/wiki/Quadratic_sieve), [General number field sieve](https://en.wikipedia.org/wiki/General_number_field_sieve) etc. diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/task.md b/1-js/02-first-steps/13-while-for/7-list-primes/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/7-list-primes/task.md rename to 1-js/02-first-steps/13-while-for/7-list-primes/task.md diff --git a/1-js/02-first-steps/13-while-for/article.md b/1-js/02-first-steps/13-while-for/article.md new file mode 100644 index 0000000000..d1b749888f --- /dev/null +++ b/1-js/02-first-steps/13-while-for/article.md @@ -0,0 +1,411 @@ +# Loops: while and for + +We often need to repeat actions. + +For example, outputting goods from a list one after another or just running the same code for each number from 1 to 10. + +*Loops* are a way to repeat the same code multiple times. + +```smart header="The for..of and for..in loops" +A small announcement for advanced readers. + +This article covers only basic loops: `while`, `do..while` and `for(..;..;..)`. + +If you came to this article searching for other types of loops, here are the pointers: + +- See [for..in](info:object#forin) to loop over object properties. +- See [for..of](info:array#loops) and [iterables](info:iterable) for looping over arrays and iterable objects. + +Otherwise, please read on. +``` + +## The "while" loop + +The `while` loop has the following syntax: + +```js +while (condition) { + // code + // so-called "loop body" +} +``` + +While the `condition` is truthy, the `code` from the loop body is executed. + +For instance, the loop below outputs `i` while `i < 3`: + +```js run +let i = 0; +while (i < 3) { // shows 0, then 1, then 2 + alert( i ); + i++; +} +``` + +A single execution of the loop body is called *an iteration*. The loop in the example above makes three iterations. + +If `i++` was missing from the example above, the loop would repeat (in theory) forever. In practice, the browser provides ways to stop such loops, and in server-side JavaScript, we can kill the process. + +Any expression or variable can be a loop condition, not just comparisons: the condition is evaluated and converted to a boolean by `while`. + +For instance, a shorter way to write `while (i != 0)` is `while (i)`: + +```js run +let i = 3; +*!* +while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops +*/!* + alert( i ); + i--; +} +``` + +````smart header="Curly braces are not required for a single-line body" +If the loop body has a single statement, we can omit the curly braces `{…}`: + +```js run +let i = 3; +*!* +while (i) alert(i--); +*/!* +``` +```` + +## The "do..while" loop + +The condition check can be moved *below* the loop body using the `do..while` syntax: + +```js +do { + // loop body +} while (condition); +``` + +The loop will first execute the body, then check the condition, and, while it's truthy, execute it again and again. + +For example: + +```js run +let i = 0; +do { + alert( i ); + i++; +} while (i < 3); +``` + +This form of syntax should only be used when you want the body of the loop to execute **at least once** regardless of the condition being truthy. Usually, the other form is preferred: `while(…) {…}`. + +## The "for" loop + +The `for` loop is more complex, but it's also the most commonly used loop. + +It looks like this: + +```js +for (begin; condition; step) { + // ... loop body ... +} +``` + +Let's learn the meaning of these parts by example. The loop below runs `alert(i)` for `i` from `0` up to (but not including) `3`: + +```js run +for (let i = 0; i < 3; i++) { // shows 0, then 1, then 2 + alert(i); +} +``` + +Let's examine the `for` statement part-by-part: + +| part | | | +|-------|----------|----------------------------------------------------------------------------| +| begin | `let i = 0` | Executes once upon entering the loop. | +| condition | `i < 3`| Checked before every loop iteration. If false, the loop stops. | +| body | `alert(i)`| Runs again and again while the condition is truthy. | +| step| `i++` | Executes after the body on each iteration. | + +The general loop algorithm works like this: + +``` +Run begin +→ (if condition → run body and run step) +→ (if condition → run body and run step) +→ (if condition → run body and run step) +→ ... +``` + +That is, `begin` executes once, and then it iterates: after each `condition` test, `body` and `step` are executed. + +If you are new to loops, it could help to go back to the example and reproduce how it runs step-by-step on a piece of paper. + +Here's exactly what happens in our case: + +```js +// for (let i = 0; i < 3; i++) alert(i) + +// run begin +let i = 0 +// if condition → run body and run step +if (i < 3) { alert(i); i++ } +// if condition → run body and run step +if (i < 3) { alert(i); i++ } +// if condition → run body and run step +if (i < 3) { alert(i); i++ } +// ...finish, because now i == 3 +``` + +````smart header="Inline variable declaration" +Here, the "counter" variable `i` is declared right in the loop. This is called an "inline" variable declaration. Such variables are visible only inside the loop. + +```js run +for (*!*let*/!* i = 0; i < 3; i++) { + alert(i); // 0, 1, 2 +} +alert(i); // error, no such variable +``` + +Instead of defining a variable, we could use an existing one: + +```js run +let i = 0; + +for (i = 0; i < 3; i++) { // use an existing variable + alert(i); // 0, 1, 2 +} + +alert(i); // 3, visible, because declared outside of the loop +``` +```` + +### Skipping parts + +Any part of `for` can be skipped. + +For example, we can omit `begin` if we don't need to do anything at the loop start. + +Like here: + +```js run +let i = 0; // we have i already declared and assigned + +for (; i < 3; i++) { // no need for "begin" + alert( i ); // 0, 1, 2 +} +``` + +We can also remove the `step` part: + +```js run +let i = 0; + +for (; i < 3;) { + alert( i++ ); +} +``` + +This makes the loop identical to `while (i < 3)`. + +We can actually remove everything, creating an infinite loop: + +```js +for (;;) { + // repeats without limits +} +``` + +Please note that the two `for` semicolons `;` must be present. Otherwise, there would be a syntax error. + +## Breaking the loop + +Normally, a loop exits when its condition becomes falsy. + +But we can force the exit at any time using the special `break` directive. + +For example, the loop below asks the user for a series of numbers, "breaking" when no number is entered: + +```js run +let sum = 0; + +while (true) { + + let value = +prompt("Enter a number", ''); + +*!* + if (!value) break; // (*) +*/!* + + sum += value; + +} +alert( 'Sum: ' + sum ); +``` + +The `break` directive is activated at the line `(*)` if the user enters an empty line or cancels the input. It stops the loop immediately, passing control to the first line after the loop. Namely, `alert`. + +The combination "infinite loop + `break` as needed" is great for situations when a loop's condition must be checked not in the beginning or end of the loop, but in the middle or even in several places of its body. + +## Continue to the next iteration [#continue] + +The `continue` directive is a "lighter version" of `break`. It doesn't stop the whole loop. Instead, it stops the current iteration and forces the loop to start a new one (if the condition allows). + +We can use it if we're done with the current iteration and would like to move on to the next one. + +The loop below uses `continue` to output only odd values: + +```js run no-beautify +for (let i = 0; i < 10; i++) { + + // if true, skip the remaining part of the body + *!*if (i % 2 == 0) continue;*/!* + + alert(i); // 1, then 3, 5, 7, 9 +} +``` + +For even values of `i`, the `continue` directive stops executing the body and passes control to the next iteration of `for` (with the next number). So the `alert` is only called for odd values. + +````smart header="The `continue` directive helps decrease nesting" +A loop that shows odd values could look like this: + +```js run +for (let i = 0; i < 10; i++) { + + if (i % 2) { + alert( i ); + } + +} +``` + +From a technical point of view, this is identical to the example above. Surely, we can just wrap the code in an `if` block instead of using `continue`. + +But as a side effect, this created one more level of nesting (the `alert` call inside the curly braces). If the code inside of `if` is longer than a few lines, that may decrease the overall readability. +```` + +````warn header="No `break/continue` to the right side of '?'" +Please note that syntax constructs that are not expressions cannot be used with the ternary operator `?`. In particular, directives such as `break/continue` aren't allowed there. + +For example, if we take this code: + +```js +if (i > 5) { + alert(i); +} else { + continue; +} +``` + +...and rewrite it using a question mark: + +```js no-beautify +(i > 5) ? alert(i) : *!*continue*/!*; // continue isn't allowed here +``` + +...it stops working: there's a syntax error. + +This is just another reason not to use the question mark operator `?` instead of `if`. +```` + +## Labels for break/continue + +Sometimes we need to break out from multiple nested loops at once. + +For example, in the code below we loop over `i` and `j`, prompting for the coordinates `(i, j)` from `(0,0)` to `(2,2)`: + +```js run no-beautify +for (let i = 0; i < 3; i++) { + + for (let j = 0; j < 3; j++) { + + let input = prompt(`Value at coords (${i},${j})`, ''); + + // what if we want to exit from here to Done (below)? + } +} + +alert('Done!'); +``` + +We need a way to stop the process if the user cancels the input. + +The ordinary `break` after `input` would only break the inner loop. That's not sufficient -- labels, come to the rescue! + +A *label* is an identifier with a colon before a loop: + +```js +labelName: for (...) { + ... +} +``` + +The `break ` statement in the loop below breaks out to the label: + +```js run no-beautify +*!*outer:*/!* for (let i = 0; i < 3; i++) { + + for (let j = 0; j < 3; j++) { + + let input = prompt(`Value at coords (${i},${j})`, ''); + + // if an empty string or canceled, then break out of both loops + if (!input) *!*break outer*/!*; // (*) + + // do something with the value... + } +} + +alert('Done!'); +``` + +In the code above, `break outer` looks upwards for the label named `outer` and breaks out of that loop. + +So the control goes straight from `(*)` to `alert('Done!')`. + +We can also move the label onto a separate line: + +```js no-beautify +outer: +for (let i = 0; i < 3; i++) { ... } +``` + +The `continue` directive can also be used with a label. In this case, code execution jumps to the next iteration of the labeled loop. + +````warn header="Labels do not allow to \"jump\" anywhere" +Labels do not allow us to jump into an arbitrary place in the code. + +For example, it is impossible to do this: + +```js +break label; // jump to the label below (doesn't work) + +label: for (...) +``` + +A `break` directive must be inside a code block. Technically, any labelled code block will do, e.g.: + +```js +label: { + // ... + break label; // works + // ... +} +``` + +...Although, 99.9% of the time `break` is used inside loops, as we've seen in the examples above. + +A `continue` is only possible from inside a loop. +```` + +## Summary + +We covered 3 types of loops: + +- `while` -- The condition is checked before each iteration. +- `do..while` -- The condition is checked after each iteration. +- `for (;;)` -- The condition is checked before each iteration, additional settings available. + +To make an "infinite" loop, usually the `while(true)` construct is used. Such a loop, just like any other, can be stopped with the `break` directive. + +If we don't want to do anything in the current iteration and would like to forward to the next one, we can use the `continue` directive. + +`break/continue` support labels before the loop. A label is the only way for `break/continue` to escape a nested loop to go to an outer one. diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md b/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md deleted file mode 100644 index e41c80418a..0000000000 --- a/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md +++ /dev/null @@ -1 +0,0 @@ -No difference. \ No newline at end of file diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md deleted file mode 100644 index c8ee9618fc..0000000000 --- a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md +++ /dev/null @@ -1,17 +0,0 @@ -Using a question mark operator `'?'`: - -```js -function checkAge(age) { - return (age > 18) ? true : confirm('Did parents allow you?'); -} -``` - -Using OR `||` (the shortest variant): - -```js -function checkAge(age) { - return (age > 18) || confirm('Did parents allow you?'); -} -``` - -Note that the parentheses around `age > 18` are not required here. They exist for better readabilty. diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md deleted file mode 100644 index 523bb127a8..0000000000 --- a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md +++ /dev/null @@ -1,26 +0,0 @@ -importance: 4 - ---- - -# Rewrite the function using '?' or '||' - -The following function returns `true` if the parameter `age` is greater than `18`. - -Otherwise it asks for a confirmation and returns its result. - -```js -function checkAge(age) { - if (age > 18) { - return true; - } else { - return confirm('Do you have your parents permission to access this page?'); - } -} -``` - -Rewrite it, to perform the same, but without `if`, in a single line. - -Make two variants of `checkAge`: - -1. Using a question mark operator `?` -2. Using OR `||` diff --git a/1-js/02-first-steps/14-function-basics/4-pow/solution.md b/1-js/02-first-steps/14-function-basics/4-pow/solution.md deleted file mode 100644 index 5ef20c3863..0000000000 --- a/1-js/02-first-steps/14-function-basics/4-pow/solution.md +++ /dev/null @@ -1,23 +0,0 @@ - -```js run demo -function pow(x, n) { - let result = x; - - for (let i = 1; i < n; i++) { - result *= x; - } - - return result; -} - -let x = prompt("x?", ''); -let n = prompt("n?", ''); - -if (n < 1) { - alert(`Power ${n} is not supported, - use an integer greater than 0`); -} else { - alert( pow(x, n) ); -} -``` - diff --git a/1-js/02-first-steps/14-function-basics/article.md b/1-js/02-first-steps/14-function-basics/article.md deleted file mode 100644 index 9f405128b8..0000000000 --- a/1-js/02-first-steps/14-function-basics/article.md +++ /dev/null @@ -1,455 +0,0 @@ -# Functions - -Quite often we need to perform a similar action in many places of the script. - -For example, we need to show a nice-looking message when a visitor logs in, logs out and maybe somewhere else. - -Functions are the main "building blocks" of the program. They allow the code to be called many times without repetition. - -We've already seen examples of built-in functions, like `alert(message)`, `prompt(message, default)` and `confirm(question)`. But we can create functions of our own as well. - -## Function Declaration - -To create a function we can use a *function declaration*. - -It looks like this: - -```js -function showMessage() { - alert( 'Hello everyone!' ); -} -``` - -The `function` keyword goes first, then goes the *name of the function*, then a list of *parameters* between the parentheses (empty in the example above) and finally the code of the function, also named "the function body", between curly braces. - -![](function_basics.png) - -Our new function can be called by its name: `showMessage()`. - -For instance: - -```js run -function showMessage() { - alert( 'Hello everyone!' ); -} - -*!* -showMessage(); -showMessage(); -*/!* -``` - -The call `showMessage()` executes the code of the function. Here we will see the message two times. - -This example clearly demonstrates one of the main purposes of functions: to avoid code duplication. - -If we ever need to change the message or the way it is shown, it's enough to modify the code in one place: the function which outputs it. - -## Local variables - -A variable declared inside a function is only visible inside that function. - -For example: - -```js run -function showMessage() { -*!* - let message = "Hello, I'm JavaScript!"; // local variable -*/!* - - alert( message ); -} - -showMessage(); // Hello, I'm JavaScript! - -alert( message ); // <-- Error! The variable is local to the function -``` - -## Outer variables - -A function can access an outer variable as well, for example: - -```js run no-beautify -let *!*userName*/!* = 'John'; - -function showMessage() { - let message = 'Hello, ' + *!*userName*/!*; - alert(message); -} - -showMessage(); // Hello, John -``` - -The function has full access to the outer variable. It can modify it as well. - -For instance: - -```js run -let *!*userName*/!* = 'John'; - -function showMessage() { - *!*userName*/!* = "Bob"; // (1) changed the outer variable - - let message = 'Hello, ' + *!*userName*/!*; - alert(message); -} - -alert( userName ); // *!*John*/!* before the function call - -showMessage(); - -alert( userName ); // *!*Bob*/!*, the value was modified by the function -``` - -The outer variable is only used if there's no local one. So an occasional modification may happen if we forget `let`. - -If a same-named variable is declared inside the function then it *shadows* the outer one. For instance, in the code below the function uses the local `userName`. The outer one is ignored: - -```js run -let userName = 'John'; - -function showMessage() { -*!* - let userName = "Bob"; // declare a local variable -*/!* - - let message = 'Hello, ' + userName; // *!*Bob*/!* - alert(message); -} - -// the function will create and use its own userName -showMessage(); - -alert( userName ); // *!*John*/!*, unchanged, the function did not access the outer variable -``` - -```smart header="Global variables" -Variables declared outside of any function, such as the outer `userName` in the code above, are called *global*. - -Global variables are visible from any function (unless shadowed by locals). - -Usually, a function declares all variables specific to its task. Global variables only store project-level data, so when it's important that these variables are accesible from anywhere. Modern code has few or no globals. Most variables reside in their functions. -``` - -## Parameters - -We can pass arbitrary data to functions using parameters (also called *function arguments*) . - -In the example below, the function has two parameters: `from` and `text`. - -```js run -function showMessage(*!*from, text*/!*) { // arguments: from, text - alert(from + ': ' + text); -} - -*!* -showMessage('Ann', 'Hello!'); // Ann: Hello! (*) -showMessage('Ann', "What's up?"); // Ann: What's up? (**) -*/!* -``` - -When the function is called in lines `(*)` and `(**)`, the given values are copied to local variables `from` and `text`. Then the function uses them. - -Here's one more example: we have a variable `from` and pass it to the function. Please note: the function changes `from`, but the change is not seen outside, because a function always gets a copy of the value: - - -```js run -function showMessage(from, text) { - -*!* - from = '*' + from + '*'; // make "from" look nicer -*/!* - - alert( from + ': ' + text ); -} - -let from = "Ann"; - -showMessage(from, "Hello"); // *Ann*: Hello - -// the value of "from" is the same, the function modified a local copy -alert( from ); // Ann -``` - -## Default values - -If a parameter is not provided, then its value becomes `undefined`. - -For instance, the aforementioned function `showMessage(from, text)` can be called with a single argument: - -```js -showMessage("Ann"); -``` - -That's not an error. Such a call would output `"Ann: undefined"`. There's no `text`, so it's assumed that `text === undefined`. - -If we want to use a "default" `text` in this case, then we can specify it after `=`: - -```js run -function showMessage(from, *!*text = "no text given"*/!*) { - alert( from + ": " + text ); -} - -showMessage("Ann"); // Ann: no text given -``` - -Now if the `text` parameter is not passed, it will get the value `"no text given"` - -Here `"no text given"` is a string, but it can be a more complex expression, which is only evaluated and assigned if the parameter is missing. So, this is also possible: - -```js run -function showMessage(from, text = anotherFunction()) { - // anotherFunction() only executed if no text given - // its result becomes the value of text -} -``` - - -````smart header="Default parameters old-style" -Old editions of JavaScript did not support default parameters. So there are alternative ways to support them, that you can find mostly in the old scripts. - -For instance, an explicit check for being `undefined`: - -```js -function showMessage(from, text) { -*!* - if (text === undefined) { - text = 'no text given'; - } -*/!* - - alert( from + ": " + text ); -} -``` - -...Or the `||` operator: - -```js -function showMessage(from, text) { - // if text is falsy then text gets the "default" value - text = text || 'no text given'; - ... -} -``` - - -```` - - -## Returning a value - -A function can return a value back into the calling code as the result. - -The simplest example would be a function that sums two values: - -```js run no-beautify -function sum(a, b) { - *!*return*/!* a + b; -} - -let result = sum(1, 2); -alert( result ); // 3 -``` - -The directive `return` can be in any place of the function. When the execution reaches it, the function stops, and the value is returned to the calling code (assigned to `result` above). - -There may be many occurrences of `return` in a single function. For instance: - -```js run -function checkAge(age) { - if (age > 18) { -*!* - return true; -*/!* - } else { -*!* - return confirm('Got a permission from the parents?'); -*/!* - } -} - -let age = prompt('How old are you?', 18); - -if ( checkAge(age) ) { - alert( 'Access granted' ); -} else { - alert( 'Access denied' ); -} -``` - -It is possible to use `return` without a value. That causes the function to exit immediately. - -For example: - -```js -function showMovie(age) { - if ( !checkAge(age) ) { -*!* - return; -*/!* - } - - alert( "Showing you the movie" ); // (*) - // ... -} -``` - -In the code above, if `checkAge(age)` returns `false`, then `showMovie` won't proceed to the `alert`. - -````smart header="A function with an empty `return` or without it returns `undefined`" -If a function does not return a value, it is the same as if it returns `undefined`: - -```js run -function doNothing() { /* empty */ } - -alert( doNothing() === undefined ); // true -``` - -An empty `return` is also the same as `return undefined`: - -```js run -function doNothing() { - return; -} - -alert( doNothing() === undefined ); // true -``` -```` - -````warn header="Never add a newline between `return` and the value" -For a long expression in `return`, it might be tempting to put it on a separate line, like this: - -```js -return - (some + long + expression + or + whatever * f(a) + f(b)) -``` -That doesn't work, because JavaScript assumes a semicolon after `return`. That'll work the same as: - -```js -return*!*;*/!* - (some + long + expression + or + whatever * f(a) + f(b)) -``` -So, it effectively becomes an empty return. We should put the value on the same line instead. -```` - -## Naming a function [#function-naming] - -Functions are actions. So their name is usually a verb. It should briefly, but as accurately as possible describe what the function does. So that a person who reads the code gets the right clue. - -It is a widespread practice to start a function with a verbal prefix which vaguely describes the action. There must be an agreement within the team on the meaning of the prefixes. - -For instance, functions that start with `"show"` usually show something. - -Function starting with... - -- `"get…"` -- return a value, -- `"calc…"` -- calculate something, -- `"create…"` -- create something, -- `"check…"` -- check something and return a boolean, etc. - -Examples of such names: - -```js no-beautify -showMessage(..) // shows a message -getAge(..) // returns the age (gets it somehow) -calcSum(..) // calculates a sum and returns the result -createForm(..) // creates a form (and usually returns it) -checkPermission(..) // checks a permission, returns true/false -``` - -With prefixes in place, a glance at a function name gives an understanding what kind of work it does and what kind of value it returns. - -```smart header="One function -- one action" -A function should do exactly what is suggested by its name, no more. - -Two independent actions usually deserve two functions, even if they are usually called together (in that case we can make a 3rd function that calls those two). - -A few examples of breaking this rule: - -- `getAge` -- would be bad if it shows an `alert` with the age (should only get). -- `createForm` -- would be bad if it modifies the document, adding a form to it (should only create it and return). -- `checkPermission` -- would be bad if displays the `access granted/denied` message (should only perform the check and return the result). - -These examples assume common meanings of prefixes. What they mean for you is determined by you and your team. Maybe it's pretty normal for your code to behave differently. But you should have a firm understanding of what a prefix means, what a prefixed function can and cannot do. All same-prefixed functions should obey the rules. And the team should share the knowledge. -``` - -```smart header="Ultrashort function names" -Functions that are used *very often* sometimes have ultrashort names. - -For example, the [jQuery](http://jquery.com) framework defines a function `$`. The [LoDash](http://lodash.com/) library has its core function named `_`. - -These are exceptions. Generally functions names should be concise, but descriptive. -``` - -## Functions == Comments - -Functions should be short and do exactly one thing. If that thing is big, maybe it's worth it to split the function into a few smaller functions. Sometimes following this rule may not be that easy, but it's definitely a good thing. - -A separate function is not only easier to test and debug -- its very existence is a great comment! - -For instance, compare the two functions `showPrimes(n)` below. Each one outputs [prime numbers](https://en.wikipedia.org/wiki/Prime_number) up to `n`. - -The first variant uses a label: - -```js -function showPrimes(n) { - nextPrime: for (let i = 2; i < n; i++) { - - for (let j = 2; j < i; j++) { - if (i % j == 0) continue nextPrime; - } - - alert( i ); // a prime - } -} -``` - -The second variant uses an additional function `isPrime(n)` to test for primality: - -```js -function showPrimes(n) { - - for (let i = 2; i < n; i++) { - *!*if (!isPrime(i)) continue;*/!* - - alert(i); // a prime - } -} - -function isPrime(n) { - for (let i = 2; i < n; i++) { - if ( n % i == 0) return false; - } - return true; -} -``` - -The second variant is easier to understand, isn't it? Instead of the code piece we see a name of the action (`isPrime`). Sometimes people refer to such code as *self-describing*. - -So, functions can be created even if we don't intend to reuse them. They structure the code and make it readable. - -## Summary - -A function declaration looks like this: - -```js -function name(parameters, delimited, by, comma) { - /* code */ -} -``` - -- Values passed to a function as parameters are copied to its local variables. -- A function may access outer variables. But it works only from inside out. The code outside of the function doesn't see its local variables. -- A function can return a value. If it doesn't, then its result is `undefined`. - -To make the code clean and easy to understand, it's recommended to use mainly local variables and parameters in the function, not outer variables. - -It is always easier to understand a function which gets parameters, works with them and returns a result than a function which gets no parameters, but modifies outer variables as a side-effect. - -Function naming: - -- A name should clearly describe what the function does. When we see a function call in the code, a good name instantly gives us an understanding what it does and returns. -- A function is an action, so function names are usually verbal. -- There exist many well-known function prefixes like `create…`, `show…`, `get…`, `check…` and so on. Use them to hint what a function does. - -Functions are the main building blocks of scripts. Now we've covered the basics, so we actually can start creating and using them. But that's only the beginning of the path. We are going to return to them many times, going more deeply into their advanced features. diff --git a/1-js/02-first-steps/14-function-basics/function_basics.png b/1-js/02-first-steps/14-function-basics/function_basics.png deleted file mode 100644 index 7eb6e11e54..0000000000 Binary files a/1-js/02-first-steps/14-function-basics/function_basics.png and /dev/null differ diff --git a/1-js/02-first-steps/14-function-basics/function_basics@2x.png b/1-js/02-first-steps/14-function-basics/function_basics@2x.png deleted file mode 100644 index e629bd712c..0000000000 Binary files a/1-js/02-first-steps/14-function-basics/function_basics@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md diff --git a/1-js/02-first-steps/14-switch/article.md b/1-js/02-first-steps/14-switch/article.md new file mode 100644 index 0000000000..d86babcec0 --- /dev/null +++ b/1-js/02-first-steps/14-switch/article.md @@ -0,0 +1,172 @@ +# The "switch" statement + +A `switch` statement can replace multiple `if` checks. + +It gives a more descriptive way to compare a value with multiple variants. + +## The syntax + +The `switch` has one or more `case` blocks and an optional default. + +It looks like this: + +```js no-beautify +switch(x) { + case 'value1': // if (x === 'value1') + ... + [break] + + case 'value2': // if (x === 'value2') + ... + [break] + + default: + ... + [break] +} +``` + +- The value of `x` is checked for a strict equality to the value from the first `case` (that is, `value1`) then to the second (`value2`) and so on. +- If the equality is found, `switch` starts to execute the code starting from the corresponding `case`, until the nearest `break` (or until the end of `switch`). +- If no case is matched then the `default` code is executed (if it exists). + +## An example + +An example of `switch` (the executed code is highlighted): + +```js run +let a = 2 + 2; + +switch (a) { + case 3: + alert( 'Too small' ); + break; +*!* + case 4: + alert( 'Exactly!' ); + break; +*/!* + case 5: + alert( 'Too big' ); + break; + default: + alert( "I don't know such values" ); +} +``` + +Here the `switch` starts to compare `a` from the first `case` variant that is `3`. The match fails. + +Then `4`. That's a match, so the execution starts from `case 4` until the nearest `break`. + +**If there is no `break` then the execution continues with the next `case` without any checks.** + +An example without `break`: + +```js run +let a = 2 + 2; + +switch (a) { + case 3: + alert( 'Too small' ); +*!* + case 4: + alert( 'Exactly!' ); + case 5: + alert( 'Too big' ); + default: + alert( "I don't know such values" ); +*/!* +} +``` + +In the example above we'll see sequential execution of three `alert`s: + +```js +alert( 'Exactly!' ); +alert( 'Too big' ); +alert( "I don't know such values" ); +``` + +````smart header="Any expression can be a `switch/case` argument" +Both `switch` and `case` allow arbitrary expressions. + +For example: + +```js run +let a = "1"; +let b = 0; + +switch (+a) { +*!* + case b + 1: + alert("this runs, because +a is 1, exactly equals b+1"); + break; +*/!* + + default: + alert("this doesn't run"); +} +``` +Here `+a` gives `1`, that's compared with `b + 1` in `case`, and the corresponding code is executed. +```` + +## Grouping of "case" + +Several variants of `case` which share the same code can be grouped. + +For example, if we want the same code to run for `case 3` and `case 5`: + +```js run no-beautify +let a = 3; + +switch (a) { + case 4: + alert('Right!'); + break; + +*!* + case 3: // (*) grouped two cases + case 5: + alert('Wrong!'); + alert("Why don't you take a math class?"); + break; +*/!* + + default: + alert('The result is strange. Really.'); +} +``` + +Now both `3` and `5` show the same message. + +The ability to "group" cases is a side effect of how `switch/case` works without `break`. Here the execution of `case 3` starts from the line `(*)` and goes through `case 5`, because there's no `break`. + +## Type matters + +Let's emphasize that the equality check is always strict. The values must be of the same type to match. + +For example, let's consider the code: + +```js run +let arg = prompt("Enter a value?"); +switch (arg) { + case '0': + case '1': + alert( 'One or zero' ); + break; + + case '2': + alert( 'Two' ); + break; + + case 3: + alert( 'Never executes!' ); + break; + default: + alert( 'An unknown value' ); +} +``` + +1. For `0`, `1`, the first `alert` runs. +2. For `2` the second `alert` runs. +3. But for `3`, the result of the `prompt` is a string `"3"`, which is not strictly equal `===` to the number `3`. So we've got a dead code in `case 3`! The `default` variant will execute. diff --git a/1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md b/1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md new file mode 100644 index 0000000000..e3a0df77c4 --- /dev/null +++ b/1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md @@ -0,0 +1,3 @@ +No difference! + +In both cases, `return confirm('Did parents allow you?')` executes exactly when the `if` condition is falsy. \ No newline at end of file diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/task.md b/1-js/02-first-steps/15-function-basics/1-if-else-required/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/1-if-else-required/task.md rename to 1-js/02-first-steps/15-function-basics/1-if-else-required/task.md diff --git a/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md new file mode 100644 index 0000000000..e48502642a --- /dev/null +++ b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md @@ -0,0 +1,17 @@ +Using a question mark operator `'?'`: + +```js +function checkAge(age) { + return (age > 18) ? true : confirm('Did parents allow you?'); +} +``` + +Using OR `||` (the shortest variant): + +```js +function checkAge(age) { + return (age > 18) || confirm('Did parents allow you?'); +} +``` + +Note that the parentheses around `age > 18` are not required here. They exist for better readability. diff --git a/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md new file mode 100644 index 0000000000..46da079c0d --- /dev/null +++ b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md @@ -0,0 +1,26 @@ +importance: 4 + +--- + +# Rewrite the function using '?' or '||' + +The following function returns `true` if the parameter `age` is greater than `18`. + +Otherwise it asks for a confirmation and returns its result. + +```js +function checkAge(age) { + if (age > 18) { + return true; + } else { + return confirm('Did parents allow you?'); + } +} +``` + +Rewrite it, to perform the same, but without `if`, in a single line. + +Make two variants of `checkAge`: + +1. Using a question mark operator `?` +2. Using OR `||` diff --git a/1-js/02-first-steps/14-function-basics/3-min/solution.md b/1-js/02-first-steps/15-function-basics/3-min/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/solution.md rename to 1-js/02-first-steps/15-function-basics/3-min/solution.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/task.md b/1-js/02-first-steps/15-function-basics/3-min/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/task.md rename to 1-js/02-first-steps/15-function-basics/3-min/task.md diff --git a/1-js/02-first-steps/15-function-basics/4-pow/solution.md b/1-js/02-first-steps/15-function-basics/4-pow/solution.md new file mode 100644 index 0000000000..19fe9011fc --- /dev/null +++ b/1-js/02-first-steps/15-function-basics/4-pow/solution.md @@ -0,0 +1,21 @@ + +```js run demo +function pow(x, n) { + let result = x; + + for (let i = 1; i < n; i++) { + result *= x; + } + + return result; +} + +let x = prompt("x?", ''); +let n = prompt("n?", ''); + +if (n < 1) { + alert(`Power ${n} is not supported, use a positive integer`); +} else { + alert( pow(x, n) ); +} +``` diff --git a/1-js/02-first-steps/14-function-basics/4-pow/task.md b/1-js/02-first-steps/15-function-basics/4-pow/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/4-pow/task.md rename to 1-js/02-first-steps/15-function-basics/4-pow/task.md diff --git a/1-js/02-first-steps/15-function-basics/article.md b/1-js/02-first-steps/15-function-basics/article.md new file mode 100644 index 0000000000..415fed3e0a --- /dev/null +++ b/1-js/02-first-steps/15-function-basics/article.md @@ -0,0 +1,539 @@ +# Functions + +Quite often we need to perform a similar action in many places of the script. + +For example, we need to show a nice-looking message when a visitor logs in, logs out and maybe somewhere else. + +Functions are the main "building blocks" of the program. They allow the code to be called many times without repetition. + +We've already seen examples of built-in functions, like `alert(message)`, `prompt(message, default)` and `confirm(question)`. But we can create functions of our own as well. + +## Function Declaration + +To create a function we can use a *function declaration*. + +It looks like this: + +```js +function showMessage() { + alert( 'Hello everyone!' ); +} +``` + +The `function` keyword goes first, then goes the *name of the function*, then a list of *parameters* between the parentheses (comma-separated, empty in the example above, we'll see examples later) and finally the code of the function, also named "the function body", between curly braces. + +```js +function name(parameter1, parameter2, ... parameterN) { + // body +} +``` + +Our new function can be called by its name: `showMessage()`. + +For instance: + +```js run +function showMessage() { + alert( 'Hello everyone!' ); +} + +*!* +showMessage(); +showMessage(); +*/!* +``` + +The call `showMessage()` executes the code of the function. Here we will see the message two times. + +This example clearly demonstrates one of the main purposes of functions: to avoid code duplication. + +If we ever need to change the message or the way it is shown, it's enough to modify the code in one place: the function which outputs it. + +## Local variables + +A variable declared inside a function is only visible inside that function. + +For example: + +```js run +function showMessage() { +*!* + let message = "Hello, I'm JavaScript!"; // local variable +*/!* + + alert( message ); +} + +showMessage(); // Hello, I'm JavaScript! + +alert( message ); // <-- Error! The variable is local to the function +``` + +## Outer variables + +A function can access an outer variable as well, for example: + +```js run no-beautify +let *!*userName*/!* = 'John'; + +function showMessage() { + let message = 'Hello, ' + *!*userName*/!*; + alert(message); +} + +showMessage(); // Hello, John +``` + +The function has full access to the outer variable. It can modify it as well. + +For instance: + +```js run +let *!*userName*/!* = 'John'; + +function showMessage() { + *!*userName*/!* = "Bob"; // (1) changed the outer variable + + let message = 'Hello, ' + *!*userName*/!*; + alert(message); +} + +alert( userName ); // *!*John*/!* before the function call + +showMessage(); + +alert( userName ); // *!*Bob*/!*, the value was modified by the function +``` + +The outer variable is only used if there's no local one. + +If a same-named variable is declared inside the function then it *shadows* the outer one. For instance, in the code below the function uses the local `userName`. The outer one is ignored: + +```js run +let userName = 'John'; + +function showMessage() { +*!* + let userName = "Bob"; // declare a local variable +*/!* + + let message = 'Hello, ' + userName; // *!*Bob*/!* + alert(message); +} + +// the function will create and use its own userName +showMessage(); + +alert( userName ); // *!*John*/!*, unchanged, the function did not access the outer variable +``` + +```smart header="Global variables" +Variables declared outside of any function, such as the outer `userName` in the code above, are called *global*. + +Global variables are visible from any function (unless shadowed by locals). + +It's a good practice to minimize the use of global variables. Modern code has few or no globals. Most variables reside in their functions. Sometimes though, they can be useful to store project-level data. +``` + +## Parameters + +We can pass arbitrary data to functions using parameters. + +In the example below, the function has two parameters: `from` and `text`. + +```js run +function showMessage(*!*from, text*/!*) { // parameters: from, text + alert(from + ': ' + text); +} + +*!*showMessage('Ann', 'Hello!');*/!* // Ann: Hello! (*) +*!*showMessage('Ann', "What's up?");*/!* // Ann: What's up? (**) +``` + +When the function is called in lines `(*)` and `(**)`, the given values are copied to local variables `from` and `text`. Then the function uses them. + +Here's one more example: we have a variable `from` and pass it to the function. Please note: the function changes `from`, but the change is not seen outside, because a function always gets a copy of the value: + +```js run +function showMessage(from, text) { + +*!* + from = '*' + from + '*'; // make "from" look nicer +*/!* + + alert( from + ': ' + text ); +} + +let from = "Ann"; + +showMessage(from, "Hello"); // *Ann*: Hello + +// the value of "from" is the same, the function modified a local copy +alert( from ); // Ann +``` + +When a value is passed as a function parameter, it's also called an *argument*. + +In other words, to put these terms straight: + +- A parameter is the variable listed inside the parentheses in the function declaration (it's a declaration time term). +- An argument is the value that is passed to the function when it is called (it's a call time term). + +We declare functions listing their parameters, then call them passing arguments. + +In the example above, one might say: "the function `showMessage` is declared with two parameters, then called with two arguments: `from` and `"Hello"`". + + +## Default values + +If a function is called, but an argument is not provided, then the corresponding value becomes `undefined`. + +For instance, the aforementioned function `showMessage(from, text)` can be called with a single argument: + +```js +showMessage("Ann"); +``` + +That's not an error. Such a call would output `"*Ann*: undefined"`. As the value for `text` isn't passed, it becomes `undefined`. + +We can specify the so-called "default" (to use if omitted) value for a parameter in the function declaration, using `=`: + +```js run +function showMessage(from, *!*text = "no text given"*/!*) { + alert( from + ": " + text ); +} + +showMessage("Ann"); // Ann: no text given +``` + +Now if the `text` parameter is not passed, it will get the value `"no text given"`. + +The default value also jumps in if the parameter exists, but strictly equals `undefined`, like this: + +```js +showMessage("Ann", undefined); // Ann: no text given +``` + +Here `"no text given"` is a string, but it can be a more complex expression, which is only evaluated and assigned if the parameter is missing. So, this is also possible: + +```js run +function showMessage(from, text = anotherFunction()) { + // anotherFunction() only executed if no text given + // its result becomes the value of text +} +``` + +```smart header="Evaluation of default parameters" +In JavaScript, a default parameter is evaluated every time the function is called without the respective parameter. + +In the example above, `anotherFunction()` isn't called at all, if the `text` parameter is provided. + +On the other hand, it's independently called every time when `text` is missing. +``` + +````smart header="Default parameters in old JavaScript code" +Several years ago, JavaScript didn't support the syntax for default parameters. So people used other ways to specify them. + +Nowadays, we can come across them in old scripts. + +For example, an explicit check for `undefined`: + +```js +function showMessage(from, text) { +*!* + if (text === undefined) { + text = 'no text given'; + } +*/!* + + alert( from + ": " + text ); +} +``` + +...Or using the `||` operator: + +```js +function showMessage(from, text) { + // If the value of text is falsy, assign the default value + // this assumes that text == "" is the same as no text at all + text = text || 'no text given'; + ... +} +``` +```` + + +### Alternative default parameters + +Sometimes it makes sense to assign default values for parameters at a later stage after the function declaration. + +We can check if the parameter is passed during the function execution, by comparing it with `undefined`: + +```js run +function showMessage(text) { + // ... + +*!* + if (text === undefined) { // if the parameter is missing + text = 'empty message'; + } +*/!* + + alert(text); +} + +showMessage(); // empty message +``` + +...Or we could use the `||` operator: + +```js +function showMessage(text) { + // if text is undefined or otherwise falsy, set it to 'empty' + text = text || 'empty'; + ... +} +``` + +Modern JavaScript engines support the [nullish coalescing operator](info:nullish-coalescing-operator) `??`, it's better when most falsy values, such as `0`, should be considered "normal": + +```js run +function showCount(count) { + // if count is undefined or null, show "unknown" + alert(count ?? "unknown"); +} + +showCount(0); // 0 +showCount(null); // unknown +showCount(); // unknown +``` + +## Returning a value + +A function can return a value back into the calling code as the result. + +The simplest example would be a function that sums two values: + +```js run no-beautify +function sum(a, b) { + *!*return*/!* a + b; +} + +let result = sum(1, 2); +alert( result ); // 3 +``` + +The directive `return` can be in any place of the function. When the execution reaches it, the function stops, and the value is returned to the calling code (assigned to `result` above). + +There may be many occurrences of `return` in a single function. For instance: + +```js run +function checkAge(age) { + if (age >= 18) { +*!* + return true; +*/!* + } else { +*!* + return confirm('Do you have permission from your parents?'); +*/!* + } +} + +let age = prompt('How old are you?', 18); + +if ( checkAge(age) ) { + alert( 'Access granted' ); +} else { + alert( 'Access denied' ); +} +``` + +It is possible to use `return` without a value. That causes the function to exit immediately. + +For example: + +```js +function showMovie(age) { + if ( !checkAge(age) ) { +*!* + return; +*/!* + } + + alert( "Showing you the movie" ); // (*) + // ... +} +``` + +In the code above, if `checkAge(age)` returns `false`, then `showMovie` won't proceed to the `alert`. + +````smart header="A function with an empty `return` or without it returns `undefined`" +If a function does not return a value, it is the same as if it returns `undefined`: + +```js run +function doNothing() { /* empty */ } + +alert( doNothing() === undefined ); // true +``` + +An empty `return` is also the same as `return undefined`: + +```js run +function doNothing() { + return; +} + +alert( doNothing() === undefined ); // true +``` +```` + +````warn header="Never add a newline between `return` and the value" +For a long expression in `return`, it might be tempting to put it on a separate line, like this: + +```js +return + (some + long + expression + or + whatever * f(a) + f(b)) +``` +That doesn't work, because JavaScript assumes a semicolon after `return`. That'll work the same as: + +```js +return*!*;*/!* + (some + long + expression + or + whatever * f(a) + f(b)) +``` + +So, it effectively becomes an empty return. + +If we want the returned expression to wrap across multiple lines, we should start it at the same line as `return`. Or at least put the opening parentheses there as follows: + +```js +return ( + some + long + expression + + or + + whatever * f(a) + f(b) + ) +``` +And it will work just as we expect it to. +```` + +## Naming a function [#function-naming] + +Functions are actions. So their name is usually a verb. It should be brief, as accurate as possible and describe what the function does, so that someone reading the code gets an indication of what the function does. + +It is a widespread practice to start a function with a verbal prefix which vaguely describes the action. There must be an agreement within the team on the meaning of the prefixes. + +For instance, functions that start with `"show"` usually show something. + +Function starting with... + +- `"get…"` -- return a value, +- `"calc…"` -- calculate something, +- `"create…"` -- create something, +- `"check…"` -- check something and return a boolean, etc. + +Examples of such names: + +```js no-beautify +showMessage(..) // shows a message +getAge(..) // returns the age (gets it somehow) +calcSum(..) // calculates a sum and returns the result +createForm(..) // creates a form (and usually returns it) +checkPermission(..) // checks a permission, returns true/false +``` + +With prefixes in place, a glance at a function name gives an understanding what kind of work it does and what kind of value it returns. + +```smart header="One function -- one action" +A function should do exactly what is suggested by its name, no more. + +Two independent actions usually deserve two functions, even if they are usually called together (in that case we can make a 3rd function that calls those two). + +A few examples of breaking this rule: + +- `getAge` -- would be bad if it shows an `alert` with the age (should only get). +- `createForm` -- would be bad if it modifies the document, adding a form to it (should only create it and return). +- `checkPermission` -- would be bad if it displays the `access granted/denied` message (should only perform the check and return the result). + +These examples assume common meanings of prefixes. You and your team are free to agree on other meanings, but usually they're not much different. In any case, you should have a firm understanding of what a prefix means, what a prefixed function can and cannot do. All same-prefixed functions should obey the rules. And the team should share the knowledge. +``` + +```smart header="Ultrashort function names" +Functions that are used *very often* sometimes have ultrashort names. + +For example, the [jQuery](https://jquery.com/) framework defines a function with `$`. The [Lodash](https://lodash.com/) library has its core function named `_`. + +These are exceptions. Generally function names should be concise and descriptive. +``` + +## Functions == Comments + +Functions should be short and do exactly one thing. If that thing is big, maybe it's worth it to split the function into a few smaller functions. Sometimes following this rule may not be that easy, but it's definitely a good thing. + +A separate function is not only easier to test and debug -- its very existence is a great comment! + +For instance, compare the two functions `showPrimes(n)` below. Each one outputs [prime numbers](https://en.wikipedia.org/wiki/Prime_number) up to `n`. + +The first variant uses a label: + +```js +function showPrimes(n) { + nextPrime: for (let i = 2; i < n; i++) { + + for (let j = 2; j < i; j++) { + if (i % j == 0) continue nextPrime; + } + + alert( i ); // a prime + } +} +``` + +The second variant uses an additional function `isPrime(n)` to test for primality: + +```js +function showPrimes(n) { + + for (let i = 2; i < n; i++) { + *!*if (!isPrime(i)) continue;*/!* + + alert(i); // a prime + } +} + +function isPrime(n) { + for (let i = 2; i < n; i++) { + if ( n % i == 0) return false; + } + return true; +} +``` + +The second variant is easier to understand, isn't it? Instead of the code piece we see a name of the action (`isPrime`). Sometimes people refer to such code as *self-describing*. + +So, functions can be created even if we don't intend to reuse them. They structure the code and make it readable. + +## Summary + +A function declaration looks like this: + +```js +function name(parameters, delimited, by, comma) { + /* code */ +} +``` + +- Values passed to a function as parameters are copied to its local variables. +- A function may access outer variables. But it works only from inside out. The code outside of the function doesn't see its local variables. +- A function can return a value. If it doesn't, then its result is `undefined`. + +To make the code clean and easy to understand, it's recommended to use mainly local variables and parameters in the function, not outer variables. + +It is always easier to understand a function which gets parameters, works with them and returns a result than a function which gets no parameters, but modifies outer variables as a side effect. + +Function naming: + +- A name should clearly describe what the function does. When we see a function call in the code, a good name instantly gives us an understanding what it does and returns. +- A function is an action, so function names are usually verbal. +- There exist many well-known function prefixes like `create…`, `show…`, `get…`, `check…` and so on. Use them to hint what a function does. + +Functions are the main building blocks of scripts. Now we've covered the basics, so we actually can start creating and using them. But that's only the beginning of the path. We are going to return to them many times, going more deeply into their advanced features. diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md b/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md deleted file mode 100644 index 3ea1124739..0000000000 --- a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md +++ /dev/null @@ -1,17 +0,0 @@ - -```js run -function ask(question, yes, no) { - if (confirm(question)) yes() - else no(); -} - -ask( - "Do you agree?", -*!* - () => alert("You agreed."), - () => alert("You canceled the execution.") -*/!* -); -``` - -Looks short and clean, right? diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md b/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md deleted file mode 100644 index a888ac157b..0000000000 --- a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md +++ /dev/null @@ -1,17 +0,0 @@ - -# Rewrite with arrow functions - -Replace Function Expressions with arrow functions in the code: - -```js run -function ask(question, yes, no) { - if (confirm(question)) yes() - else no(); -} - -ask( - "Do you agree?", - function() { alert("You agreed."); }, - function() { alert("You canceled the execution."); } -); -``` diff --git a/1-js/02-first-steps/15-function-expressions-arrows/article.md b/1-js/02-first-steps/15-function-expressions-arrows/article.md deleted file mode 100644 index 201a1ffe21..0000000000 --- a/1-js/02-first-steps/15-function-expressions-arrows/article.md +++ /dev/null @@ -1,478 +0,0 @@ -# Function expressions and arrows - -In JavaScript, a function is not a "magical language structure", but a special kind of value. - -The syntax that we used before is called a *Function Declaration*: - -```js -function sayHi() { - alert( "Hello" ); -} -``` - -There is another syntax for creating a function that is called a *Function Expression*. - -It looks like this: - -```js -let sayHi = function() { - alert( "Hello" ); -}; -``` - -Here, the function is created and assigned to the variable explicitly, like any other value. No matter how the function is defined, it's just a value stored in the variable `sayHi`. - - -The meaning of these code samples is the same: "create a function and put it into the variable `sayHi`". - -We can even print out that value using `alert`: - -```js run -function sayHi() { - alert( "Hello" ); -} - -*!* -alert( sayHi ); // shows the function code -*/!* -``` - -Please note that the last line does not run the function, because there are no parentheses after `sayHi`. There are programming languages where any mention of a function name causes its execution, but JavaScript is not like that. - -In JavaScript, a function is a value, so we can deal with it as a value. The code above shows its string representation, which is the source code. - -It is a special value of course, in the sense that we can call it like `sayHi()`. - -But it's still a value. So we can work with it like with other kinds of values. - -We can copy a function to another variable: - -```js run no-beautify -function sayHi() { // (1) create - alert( "Hello" ); -} - -let func = sayHi; // (2) copy - -func(); // Hello // (3) run the copy (it works)! -sayHi(); // Hello // this still works too (why wouldn't it) -``` - -Here's what happens above in detail: - -1. The Function Declaration `(1)` creates the function and puts it into the variable named `sayHi`. -2. Line `(2)` copies it into the variable `func`. - - Please note again: there are no parentheses after `sayHi`. If there were, then `func = sayHi()` would write *the result of the call* `sayHi()` into `func`, not *the function* `sayHi` itself. -3. Now the function can be called as both `sayHi()` and `func()`. - -Note that we could also have used a Function Expression to declare `sayHi`, in the first line: - -```js -let sayHi = function() { ... }; - -let func = sayHi; -// ... -``` - -Everything would work the same. Even more obvious what's going on, right? - - -````smart header="Why there's a semicolon at the end?" -There might be a question, why does Function Expression have a semicolon `;` at the end, and Function Declaration does not: - -```js -function sayHi() { - // ... -} - -let sayHi = function() { - // ... -}*!*;*/!* -``` - -The answer is simple: -- There's no need for `;` at the end of code blocks and syntax structures that use them like `if { ... }`, `for { }`, `function f { }` etc. -- A Function Expression is used inside the statement: `let sayHi = ...;`, as a value. It's not a code block. The semicolon `;` is recommended at the end of statements, no matter what is the value. So the semicolon here is not related to the Function Expression itself in any way, it just terminates the statement. -```` - -## Callback functions - -Let's look at more examples of passing functions as values and using function expressions. - -We'll write a function `ask(question, yes, no)` with three parameters: - -`question` -: Text of the question - -`yes` -: Function to run if the answer is "Yes" - -`no` -: Function to run if the answer is "No" - -The function should ask the `question` and, depending on the user's answer, call `yes()` or `no()`: - -```js run -*!* -function ask(question, yes, no) { - if (confirm(question)) yes() - else no(); -} -*/!* - -function showOk() { - alert( "You agreed." ); -} - -function showCancel() { - alert( "You canceled the execution." ); -} - -// usage: functions showOk, showCancel are passed as arguments to ask -ask("Do you agree?", showOk, showCancel); -``` - -Before we explore how we can write it in a much shorter way, let's note that in the browser (and on the server-side in some cases) such functions are quite popular. The major difference between a real-life implementation and the example above is that real-life functions use more complex ways to interact with the user than a simple `confirm`. In the browser, such a function usually draws a nice-looking question window. But that's another story. - -**The arguments of `ask` are called *callback functions* or just *callbacks*.** - -The idea is that we pass a function and expect it to be "called back" later if necessary. In our case, `showOk` becomes the callback for the "yes" answer, and `showCancel` for the "no" answer. - -We can use Function Expressions to write the same function much shorter: - -```js run no-beautify -function ask(question, yes, no) { - if (confirm(question)) yes() - else no(); -} - -*!* -ask( - "Do you agree?", - function() { alert("You agreed."); }, - function() { alert("You canceled the execution."); } -); -*/!* -``` - - -Here, functions are declared right inside the `ask(...)` call. They have no name, and so are called *anonymous*. Such functions are not accessible outside of `ask` (because they are not assigned to variables), but that's just what we want here. - -Such code appears in our scripts very naturally, it's in the spirit of JavaScript. - - -```smart header="A function is a value representing an \"action\"" -Regular values like strings or numbers represent the *data*. - -A function can be perceived as an *action*. - -We can pass it between variables and run when we want. -``` - - -## Function Expression vs Function Declaration - -Let's formulate the key differences between Function Declarations and Expressions. - -First, the syntax: how to see what is what in the code. - -- *Function Declaration:* a function, declared as a separate statement, in the main code flow. - - ```js - // Function Declaration - function sum(a, b) { - return a + b; - } - ``` -- *Function Expression:* a function, created inside an expression or inside another syntax construct. Here, the function is created at the right side of the "assignment expression" `=`: - - ```js - // Function Expression - let sum = function(a, b) { - return a + b; - }; - ``` - -The more subtle difference is *when* a function is created by the JavaScript engine. - -**A Function Expression is created when the execution reaches it and is usable from then on.** - -Once the execution flow passes to the right side of the assignment `let sum = function…` -- here we go, the function is created and can be used (assigned, called etc) from now on. - -Function Declarations are different. - -**A Function Declaration is usable in the whole script/code block.** - -In other words, when JavaScript *prepares* to run the script or a code block, it first looks for Function Declarations in it and creates the functions. We can think of it as an "initialization stage". - -And after all of the Function Declarations are processed, the execution goes on. - -As a result, a function declared as a Function Declaration can be called earlier than it is defined. - -For example, this works: - -```js run refresh untrusted -*!* -sayHi("John"); // Hello, John -*/!* - -function sayHi(name) { - alert( `Hello, ${name}` ); -} -``` - -The Function Declaration `sayHi` is created when JavaScript is preparing to start the script and is visible everywhere in it. - -...If it was a Function Expression, then it wouldn't work: - -```js run refresh untrusted -*!* -sayHi("John"); // error! -*/!* - -let sayHi = function(name) { // (*) no magic any more - alert( `Hello, ${name}` ); -}; -``` - -Function Expressions are created when the execution reaches them. That would happen only in the line `(*)`. Too late. - -**When a Function Declaration is made within a code block, it is visible everywhere inside that block. But not outside of it.** - -Sometimes that's handy to declare a local function only needed in that block alone. But that feature may also cause problems. - -For instance, let's imagine that we need to declare a function `welcome()` depending on the `age` variable that we get during runtime. And then we plan to use it some time later. - -The code below doesn't work: - -```js run -let age = prompt("What is your age?", 18); - -// conditionally declare a function -if (age < 18) { - - function welcome() { - alert("Hello!"); - } - -} else { - - function welcome() { - alert("Greetings!"); - } - -} - -// ...use it later -*!* -welcome(); // Error: welcome is not defined -*/!* -``` - -That's because a Function Declaration is only visible inside the code block in which it resides. - -Here's another example: - -```js run -let age = 16; // take 16 as an example - -if (age < 18) { -*!* - welcome(); // \ (runs) -*/!* - // | - function welcome() { // | - alert("Hello!"); // | Function Declaration is available - } // | everywhere in the block where it's declared - // | -*!* - welcome(); // / (runs) -*/!* - -} else { - - function welcome() { // for age = 16, this "welcome" is never created - alert("Greetings!"); - } -} - -// Here we're out of curly braces, -// so we can not see Function Declarations made inside of them. - -*!* -welcome(); // Error: welcome is not defined -*/!* -``` - -What can we do to make `welcome` visible outside of `if`? - -The correct approach would be to use a Function Expression and assign `welcome` to the variable that is declared outside of `if` and has the proper visibility. - -Now it works as intended: - -```js run -let age = prompt("What is your age?", 18); - -let welcome; - -if (age < 18) { - - welcome = function() { - alert("Hello!"); - }; - -} else { - - welcome = function() { - alert("Greetings!"); - }; - -} - -*!* -welcome(); // ok now -*/!* -``` - -Or we could simplify it even further using a question mark operator `?`: - -```js run -let age = prompt("What is your age?", 18); - -let welcome = (age < 18) ? - function() { alert("Hello!"); } : - function() { alert("Greetings!"); }; - -*!* -welcome(); // ok now -*/!* -``` - - -```smart header="When to choose Function Declaration versus Function Expression?" -As a rule of thumb, when we need to declare a function, the first to consider is Function Declaration syntax, the one we used before. It gives more freedom in how to organize our code, because we can call such functions before they are declared. - -It's also a little bit easier to look up `function f(…) {…}` in the code than `let f = function(…) {…}`. Function Declarations are more "eye-catching". - -...But if a Function Declaration does not suit us for some reason (we've seen an example above), then Function Expression should be used. -``` - - -## Arrow functions [#arrow-functions] - -There's one more very simple and concise syntax for creating functions, that's often better than Function Expressions. It's called "arrow functions", because it looks like this: - - -```js -let func = (arg1, arg2, ...argN) => expression -``` - -...This creates a function `func` that has arguments `arg1..argN`, evaluates the `expression` on the right side with their use and returns its result. - -In other words, it's roughly the same as: - -```js -let func = function(arg1, arg2, ...argN) { - return expression; -} -``` - -...But much more concise. - -Let's see an example: - -```js run -let sum = (a, b) => a + b; - -/* The arrow function is a shorter form of: - -let sum = function(a, b) { - return a + b; -}; -*/ - -alert( sum(1, 2) ); // 3 - -``` - -If we have only one argument, then parentheses can be omitted, making that even shorter: - -```js run -// same as -// let double = function(n) { return n * 2 } -*!* -let double = n => n * 2; -*/!* - -alert( double(3) ); // 6 -``` - -If there are no arguments, parentheses should be empty (but they should be present): - -```js run -let sayHi = () => alert("Hello!"); - -sayHi(); -``` - -Arrow functions can be used in the same way as Function Expressions. - -For instance, here's the rewritten example with `welcome()`: - -```js run -let age = prompt("What is your age?", 18); - -let welcome = (age < 18) ? - () => alert('Hello') : - () => alert("Greetings!"); - -welcome(); // ok now -``` - -Arrow functions may appear unfamiliar and not very readable at first, but that quickly changes as the eyes get used to the structure. - -They are very convenient for simple one-line actions, when we're just too lazy to write many words. - -```smart header="Multiline arrow functions" - -The examples above took arguments from the left of `=>` and evaluated the right-side expression with them. - -Sometimes we need something a little bit more complex, like multiple expressions or statements. It is also possible, but we should enclose them in curly braces. Then use a normal `return` within them. - -Like this: - -```js run -let sum = (a, b) => { // the curly brace opens a multiline function - let result = a + b; -*!* - return result; // if we use curly braces, use return to get results -*/!* -}; - -alert( sum(1, 2) ); // 3 -``` - -```smart header="More to come" -Here we praised arrow functions for brevity. But that's not all! Arrow functions have other interesting features. We'll return to them later in the chapter . - -For now, we can already use them for one-line actions and callbacks. -``` - -## Summary - -- Functions are values. They can be assigned, copied or declared in any place of the code. -- If the function is declared as a separate statement in the main code flow, that's called a "Function Declaration". -- If the function is created as a part of an expression, it's called a "Function Expression". -- Function Declarations are processed before the code block is executed. They are visible everywhere in the block. -- Function Expressions are created when the execution flow reaches them. - - -In most cases when we need to declare a function, a Function Declaration is preferable, because it is visible prior to the declaration itself. That gives us more flexibility in code organization, and is usually more readable. - -So we should use a Function Expression only when a Function Declaration is not fit for the task. We've seen a couple of examples of that in this chapter, and will see more in the future. - -Arrow functions are handy for one-liners. They come in two flavors: - -1. Without curly braces: `(...args) => expression` -- the right side is an expression: the function evaluates it and returns the result. -2. With curly braces: `(...args) => { body }` -- brackets allow us to write multiple statements inside the function, but we need an explicit `return` to return something. diff --git a/1-js/02-first-steps/16-function-expressions/article.md b/1-js/02-first-steps/16-function-expressions/article.md new file mode 100644 index 0000000000..b952d59435 --- /dev/null +++ b/1-js/02-first-steps/16-function-expressions/article.md @@ -0,0 +1,380 @@ +# Function expressions + +In JavaScript, a function is not a "magical language structure", but a special kind of value. + +The syntax that we used before is called a *Function Declaration*: + +```js +function sayHi() { + alert( "Hello" ); +} +``` + +There is another syntax for creating a function that is called a *Function Expression*. + +It allows us to create a new function in the middle of any expression. + +For example: + +```js +let sayHi = function() { + alert( "Hello" ); +}; +``` + +Here we can see a variable `sayHi` getting a value, the new function, created as `function() { alert("Hello"); }`. + +As the function creation happens in the context of the assignment expression (to the right side of `=`), this is a *Function Expression*. + +Please note, there's no name after the `function` keyword. Omitting a name is allowed for Function Expressions. + +Here we immediately assign it to the variable, so the meaning of these code samples is the same: "create a function and put it into the variable `sayHi`". + +In more advanced situations, that we'll come across later, a function may be created and immediately called or scheduled for a later execution, not stored anywhere, thus remaining anonymous. + +## Function is a value + +Let's reiterate: no matter how the function is created, a function is a value. Both examples above store a function in the `sayHi` variable. + +We can even print out that value using `alert`: + +```js run +function sayHi() { + alert( "Hello" ); +} + +*!* +alert( sayHi ); // shows the function code +*/!* +``` + +Please note that the last line does not run the function, because there are no parentheses after `sayHi`. There are programming languages where any mention of a function name causes its execution, but JavaScript is not like that. + +In JavaScript, a function is a value, so we can deal with it as a value. The code above shows its string representation, which is the source code. + +Surely, a function is a special value, in the sense that we can call it like `sayHi()`. + +But it's still a value. So we can work with it like with other kinds of values. + +We can copy a function to another variable: + +```js run no-beautify +function sayHi() { // (1) create + alert( "Hello" ); +} + +let func = sayHi; // (2) copy + +func(); // Hello // (3) run the copy (it works)! +sayHi(); // Hello // this still works too (why wouldn't it) +``` + +Here's what happens above in detail: + +1. The Function Declaration `(1)` creates the function and puts it into the variable named `sayHi`. +2. Line `(2)` copies it into the variable `func`. Please note again: there are no parentheses after `sayHi`. If there were, then `func = sayHi()` would write *the result of the call* `sayHi()` into `func`, not *the function* `sayHi` itself. +3. Now the function can be called as both `sayHi()` and `func()`. + +We could also have used a Function Expression to declare `sayHi`, in the first line: + +```js +let sayHi = function() { // (1) create + alert( "Hello" ); +}; + +let func = sayHi; +// ... +``` + +Everything would work the same. + + +````smart header="Why is there a semicolon at the end?" +You might wonder, why do Function Expressions have a semicolon `;` at the end, but Function Declarations do not: + +```js +function sayHi() { + // ... +} + +let sayHi = function() { + // ... +}*!*;*/!* +``` + +The answer is simple: a Function Expression is created here as `function(…) {…}` inside the assignment statement: `let sayHi = …;`. The semicolon `;` is recommended at the end of the statement, it's not a part of the function syntax. + +The semicolon would be there for a simpler assignment, such as `let sayHi = 5;`, and it's also there for a function assignment. +```` + +## Callback functions + +Let's look at more examples of passing functions as values and using function expressions. + +We'll write a function `ask(question, yes, no)` with three parameters: + +`question` +: Text of the question + +`yes` +: Function to run if the answer is "Yes" + +`no` +: Function to run if the answer is "No" + +The function should ask the `question` and, depending on the user's answer, call `yes()` or `no()`: + +```js run +*!* +function ask(question, yes, no) { + if (confirm(question)) yes() + else no(); +} +*/!* + +function showOk() { + alert( "You agreed." ); +} + +function showCancel() { + alert( "You canceled the execution." ); +} + +// usage: functions showOk, showCancel are passed as arguments to ask +ask("Do you agree?", showOk, showCancel); +``` + +In practice, such functions are quite useful. The major difference between a real-life `ask` and the example above is that real-life functions use more complex ways to interact with the user than a simple `confirm`. In the browser, such functions usually draw a nice-looking question window. But that's another story. + +**The arguments `showOk` and `showCancel` of `ask` are called *callback functions* or just *callbacks*.** + +The idea is that we pass a function and expect it to be "called back" later if necessary. In our case, `showOk` becomes the callback for "yes" answer, and `showCancel` for "no" answer. + +We can use Function Expressions to write an equivalent, shorter function: + +```js run no-beautify +function ask(question, yes, no) { + if (confirm(question)) yes() + else no(); +} + +*!* +ask( + "Do you agree?", + function() { alert("You agreed."); }, + function() { alert("You canceled the execution."); } +); +*/!* +``` + +Here, functions are declared right inside the `ask(...)` call. They have no name, and so are called *anonymous*. Such functions are not accessible outside of `ask` (because they are not assigned to variables), but that's just what we want here. + +Such code appears in our scripts very naturally, it's in the spirit of JavaScript. + +```smart header="A function is a value representing an \"action\"" +Regular values like strings or numbers represent the *data*. + +A function can be perceived as an *action*. + +We can pass it between variables and run when we want. +``` + + +## Function Expression vs Function Declaration + +Let's formulate the key differences between Function Declarations and Expressions. + +First, the syntax: how to differentiate between them in the code. + +- *Function Declaration:* a function, declared as a separate statement, in the main code flow: + + ```js + // Function Declaration + function sum(a, b) { + return a + b; + } + ``` +- *Function Expression:* a function, created inside an expression or inside another syntax construct. Here, the function is created on the right side of the "assignment expression" `=`: + + ```js + // Function Expression + let sum = function(a, b) { + return a + b; + }; + ``` + +The more subtle difference is *when* a function is created by the JavaScript engine. + +**A Function Expression is created when the execution reaches it and is usable only from that moment.** + +Once the execution flow passes to the right side of the assignment `let sum = function…` -- here we go, the function is created and can be used (assigned, called, etc. ) from now on. + +Function Declarations are different. + +**A Function Declaration can be called earlier than it is defined.** + +For example, a global Function Declaration is visible in the whole script, no matter where it is. + +That's due to internal algorithms. When JavaScript prepares to run the script, it first looks for global Function Declarations in it and creates the functions. We can think of it as an "initialization stage". + +And after all Function Declarations are processed, the code is executed. So it has access to these functions. + +For example, this works: + +```js run refresh untrusted +*!* +sayHi("John"); // Hello, John +*/!* + +function sayHi(name) { + alert( `Hello, ${name}` ); +} +``` + +The Function Declaration `sayHi` is created when JavaScript is preparing to start the script and is visible everywhere in it. + +...If it were a Function Expression, then it wouldn't work: + +```js run refresh untrusted +*!* +sayHi("John"); // error! +*/!* + +let sayHi = function(name) { // (*) no magic any more + alert( `Hello, ${name}` ); +}; +``` + +Function Expressions are created when the execution reaches them. That would happen only in the line `(*)`. Too late. + +Another special feature of Function Declarations is their block scope. + +**In strict mode, when a Function Declaration is within a code block, it's visible everywhere inside that block. But not outside of it.** + +For instance, let's imagine that we need to declare a function `welcome()` depending on the `age` variable that we get during runtime. And then we plan to use it some time later. + +If we use Function Declaration, it won't work as intended: + +```js run +let age = prompt("What is your age?", 18); + +// conditionally declare a function +if (age < 18) { + + function welcome() { + alert("Hello!"); + } + +} else { + + function welcome() { + alert("Greetings!"); + } + +} + +// ...use it later +*!* +welcome(); // Error: welcome is not defined +*/!* +``` + +That's because a Function Declaration is only visible inside the code block in which it resides. + +Here's another example: + +```js run +let age = 16; // take 16 as an example + +if (age < 18) { +*!* + welcome(); // \ (runs) +*/!* + // | + function welcome() { // | + alert("Hello!"); // | Function Declaration is available + } // | everywhere in the block where it's declared + // | +*!* + welcome(); // / (runs) +*/!* + +} else { + + function welcome() { + alert("Greetings!"); + } +} + +// Here we're out of curly braces, +// so we can not see Function Declarations made inside of them. + +*!* +welcome(); // Error: welcome is not defined +*/!* +``` + +What can we do to make `welcome` visible outside of `if`? + +The correct approach would be to use a Function Expression and assign `welcome` to the variable that is declared outside of `if` and has the proper visibility. + +This code works as intended: + +```js run +let age = prompt("What is your age?", 18); + +let welcome; + +if (age < 18) { + + welcome = function() { + alert("Hello!"); + }; + +} else { + + welcome = function() { + alert("Greetings!"); + }; + +} + +*!* +welcome(); // ok now +*/!* +``` + +Or we could simplify it even further using a question mark operator `?`: + +```js run +let age = prompt("What is your age?", 18); + +let welcome = (age < 18) ? + function() { alert("Hello!"); } : + function() { alert("Greetings!"); }; + +*!* +welcome(); // ok now +*/!* +``` + + +```smart header="When to choose Function Declaration versus Function Expression?" +As a rule of thumb, when we need to declare a function, the first thing to consider is Function Declaration syntax. It gives more freedom in how to organize our code, because we can call such functions before they are declared. + +That's also better for readability, as it's easier to look up `function f(…) {…}` in the code than `let f = function(…) {…};`. Function Declarations are more "eye-catching". + +...But if a Function Declaration does not suit us for some reason, or we need a conditional declaration (we've just seen an example), then Function Expression should be used. +``` + +## Summary + +- Functions are values. They can be assigned, copied or declared in any place of the code. +- If the function is declared as a separate statement in the main code flow, that's called a "Function Declaration". +- If the function is created as a part of an expression, it's called a "Function Expression". +- Function Declarations are processed before the code block is executed. They are visible everywhere in the block. +- Function Expressions are created when the execution flow reaches them. + +In most cases when we need to declare a function, a Function Declaration is preferable, because it is visible prior to the declaration itself. That gives us more flexibility in code organization, and is usually more readable. + +So we should use a Function Expression only when a Function Declaration is not fit for the task. We've seen a couple of examples of that in this chapter, and will see more in the future. diff --git a/1-js/02-first-steps/16-javascript-specials/article.md b/1-js/02-first-steps/16-javascript-specials/article.md deleted file mode 100644 index 525e72d4c8..0000000000 --- a/1-js/02-first-steps/16-javascript-specials/article.md +++ /dev/null @@ -1,287 +0,0 @@ -# JavaScript specials - -This chapter briefly recaps the features of JavaScript that we've learned by now, paying special attention to subtle moments. - -## Code structure - -Statements are delimited with a semicolon: - -```js run no-beautify -alert('Hello'); alert('World'); -``` - -Usually, a line-break is also treated as a delimiter, so that would also work: - -```js run no-beautify -alert('Hello') -alert('World') -``` - -That's called "automatic semicolon insertion". Sometimes it doesn't work, for instance: - -```js run -alert("There will be an error after this message") - -[1, 2].forEach(alert) -``` - -Most codestyle guides agree that we should put a semicolon after each statement. - -Semicolons are not required after code blocks `{...}` and syntax constructs with them like loops: - -```js -function f() { - // no semicolon needed after function declaration -} - -for(;;) { - // no semicolon needed after the loop -} -``` - -...But even if we can put an "extra" semicolon somewhere, that's not an error. It will be ignored. - -More in: . - -## Strict mode - -To fully enable all features of modern JavaScript, we should start scripts with `"use strict"`. - -```js -'use strict'; - -... -``` - -The directive must be at the top of a script or at the beginning of a function. - -Without `"use strict"`, everything still works, but some features behave in the old-fashion, "compatible" way. We'd generally prefer the modern behavior. - -Some modern features of the language (like classes that we'll study in the future) enable strict mode implicitly. - -More in: . - -## Variables - -Can be declared using: - -- `let` -- `const` (constant, can't be changed) -- `var` (old-style, will see later) - -A variable name can include: -- Letters and digits, but the first character may not be a digit. -- Characters `$` and `_` are normal, on par with letters. -- Non-Latin alphabets and hieroglyphs are also allowed, but commonly not used. - -Variables are dynamically typed. They can store any value: - -```js -let x = 5; -x = "John"; -``` - -There are 7 data types: - -- `number` for both floating-point and integer numbers, -- `string` for strings, -- `boolean` for logical values: `true/false`, -- `null` -- a type with a single value `null`, meaning "empty" or "does not exist", -- `undefined` -- a type with a single value `undefined`, meaning "not assigned", -- `object` and `symbol` -- for complex data structures and unique identifiers, we haven't learnt them yet. - -The `typeof` operator returns the type for a value, with two exceptions: -```js -typeof null == "object" // error in the language -typeof function(){} == "function" // functions are treated specially -``` - -More in: and . - -## Interaction - -We're using a browser as a working environment, so basic UI functions will be: - -[`prompt(question[, default])`](mdn:api/Window/prompt) -: Ask a `question`, and return either what the visitor entered or `null` if they pressed "cancel". - -[`confirm(question)`](mdn:api/Window/confirm) -: Ask a `question` and suggest to choose between Ok and Cancel. The choice is returned as `true/false`. - -[`alert(message)`](mdn:api/Window/alert) -: Output a `message`. - -All these functions are *modal*, they pause the code execution and prevent the visitor from interacting with the page until they answer. - -For instance: - -```js run -let userName = prompt("Your name?", "Alice"); -let isTeaWanted = confirm("Do you want some tea?"); - -alert( "Visitor: " + userName ); // Alice -alert( "Tea wanted: " + isTeaWanted ); // true -``` - -More in: . - -## Operators - -JavaScript supports the following operators: - -Arithmetical -: Regular: `* + - /`, also `%` for the remainder and `**` for power of a number. - - The binary plus `+` concatenates strings. And if any of the operands is a string, the other one is converted to string too: - - ```js run - alert( '1' + 2 ); // '12', string - alert( 1 + '2' ); // '12', string - ``` - -Assignments -: There is a simple assignment: `a = b` and combined ones like `a *= 2`. - -Bitwise -: Bitwise operators work with integers on bit-level: see the [docs](mdn:/JavaScript/Reference/Operators/Bitwise_Operators) when they are needed. - -Ternary -: The only operator with three parameters: `cond ? resultA : resultB`. If `cond` is truthy, returns `resultA`, otherwise `resultB`. - -Logical operators -: Logical AND `&&` and OR `||` perform short-circuit evaluation and then return the value where it stopped. - -Comparisons -: Equality check `==` for values of different types converts them to a number (except `null` and `undefined` that equal each other and nothing else), so these are equal: - - ```js run - alert( 0 == false ); // true - alert( 0 == '' ); // true - ``` - - Other comparisons convert to a number as well. - - The strict equality operator `===` doesn't do the conversion: different types always mean different values for it, so: - - Values `null` and `undefined` are special: they equal `==` each other and don't equal anything else. - - Greater/less comparisons compare strings character-by-character, other types are converted to a number. - -Logical operators -: There are few others, like a comma operator. - -More in: , , . - -## Loops - -- We covered 3 types of loops: - - ```js - // 1 - while (condition) { - ... - } - - // 2 - do { - ... - } while (condition); - - // 3 - for(let i = 0; i < 10; i++) { - ... - } - ``` - -- The variable declared in `for(let...)` loop is visible only inside the loop. But we can also omit `let` and reuse an existing variable. -- Directives `break/continue` allow to exit the whole loop/current iteration. Use labels to break nested loops. - -Details in: . - -Later we'll study more types of loops to deal with objects. - -## The "switch" construct - -The "switch" construct can replace multiple `if` checks. It uses `===` (strict equality) for comparisons. - -For instance: - -```js run -let age = prompt('Your age?', 18); - -switch (age) { - case 18: - alert("Won't work"); // the result of prompt is a string, not a number - - case "18": - alert("This works!"); - break; - - default: - alert("Any value not equal to one above"); -} -``` - -Details in: . - -## Functions - -We covered three ways to create a function in JavaScript: - -1. Function Declaration: the function in the main code flow - - ```js - function sum(a, b) { - let result = a + b; - - return result; - } - ``` - -2. Function Expression: the function in the context of an expression - - ```js - let sum = function(a, b) { - let result = a + b; - - return result; - } - ``` - - Function expressions can have a name, like `sum = function name(a, b)`, but that `name` is only visible inside that function. - -3. Arrow functions: - - ```js - // expression at the right side - let sum = (a, b) => a + b; - - // or multi-line syntax with { ... }, need return here: - let sum = (a, b) => { - // ... - return a + b; - } - - // without arguments - let sayHi = () => alert("Hello"); - - // with a single argument - let double = n => n * 2; - ``` - - -- Functions may have local variables: those declared inside its body. Such variables are only visible inside the function. -- Parameters can have default values: `function sum(a = 1, b = 2) {...}`. -- Functions always return something. If there's no `return` statement, then the result is `undefined`. - - -| Function Declaration | Function Expression | -|----------------------|---------------------| -| visible in the whole code block | created when the execution reaches it | -| - | can have a name, visible only inside the function | - -More: see , . - -## More to come - -That was a brief list of JavaScript features. As of now we've studied only basics. Further in the tutorial you'll find more specials and advanced features of JavaScript. diff --git a/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md new file mode 100644 index 0000000000..041db18bc6 --- /dev/null +++ b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md @@ -0,0 +1,17 @@ + +```js run +function ask(question, yes, no) { + if (confirm(question)) yes(); + else no(); +} + +ask( + "Do you agree?", +*!* + () => alert("You agreed."), + () => alert("You canceled the execution.") +*/!* +); +``` + +Looks short and clean, right? diff --git a/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md new file mode 100644 index 0000000000..e18c08a83e --- /dev/null +++ b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md @@ -0,0 +1,17 @@ + +# Rewrite with arrow functions + +Replace Function Expressions with arrow functions in the code below: + +```js run +function ask(question, yes, no) { + if (confirm(question)) yes(); + else no(); +} + +ask( + "Do you agree?", + function() { alert("You agreed."); }, + function() { alert("You canceled the execution."); } +); +``` diff --git a/1-js/02-first-steps/17-arrow-functions-basics/article.md b/1-js/02-first-steps/17-arrow-functions-basics/article.md new file mode 100644 index 0000000000..50c0d475da --- /dev/null +++ b/1-js/02-first-steps/17-arrow-functions-basics/article.md @@ -0,0 +1,111 @@ +# Arrow functions, the basics + +There's another very simple and concise syntax for creating functions, that's often better than Function Expressions. + +It's called "arrow functions", because it looks like this: + +```js +let func = (arg1, arg2, ..., argN) => expression; +``` + +This creates a function `func` that accepts arguments `arg1..argN`, then evaluates the `expression` on the right side with their use and returns its result. + +In other words, it's the shorter version of: + +```js +let func = function(arg1, arg2, ..., argN) { + return expression; +}; +``` + +Let's see a concrete example: + +```js run +let sum = (a, b) => a + b; + +/* This arrow function is a shorter form of: + +let sum = function(a, b) { + return a + b; +}; +*/ + +alert( sum(1, 2) ); // 3 +``` + +As you can see, `(a, b) => a + b` means a function that accepts two arguments named `a` and `b`. Upon the execution, it evaluates the expression `a + b` and returns the result. + +- If we have only one argument, then parentheses around parameters can be omitted, making that even shorter. + + For example: + + ```js run + *!* + let double = n => n * 2; + // roughly the same as: let double = function(n) { return n * 2 } + */!* + + alert( double(3) ); // 6 + ``` + +- If there are no arguments, parentheses are empty, but they must be present: + + ```js run + let sayHi = () => alert("Hello!"); + + sayHi(); + ``` + +Arrow functions can be used in the same way as Function Expressions. + +For instance, to dynamically create a function: + +```js run +let age = prompt("What is your age?", 18); + +let welcome = (age < 18) ? + () => alert('Hello!') : + () => alert("Greetings!"); + +welcome(); +``` + +Arrow functions may appear unfamiliar and not very readable at first, but that quickly changes as the eyes get used to the structure. + +They are very convenient for simple one-line actions, when we're just too lazy to write many words. + +## Multiline arrow functions + +The arrow functions that we've seen so far were very simple. They took arguments from the left of `=>`, evaluated and returned the right-side expression with them. + +Sometimes we need a more complex function, with multiple expressions and statements. In that case, we can enclose them in curly braces. The major difference is that curly braces require a `return` within them to return a value (just like a regular function does). + +Like this: + +```js run +let sum = (a, b) => { // the curly brace opens a multiline function + let result = a + b; +*!* + return result; // if we use curly braces, then we need an explicit "return" +*/!* +}; + +alert( sum(1, 2) ); // 3 +``` + +```smart header="More to come" +Here we praised arrow functions for brevity. But that's not all! + +Arrow functions have other interesting features. + +To study them in-depth, we first need to get to know some other aspects of JavaScript, so we'll return to arrow functions later in the chapter . + +For now, we can already use arrow functions for one-line actions and callbacks. +``` + +## Summary + +Arrow functions are handy for simple actions, especially for one-liners. They come in two flavors: + +1. Without curly braces: `(...args) => expression` -- the right side is an expression: the function evaluates it and returns the result. Parentheses can be omitted, if there's only a single argument, e.g. `n => n*2`. +2. With curly braces: `(...args) => { body }` -- brackets allow us to write multiple statements inside the function, but we need an explicit `return` to return something. diff --git a/1-js/02-first-steps/18-javascript-specials/article.md b/1-js/02-first-steps/18-javascript-specials/article.md new file mode 100644 index 0000000000..e7ddacac41 --- /dev/null +++ b/1-js/02-first-steps/18-javascript-specials/article.md @@ -0,0 +1,284 @@ +# JavaScript specials + +This chapter briefly recaps the features of JavaScript that we've learned by now, paying special attention to subtle moments. + +## Code structure + +Statements are delimited with a semicolon: + +```js run no-beautify +alert('Hello'); alert('World'); +``` + +Usually, a line-break is also treated as a delimiter, so that would also work: + +```js run no-beautify +alert('Hello') +alert('World') +``` + +That's called "automatic semicolon insertion". Sometimes it doesn't work, for instance: + +```js run +alert("There will be an error after this message") + +[1, 2].forEach(alert) +``` + +Most codestyle guides agree that we should put a semicolon after each statement. + +Semicolons are not required after code blocks `{...}` and syntax constructs with them like loops: + +```js +function f() { + // no semicolon needed after function declaration +} + +for(;;) { + // no semicolon needed after the loop +} +``` + +...But even if we can put an "extra" semicolon somewhere, that's not an error. It will be ignored. + +More in: . + +## Strict mode + +To fully enable all features of modern JavaScript, we should start scripts with `"use strict"`. + +```js +'use strict'; + +... +``` + +The directive must be at the top of a script or at the beginning of a function body. + +Without `"use strict"`, everything still works, but some features behave in the old-fashioned, "compatible" way. We'd generally prefer the modern behavior. + +Some modern features of the language (like classes that we'll study in the future) enable strict mode implicitly. + +More in: . + +## Variables + +Can be declared using: + +- `let` +- `const` (constant, can't be changed) +- `var` (old-style, will see later) + +A variable name can include: +- Letters and digits, but the first character may not be a digit. +- Characters `$` and `_` are normal, on par with letters. +- Non-Latin alphabets and hieroglyphs are also allowed, but commonly not used. + +Variables are dynamically typed. They can store any value: + +```js +let x = 5; +x = "John"; +``` + +There are 8 data types: + +- `number` for both floating-point and integer numbers, +- `bigint` for integer numbers of arbitrary length, +- `string` for strings, +- `boolean` for logical values: `true/false`, +- `null` -- a type with a single value `null`, meaning "empty" or "does not exist", +- `undefined` -- a type with a single value `undefined`, meaning "not assigned", +- `object` and `symbol` -- for complex data structures and unique identifiers, we haven't learnt them yet. + +The `typeof` operator returns the type for a value, with two exceptions: +```js +typeof null == "object" // error in the language +typeof function(){} == "function" // functions are treated specially +``` + +More in: and . + +## Interaction + +We're using a browser as a working environment, so basic UI functions will be: + +[`prompt(question, [default])`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) +: Ask a `question`, and return either what the visitor entered or `null` if they clicked "cancel". + +[`confirm(question)`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) +: Ask a `question` and suggest to choose between Ok and Cancel. The choice is returned as `true/false`. + +[`alert(message)`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) +: Output a `message`. + +All these functions are *modal*, they pause the code execution and prevent the visitor from interacting with the page until they answer. + +For instance: + +```js run +let userName = prompt("Your name?", "Alice"); +let isTeaWanted = confirm("Do you want some tea?"); + +alert( "Visitor: " + userName ); // Alice +alert( "Tea wanted: " + isTeaWanted ); // true +``` + +More in: . + +## Operators + +JavaScript supports the following operators: + +Arithmetical +: Regular: `* + - /`, also `%` for the remainder and `**` for power of a number. + + The binary plus `+` concatenates strings. And if any of the operands is a string, the other one is converted to string too: + + ```js run + alert( '1' + 2 ); // '12', string + alert( 1 + '2' ); // '12', string + ``` + +Assignments +: There is a simple assignment: `a = b` and combined ones like `a *= 2`. + +Bitwise +: Bitwise operators work with 32-bit integers at the lowest, bit-level: see the [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators#bitwise_operators) when they are needed. + +Conditional +: The only operator with three parameters: `cond ? resultA : resultB`. If `cond` is truthy, returns `resultA`, otherwise `resultB`. + +Logical operators +: Logical AND `&&` and OR `||` perform short-circuit evaluation and then return the value where it stopped (not necessary `true`/`false`). Logical NOT `!` converts the operand to boolean type and returns the inverse value. + +Nullish coalescing operator +: The `??` operator provides a way to choose a defined value from a list of variables. The result of `a ?? b` is `a` unless it's `null/undefined`, then `b`. + +Comparisons +: Equality check `==` for values of different types converts them to a number (except `null` and `undefined` that equal each other and nothing else), so these are equal: + + ```js run + alert( 0 == false ); // true + alert( 0 == '' ); // true + ``` + + Other comparisons convert to a number as well. + + The strict equality operator `===` doesn't do the conversion: different types always mean different values for it. + + Values `null` and `undefined` are special: they equal `==` each other and don't equal anything else. + + Greater/less comparisons compare strings character-by-character, other types are converted to a number. + +Other operators +: There are few others, like a comma operator. + +More in: , , , . + +## Loops + +- We covered 3 types of loops: + + ```js + // 1 + while (condition) { + ... + } + + // 2 + do { + ... + } while (condition); + + // 3 + for(let i = 0; i < 10; i++) { + ... + } + ``` + +- The variable declared in `for(let...)` loop is visible only inside the loop. But we can also omit `let` and reuse an existing variable. +- Directives `break/continue` allow to exit the whole loop/current iteration. Use labels to break nested loops. + +Details in: . + +Later we'll study more types of loops to deal with objects. + +## The "switch" construct + +The "switch" construct can replace multiple `if` checks. It uses `===` (strict equality) for comparisons. + +For instance: + +```js run +let age = prompt('Your age?', 18); + +switch (age) { + case 18: + alert("Won't work"); // the result of prompt is a string, not a number + break; + + case "18": + alert("This works!"); + break; + + default: + alert("Any value not equal to one above"); +} +``` + +Details in: . + +## Functions + +We covered three ways to create a function in JavaScript: + +1. Function Declaration: the function in the main code flow + + ```js + function sum(a, b) { + let result = a + b; + + return result; + } + ``` + +2. Function Expression: the function in the context of an expression + + ```js + let sum = function(a, b) { + let result = a + b; + + return result; + }; + ``` + +3. Arrow functions: + + ```js + // expression on the right side + let sum = (a, b) => a + b; + + // or multi-line syntax with { ... }, need return here: + let sum = (a, b) => { + // ... + return a + b; + } + + // without arguments + let sayHi = () => alert("Hello"); + + // with a single argument + let double = n => n * 2; + ``` + + +- Functions may have local variables: those declared inside its body or its parameter list. Such variables are only visible inside the function. +- Parameters can have default values: `function sum(a = 1, b = 2) {...}`. +- Functions always return something. If there's no `return` statement, then the result is `undefined`. + +Details: see , . + +## More to come + +That was a brief list of JavaScript features. As of now we've studied only basics. Further in the tutorial you'll find more specials and advanced features of 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 d09493a2eb..4f50fb428b 100644 --- a/1-js/03-code-quality/01-debugging-chrome/article.md +++ b/1-js/03-code-quality/01-debugging-chrome/article.md @@ -1,46 +1,46 @@ -# Debugging in Chrome +# Debugging in the browser Before writing more complex code, let's talk about debugging. -All modern browsers and most other environments support "debugging" -- a special UI in developer tools that makes finding and fixing errors much easier. +[Debugging](https://en.wikipedia.org/wiki/Debugging) is the process of finding and fixing errors within a script. All modern browsers and most other environments support debugging tools -- a special UI in developer tools that makes debugging much easier. It also allows to trace the code step by step to see what exactly is going on. -We'll be using Chrome here, because it's probably the most feature-rich in this aspect. +We'll be using Chrome here, because it has enough features, most other browsers have a similar process. -## The "sources" pane +## The "Sources" panel Your Chrome version may look a little bit different, but it still should be obvious what's there. - Open the [example page](debugging/index.html) in Chrome. - Turn on developer tools with `key:F12` (Mac: `key:Cmd+Opt+I`). -- Select the `sources` pane. +- Select the `Sources` panel. Here's what you should see if you are doing it for the first time: -![](chrome-open-sources.png) +![](chrome-open-sources.svg) -The toggler button opens the tab with files. +The toggler button opens the tab with files. -Let's click it and select `index.html` and then `hello.js` in the tree view. Here's what should show up: +Let's click it and select `hello.js` in the tree view. Here's what should show up: -![](chrome-tabs.png) +![](chrome-tabs.svg) -Here we can see three zones: +The Sources panel has 3 parts: -1. The **Resources zone** lists HTML, JavaScript, CSS and other files, including images that are attached to the page. Chrome extensions may appear here too. -2. The **Source zone** shows the source code. -3. The **Information and control zone** is for debugging, we'll explore it soon. +1. The **File Navigator** pane lists HTML, JavaScript, CSS and other files, including images that are attached to the page. Chrome extensions may appear here too. +2. The **Code Editor** pane shows the source code. +3. The **JavaScript Debugging** pane is for debugging, we'll explore it soon. -Now you could click the same toggler again to hide the resources list and give the code some space. +Now you could click the same toggler again to hide the resources list and give the code some space. ## Console -If we press `Esc`, then a console opens below. We can type commands there and press `key:Enter` to execute. +If we press `key:Esc`, then a console opens below. We can type commands there and press `key:Enter` to execute. After a statement is executed, its result is shown below. -For example, here `1+2` results in `3`, and `hello("debugger")` returns nothing, so the result is `undefined`: +For example, here `1+2` results in `3`, while the function call `hello("debugger")` returns nothing, so the result is `undefined`: -![](chrome-sources-console.png) +![](chrome-sources-console.svg) ## Breakpoints @@ -50,27 +50,27 @@ Congratulations! You've set a breakpoint. Please also click on the number for li It should look like this (blue is where you should click): -![](chrome-sources-breakpoint.png) +![](chrome-sources-breakpoint.svg) A *breakpoint* is a point of code where the debugger will automatically pause the JavaScript execution. While the code is paused, we can examine current variables, execute commands in the console etc. In other words, we can debug it. -We can always find a list of breakpoints in the right pane. That's useful when we have many breakpoints in various files. It allows to: -- Quickly jump to the breakpoint in the code (by clicking on it in the right pane). +We can always find a list of breakpoints in the right panel. That's useful when we have many breakpoints in various files. It allows us to: +- Quickly jump to the breakpoint in the code (by clicking on it in the right panel). - Temporarily disable the breakpoint by unchecking it. - Remove the breakpoint by right-clicking and selecting Remove. - ...And so on. ```smart header="Conditional breakpoints" -*Right click* on the line number allows to create a *conditional* breakpoint. It only triggers when the given expression is truthy. +*Right click* on the line number allows to create a *conditional* breakpoint. It only triggers when the given expression, that you should provide when you create it, is truthy. That's handy when we need to stop only for a certain variable value or for certain function parameters. ``` -## Debugger command +## The command "debugger" -We can also pause the code by using the `debugger` command, like this: +We can also pause the code by using the `debugger` command in it, like this: ```js function hello(name) { @@ -84,28 +84,27 @@ function hello(name) { } ``` -That's very convenient when we are in a code editor and don't want to switch to the browser and look up the script in developer tools to set the breakpoint. - +Such command works only when the development tools are open, otherwise the browser ignores it. ## Pause and look around -In our example, `hello()` is called during the page load, so the easiest way to activate the debugger is to reload the page. So let's press `key:F5` (Windows, Linux) or `key:Cmd+R` (Mac). +In our example, `hello()` is called during the page load, so the easiest way to activate the debugger (after we've set the breakpoints) is to reload the page. So let's press `key:F5` (Windows, Linux) or `key:Cmd+R` (Mac). As the breakpoint is set, the execution pauses at the 4th line: -![](chrome-sources-debugger-pause.png) +![](chrome-sources-debugger-pause.svg) Please open the informational dropdowns to the right (labeled with arrows). They allow you to examine the current code state: 1. **`Watch` -- shows current values for any expressions.** - You can click the plus `+` and input an expression. The debugger will show its value at any moment, automatically recalculating it in the process of execution. + You can click the plus `+` and input an expression. The debugger will show its value, automatically recalculating it in the process of execution. 2. **`Call Stack` -- shows the nested calls chain.** At the current moment the debugger is inside `hello()` call, called by a script in `index.html` (no function there, so it's called "anonymous"). - If you click on a stack item, the debugger jumps to the corresponding code, and all its variables can be examined as well. + If you click on a stack item (e.g. "anonymous"), the debugger jumps to the corresponding code, and all its variables can be examined as well. 3. **`Scope` -- current variables.** `Local` shows local function variables. You can also see their values highlighted right over the source. @@ -118,52 +117,65 @@ Please open the informational dropdowns to the right (labeled with arrows). They Now it's time to *trace* the script. -There are buttons for it at the top of the right pane. Let's engage them. - - -- continue the execution, hotkey `key:F8`. +There are buttons for it at the top of the right panel. Let's engage them. + + -- "Resume": continue the execution, hotkey `key:F8`. : Resumes the execution. If there are no additional breakpoints, then the execution just continues and the debugger loses control. Here's what we can see after a click on it: - ![](chrome-sources-debugger-trace-1.png) + ![](chrome-sources-debugger-trace-1.svg) + + The execution has resumed, reached another breakpoint inside `say()` and paused there. Take a look at the "Call Stack" at the right. It has increased by one more call. We're inside `say()` now. + + -- "Step": run the next command, hotkey `key:F9`. +: Run the next statement. If we click it now, `alert` will be shown. + + Clicking this again and again will step through all script statements one by one. + + -- "Step over": run the next command, but *don't go into a function*, hotkey `key:F10`. +: Similar to the previous "Step" command, but behaves differently if the next statement is a function call (not a built-in, like `alert`, but a function of our own). + + If we compare them, the "Step" command goes into a nested function call and pauses the execution at its first line, while "Step over" executes the nested function call invisibly to us, skipping the function internals. + + The execution is then paused immediately after that function call. - The execution has resumed, reached another breakpoint inside `say()` and paused there. Take a look at the "Call stack" at the right. It has increased by one more call. We're inside `say()` now. + That's good if we're not interested to see what happens inside the function call. - -- make a step (run the next command), but *don't go into the function*, hotkey `key:F10`. -: If we click it now, `alert` will be shown. The important thing is that `alert` can be any function, the execution "steps over it", skipping the function internals. + -- "Step into", hotkey `key:F11`. +: That's similar to "Step", but behaves differently in case of asynchronous function calls. If you're only starting to learn JavaScript, then you can ignore the difference, as we don't have asynchronous calls yet. - -- make a step, hotkey `key:F11`. -: The same as the previous one, but "steps into" nested functions. Clicking this will step through all script actions one by one. + For the future, just note that "Step" command ignores async actions, such as `setTimeout` (scheduled function call), that execute later. The "Step into" goes into their code, waiting for them if necessary. See [DevTools manual](https://developers.google.com/web/updates/2018/01/devtools#async) for more details. - -- continue the execution till the end of the current function, hotkey `key:Shift+F11`. -: The execution would stop at the very last line of the current function. That's handy when we accidentally entered a nested call using , but it does not interest us, and we want to continue to its end as soon as possible. + -- "Step out": continue the execution till the end of the current function, hotkey `key:Shift+F11`. +: Continue the execution and stop it at the very last line of the current function. That's handy when we accidentally entered a nested call using , but it does not interest us, and we want to continue to its end as soon as possible. - -- enable/disable all breakpoints. + -- enable/disable all breakpoints. : That button does not move the execution. Just a mass on/off for breakpoints. - -- enable/disable automatic pause in case of an error. -: When enabled, and the developer tools is open, a script error automatically pauses the execution. Then we can analyze variables to see what went wrong. So if our script dies with an error, we can open debugger, enable this option and reload the page to see where it dies and what's the context at that moment. + -- enable/disable automatic pause in case of an error. +: When enabled, if the developer tools is open, an error during the script execution automatically pauses it. Then we can analyze variables in the debugger to see what went wrong. So if our script dies with an error, we can open debugger, enable this option and reload the page to see where it dies and what's the context at that moment. ```smart header="Continue to here" Right click on a line of code opens the context menu with a great option called "Continue to here". -That's handy when we want to move multiple steps forward, but we're too lazy to set a breakpoint. +That's handy when we want to move multiple steps forward to the line, but we're too lazy to set a breakpoint. ``` ## Logging -To output something to console, there's `console.log` function. +To output something to console from our code, there's `console.log` function. For instance, this outputs values from `0` to `4` to console: ```js run // open console to see for (let i = 0; i < 5; i++) { - console.log("value", i); + console.log("value,", i); } ``` -Regular users don't see that output, it is in the console. To see it, either open the Console tab of developer tools or press `key:Esc` while in another tab: that opens the console at the bottom. +Regular users don't see that output, it is in the console. To see it, either open the Console panel of developer tools or press `key:Esc` while in another panel: that opens the console at the bottom. If we have enough logging in our code, then we can see what's going on from the records, without the debugger. @@ -172,12 +184,12 @@ If we have enough logging in our code, then we can see what's going on from the As we can see, there are three main ways to pause a script: 1. A breakpoint. 2. The `debugger` statements. -3. An error (if dev tools are open and the button is "on") +3. An error (if dev tools are open and the button is "on"). -Then we can examine variables and step on to see where the execution goes wrong. +When paused, we can debug: examine variables and trace the code to see where the execution goes wrong. There are many more options in developer tools than covered here. The full manual is at . The information from this chapter is enough to begin debugging, but later, especially if you do a lot of browser stuff, please go there and look through more advanced capabilities of developer tools. -Oh, and also you can click at various places of dev tools and just see what's showing up. That's probably the fastest route to learn dev tools. Don't forget about the right click as well! +Oh, and also you can click at various places of dev tools and just see what's showing up. That's probably the fastest route to learn dev tools. Don't forget about the right click and context menus! diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png deleted file mode 100644 index efa3c19df1..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg new file mode 100644 index 0000000000..5fc6dce3aa --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg @@ -0,0 +1 @@ +open sources \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png deleted file mode 100644 index e184bdd01f..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png deleted file mode 100644 index 2fe449c9b6..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg new file mode 100644 index 0000000000..63bf4966e2 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg @@ -0,0 +1 @@ +here's the listbreakpoints \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png deleted file mode 100644 index e4abc89d1f..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png deleted file mode 100644 index 98b22e777c..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg new file mode 100644 index 0000000000..3fe5f124f2 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png deleted file mode 100644 index 3269a80f06..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png deleted file mode 100644 index 719293d2e5..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg new file mode 100644 index 0000000000..0147c2e0aa --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg @@ -0,0 +1 @@ +213see the outer call detailswatch expressionscurrent variables \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png deleted file mode 100644 index 5c22ab361b..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png deleted file mode 100644 index 1848ccfacf..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg new file mode 100644 index 0000000000..9fa1b3b8cc --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg @@ -0,0 +1 @@ +nested calls \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png deleted file mode 100644 index fcabf722eb..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png deleted file mode 100644 index ff91c531f8..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg new file mode 100644 index 0000000000..0167082569 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg @@ -0,0 +1 @@ +213 \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png deleted file mode 100644 index 09b10bf48d..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png b/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png deleted file mode 100644 index 95399c7bb6..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png deleted file mode 100644 index d9d576ecea..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png deleted file mode 100644 index 0482bbed53..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png deleted file mode 100644 index fc65ed3fc5..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png deleted file mode 100644 index ac8fb1ff5d..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png deleted file mode 100644 index d6eadbe6f6..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png deleted file mode 100644 index 22fb9a5d57..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png deleted file mode 100644 index eba2b9bfc3..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png deleted file mode 100644 index 0f29946c3a..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png deleted file mode 100644 index 7a16ea1c26..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/console_error.png b/1-js/03-code-quality/01-debugging-chrome/console_error.png deleted file mode 100644 index ccf1b515a2..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/console_error.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png b/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png deleted file mode 100644 index 4ab2fcea83..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png and /dev/null differ 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/manage1.png b/1-js/03-code-quality/01-debugging-chrome/manage1.png deleted file mode 100644 index f624a1fd83..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage1.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png deleted file mode 100644 index 3f3c8116fc..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage2.png b/1-js/03-code-quality/01-debugging-chrome/manage2.png deleted file mode 100644 index a038e31069..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage2.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png deleted file mode 100644 index 904280e499..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage3.png b/1-js/03-code-quality/01-debugging-chrome/manage3.png deleted file mode 100644 index 94bd7b31db..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage3.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png deleted file mode 100644 index 3a988aa2ac..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage4.png b/1-js/03-code-quality/01-debugging-chrome/manage4.png deleted file mode 100644 index 04f57c72dc..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage4.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png deleted file mode 100644 index d8758709ab..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage5.png b/1-js/03-code-quality/01-debugging-chrome/manage5.png deleted file mode 100644 index 55355c37db..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage5.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png deleted file mode 100644 index c08f8a6868..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage6.png b/1-js/03-code-quality/01-debugging-chrome/manage6.png deleted file mode 100644 index cdd9bd0ef3..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage6.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png deleted file mode 100644 index 52dad975a7..0000000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png and /dev/null differ 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 810fdad537..4facc8b291 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 @@ -4,22 +4,22 @@ You could note the following: ```js no-beautify function pow(x,n) // <- no space between arguments { // <- figure bracket on a separate line - let result=1; // <- no spaces to the both sides of = + let result=1; // <- no spaces before or after = for(let i=0;i -![](figure-bracket-style.png) +No one likes to read a long horizontal line of code. It's best practice to split them. -As a summary: -- For a really short code, one line is acceptable: like `if (cond) return null`. -- But a separate line for each statement in brackets is usually better. +For example: +```js +// backtick quotes ` allow to split the string into multiple lines +let str = ` + ECMA International's TC39 is a group of JavaScript developers, + implementers, academics, and more, collaborating with the community + to maintain and evolve the definition of JavaScript. +`; +``` -### Line length +And, for `if` statements: -The maximal line length should be limited. No one likes to eye-follow a long horizontal line. It's better to split it. +```js +if ( + id === 123 && + moonPhase === 'Waning Gibbous' && + zodiacSign === 'Libra' +) { + letTheSorceryBegin(); +} +``` -The maximal line length is agreed on the team-level. It's usually 80 or 120 characters. +The maximum line length should be agreed upon at the team-level. It's usually 80 or 120 characters. ### Indents There are two types of indents: -- **A horizontal indent: 2(4) spaces.** +- **Horizontal indents: 2 or 4 spaces.** - A horizontal indentation is made using either 2 or 4 spaces or the "Tab" symbol. Which one to choose is an old holy war. Spaces are more common nowadays. + A horizontal indentation is made using either 2 or 4 spaces or the horizontal tab symbol (key `key:Tab`). Which one to choose is an old holy war. Spaces are more common nowadays. - One advantage of spaces over tabs is that spaces allow more flexible configurations of indents than the "Tab" symbol. + One advantage of spaces over tabs is that spaces allow more flexible configurations of indents than the tab symbol. - For instance, we can align the arguments with the opening bracket, like this: + For instance, we can align the parameters with the opening bracket, like this: ```js no-beautify show(parameters, @@ -107,9 +129,9 @@ There are two types of indents: } ``` -- **A vertical indent: empty lines for splitting code into logical blocks.** +- **Vertical indents: empty lines for splitting code into logical blocks.** - Even a single function can often be divided in logical blocks. In the example below, the initialization of variables, the main loop and returning the result are split vertically: + Even a single function can often be divided into logical blocks. In the example below, the initialization of variables, the main loop and returning the result are split vertically: ```js function pow(x, n) { @@ -125,21 +147,21 @@ There are two types of indents: Insert an extra newline where it helps to make the code more readable. There should not be more than nine lines of code without a vertical indentation. -### A semicolon +### Semicolons -A semicolon should be present after each statement. Even if it could possibly be skipped. +A semicolon should be present after each statement, even if it could possibly be skipped. -There are languages where a semicolon is truly optional. It's rarely used there. But in JavaScript there are few cases when a line break is sometimes not interpreted as a semicolon. That leaves a place for programming errors. +There are languages where a semicolon is truly optional and it is rarely used. In JavaScript, though, there are cases where a line break is not interpreted as a semicolon, leaving the code vulnerable to errors. See more about that in the chapter . -As you become more mature as a programmer, you may choose a no-semicolon style, like [StandardJS](https://standardjs.com/), but that's only when you know JavaScript well and understand possible pitfalls. +If you're an experienced JavaScript programmer, you may choose a no-semicolon code style like [StandardJS](https://standardjs.com/). Otherwise, it's best to use semicolons to avoid possible pitfalls. The majority of developers put semicolons. -### Nesting levels +### Nesting Levels -There should not be too many nesting levels. +Try to avoid nesting code too many levels deep. -Sometimes it's a good idea to use the ["continue"](info:while-for#continue) directive in the loop to evade extra nesting in `if(..) { ... }`: +For example, in the loop, it's sometimes a good idea to use the [`continue`](info:while-for#continue) directive to avoid extra nesting. -Instead of: +For example, instead of adding a nested `if` conditional like this: ```js for (let i = 0; i < 10; i++) { @@ -162,7 +184,7 @@ A similar thing can be done with `if/else` and `return`. For example, two constructs below are identical. -The first one: +Option 1: ```js function pow(x, n) { @@ -180,7 +202,7 @@ function pow(x, n) { } ``` -And this: +Option 2: ```js function pow(x, n) { @@ -199,13 +221,13 @@ function pow(x, n) { } ``` -...But the second one is more readable, because the "edge case" of `n < 0` is handled early on, and then we have the "main" code flow, without an additional nesting. +The second one is more readable because the "special case" of `n < 0` is handled early on. Once the check is done we can move on to the "main" code flow without the need for additional nesting. -## Functions below the code +## Function Placement -If you are writing several "helper" functions and the code to use them, then there are three ways to place them. +If you are writing several "helper" functions and the code that uses them, there are three ways to organize the functions. -1. Functions above the code that uses them: +1. Declare the functions *above* the code that uses them: ```js // *!*function declarations*/!* @@ -235,7 +257,6 @@ If you are writing several "helper" functions and the code to use them, then the walkAround(); // --- *!*helper functions*/!* --- - function createElement() { ... } @@ -248,55 +269,54 @@ If you are writing several "helper" functions and the code to use them, then the ... } ``` -3. Mixed: a function is described where it's first used. +3. Mixed: a function is declared where it's first used. Most of time, the second variant is preferred. -That's because when reading a code, we first want to know "what it does". If the code goes first, then it provides that information. And then maybe we won't need to read functions at all, especially if their names are adequate to what they're doing. +That's because when reading code, we first want to know *what it does*. If the code goes first, then it becomes clear from the start. Then, maybe we won't need to read the functions at all, especially if their names are descriptive of what they actually do. -## Style guides +## Style Guides -A style guide contains general rules about "how to write": which quotes to use, how many spaces to indent, where to put line breaks, etc. A lot of minor things. +A style guide contains general rules about "how to write" code, e.g. which quotes to use, how many spaces to indent, the maximal line length, etc. A lot of minor things. -In total, when all members of a team use the same style guide, the code looks uniform. No matter who of the team wrote it, it's still the same style. +When all members of a team use the same style guide, the code looks uniform, regardless of which team member wrote it. -Surely, a team may think out a style guide themselves. But as of now, there's no need to. There are many tried, worked-out style guides, which are easy to adopt. +Of course, a team can always write their own style guide, but usually there's no need to. There are many existing guides to choose from. -For instance: +Some popular choices: -- [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml) +- [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) - [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) - [Idiomatic.JS](https://github.com/rwaldron/idiomatic.js) - [StandardJS](https://standardjs.com/) -- (there are more) - -If you're a novice developer, then you could start with the cheatsheet above in the chapter, and later browse the style guides to pick up the common principles and maybe choose one. +- (plus many more) -## Automated linters +If you're a novice developer, start with the cheat sheet at the beginning of this chapter. Then you can browse other style guides to pick up more ideas and decide which one you like best. -There are tools that can check the code style automatically. They are called "linters". +## Automated Linters -The great thing about them is that style-checking also finds some bugs, like a typo in a variable or function name. +Linters are tools that can automatically check the style of your code and make improving suggestions. -So it's recommended to install one, even if you don't want to stick to a "code style". They help to find typos -- and that's already good enough. +The great thing about them is that style-checking can also find some bugs, like typos in variable or function names. Because of this feature, using a linter is recommended even if you don't want to stick to one particular "code style". -Most well-known tools are: +Here are some well-known linting tools: -- [JSLint](http://www.jslint.com/) -- one of the first linters. -- [JSHint](http://www.jshint.com/) -- more settings than JSLint. -- [ESLint](http://eslint.org/) -- probably the newest one. +- [JSLint](https://www.jslint.com/) -- one of the first linters. +- [JSHint](https://jshint.com/) -- more settings than JSLint. +- [ESLint](https://eslint.org/) -- probably the newest one. -All of them can do the job. The author uses [ESLint](http://eslint.org/). +All of them can do the job. The author uses [ESLint](https://eslint.org/). -Most linters are integrated with editors: just enable the plugin in the editor and configure the style. +Most linters are integrated with many popular editors: just enable the plugin in the editor and configure the style. For instance, for ESLint you should do the following: -1. Install [Node.JS](https://nodejs.org/). +1. Install [Node.js](https://nodejs.org/). 2. Install ESLint with the command `npm install -g eslint` (npm is a JavaScript package installer). 3. Create a config file named `.eslintrc` in the root of your JavaScript project (in the folder that contains all your files). +4. Install/enable the plugin for your editor that integrates with ESLint. The majority of editors have one. -Here's an example of `.eslintrc`: +Here's an example of an `.eslintrc` file: ```js { @@ -308,27 +328,21 @@ Here's an example of `.eslintrc`: }, "rules": { "no-console": 0, - }, - "indent": 2 + "indent": 2 + } } ``` -Here the directive `"extends"` denotes that we base on the "eslint:recommended" set of settings, and then we specify our own. - -Then install/enable the plugin for your editor that integrates with ESLint. The majority of editors have it. - -It is possible to download style rule sets from the web and extend them instead. See for more details about installation. - -Using a linter has a great side-effect: linters catch typos. For instance, when an undefined variable is accessed, a linter detects it and (if integrated with an editor) highlights it. In most cases that's a mistype. So we can fix it right ahead. +Here the directive `"extends"` denotes that the configuration is based on the "eslint:recommended" set of settings. After that, we specify our own. -For that reason even if you're not concerned about styles, using a linter is highly recommended. +It is also possible to download style rule sets from the web and extend them instead. See for more details about installation. -Also certain IDEs support built-in linting, that also may be good, but not so tunable as ESLint. +Also certain IDEs have built-in linting, which is convenient but not as customizable as ESLint. ## Summary -All syntax rules from this chapter and the style guides aim to increase readability, so all of them are debatable. +All syntax rules described in this chapter (and in the style guides referenced) aim to increase the readability of your code. All of them are debatable. -When we think about "how to write better?", the sole criterion is "what makes the code more readable and easier to understand? what helps to avoid errors?" That's the main thing to keep in mind when choosing the style or discussing which one is better. +When we think about writing "better" code, the questions we should ask ourselves are: "What makes the code more readable and easier to understand?" and "What can help us avoid errors?" These are the main things to keep in mind when choosing and debating code styles. -Read style guides to see the latest ideas about that and follow those that you find the best. +Reading popular style guides will allow you to keep up to date with the latest ideas about code style trends and best practices. diff --git a/1-js/03-code-quality/02-coding-style/code-style.png b/1-js/03-code-quality/02-coding-style/code-style.png deleted file mode 100644 index 278fd294cf..0000000000 Binary files a/1-js/03-code-quality/02-coding-style/code-style.png and /dev/null differ 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 new file mode 100644 index 0000000000..739d9f1edf --- /dev/null +++ b/1-js/03-code-quality/02-coding-style/code-style.svg @@ -0,0 +1 @@ +2No space between the function name and parentheses between the parentheses and the parameterIndentation 2 spacesA space after for/if/while…} else { without a line breakSpaces around a nested callAn empty line between logical blocksLines are not very longA semicolon ; is mandatorySpaces around operatorsCurly brace { on the same line, after a spaceA space between argumentsA space between parameters \ No newline at end of file diff --git a/1-js/03-code-quality/02-coding-style/code-style@2x.png b/1-js/03-code-quality/02-coding-style/code-style@2x.png deleted file mode 100644 index b576143455..0000000000 Binary files a/1-js/03-code-quality/02-coding-style/code-style@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/02-coding-style/figure-bracket-style.png b/1-js/03-code-quality/02-coding-style/figure-bracket-style.png deleted file mode 100644 index 112c2803ed..0000000000 Binary files a/1-js/03-code-quality/02-coding-style/figure-bracket-style.png and /dev/null differ diff --git a/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png b/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png deleted file mode 100644 index ce6e75c4dd..0000000000 Binary files a/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/03-comments/article.md b/1-js/03-code-quality/03-comments/article.md index 744751ccf6..af3a06c80b 100644 --- a/1-js/03-code-quality/03-comments/article.md +++ b/1-js/03-code-quality/03-comments/article.md @@ -4,7 +4,7 @@ As we know from the chapter , comments can be single-line: start We normally use them to describe how and why the code works. -From the first sight, commenting might be obvious, but novices in programming usually get it wrong. +At first sight, commenting might be obvious, but novices in programming often use them wrongly. ## Bad comments @@ -18,7 +18,7 @@ complex; code; ``` -But in good code the amount of such "explanatory" comments should be minimal. Seriously, code should be easy to understand without them. +But in good code, the amount of such "explanatory" comments should be minimal. Seriously, the code should be easy to understand without them. There's a great rule about that: "if the code is so unclear that it requires a comment, then maybe it should be rewritten instead". @@ -120,30 +120,30 @@ In reality, we can't totally avoid "explanatory" comments. There are complex alg So, explanatory comments are usually bad. Which comments are good? Describe the architecture -: Provide a high-level overview of components, how they interact, what's the control flow in various situations... In short -- the bird's eye view of the code. There's a special diagram language [UML](http://wikipedia.org/wiki/Unified_Modeling_Language) for high-level architecture diagrams. Definitely worth studying. +: Provide a high-level overview of components, how they interact, what's the control flow in various situations... In short -- the bird's eye view of the code. There's a special language [UML](http://wikipedia.org/wiki/Unified_Modeling_Language) to build high-level architecture diagrams explaining the code. Definitely worth studying. -Document a function usage +Document function parameters and usage : There's a special syntax [JSDoc](http://en.wikipedia.org/wiki/JSDoc) to document a function: usage, parameters, returned value. - For instance: - ```js - /** - * Returns x raised to the n-th power. - * - * @param {number} x The number to raise. - * @param {number} n The power, must be a natural number. - * @return {number} x raised to the n-th power. - */ - function pow(x, n) { - ... - } - ``` +For instance: +```js +/** + * Returns x raised to the n-th power. + * + * @param {number} x The number to raise. + * @param {number} n The power, must be a natural number. + * @return {number} x raised to the n-th power. + */ +function pow(x, n) { + ... +} +``` - Such comments allow us to understand the purpose of the function and use it the right way without looking in its code. +Such comments allow us to understand the purpose of the function and use it the right way without looking in its code. - By the way, many editors like [WebStorm](https://www.jetbrains.com/webstorm/) can understand them as well and use them to provide autocomplete and some automatic code-checking. +By the way, many editors like [WebStorm](https://www.jetbrains.com/webstorm/) can understand them as well and use them to provide autocomplete and some automatic code-checking. - Also, there are tools like [JSDoc 3](https://github.com/jsdoc3/jsdoc) that can generate HTML-documentation from the comments. You can read more information about JSDoc at . +Also, there are tools like [JSDoc 3](https://github.com/jsdoc/jsdoc) that can generate HTML-documentation from the comments. You can read more information about JSDoc at . Why is the task solved this way? : What's written is important. But what's *not* written may be even more important to understand what's going on. Why is the task solved exactly this way? The code gives no answer. @@ -162,7 +162,7 @@ Any subtle features of the code? Where they are used? ## Summary -An important sign of a good developer is comments: their presence and even their absense. +An important sign of a good developer is comments: their presence and even their absence. Good comments allow us to maintain the code well, come back to it after a delay and use it more effectively. @@ -175,6 +175,6 @@ Good comments allow us to maintain the code well, come back to it after a delay **Avoid comments:** - That tell "how code works" and "what it does". -- Put them only if it's impossible to make the code so simple and self-descriptive that it doesn't require those. +- Put them in only if it's impossible to make the code so simple and self-descriptive that it doesn't require them. Comments are also used for auto-documenting tools like JSDoc3: they read them and generate HTML-docs (or docs in another format). 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 05d4f3fe5b..96fdf4143c 100644 --- a/1-js/03-code-quality/04-ninja-code/article.md +++ b/1-js/03-code-quality/04-ninja-code/article.md @@ -1,11 +1,11 @@ # Ninja code -```quote author="Confucius" +```quote author="Confucius (Analects)" Learning without thought is labor lost; thought without learning is perilous. ``` -Programmer ninjas of the past used these tricks to make sharpen the mind of code maintainers. +Programmer ninjas of the past used these tricks to sharpen the mind of code maintainers. Code review gurus look for them in test tasks. @@ -32,7 +32,7 @@ For instance, take a look at this ternary operator `'?'`: i = i ? i < 0 ? Math.max(0, len + i) : i : 0; ``` -Cool, right? If you write like that, the developer who comes across this line and tries to understand what is the value of `i` is going to have a merry time. Then come to you, seeking for an answer. +Cool, right? If you write like that, a developer who comes across this line and tries to understand what is the value of `i` is going to have a merry time. Then come to you, seeking for an answer. Tell them that shorter is always better. Initiate them into the paths of ninja. @@ -43,7 +43,7 @@ The Dao hides in wordlessness. Only the Dao is well begun and well completed. ``` -Another way to code faster is to use single-letter variable names everywhere. Like `a`, `b` or `c`. +Another way to code shorter is to use single-letter variable names everywhere. Like `a`, `b` or `c`. A short variable disappears in the code like a real ninja in the forest. No one will be able to find it using "search" of the editor. And even if someone does, they won't be able to "decipher" what the name `a` or `b` means. @@ -83,7 +83,7 @@ While choosing a name try to use the most abstract word. Like `obj`, `data`, `va Give them a try. A young initiate may wonder -- are such names really useful for a ninja? Indeed, they are! - Sure, the variable name still means something. It says what's inside the variable: a string, a number or something else. But when an outsider tries to understand the code, he'll be surprised to see that there's actually no information at all! And will ultimately fail to alter your well-thought code. + Sure, the variable name still means something. It says what's inside the variable: a string, a number or something else. But when an outsider tries to understand the code, they'll be surprised to see that there's actually no information at all! And will ultimately fail to alter your well-thought code. The value type is easy to find out by debugging. But what's the meaning of the variable? Which string/number does it store? @@ -104,8 +104,8 @@ A quick read of such code becomes impossible. And when there's a typo... Ummm... ## Smart synonyms -```quote author="Confucius" -The hardest thing of all is to find a black cat in a dark room, especially if there is no cat. +```quote author="Laozi (Tao Te Ching)" +The Tao that can be told is not the eternal Tao. The name that can be named is not the eternal name. ``` Using *similar* names for *same* things makes life more interesting and shows your creativity to the public. @@ -137,7 +137,7 @@ Instead, reuse existing names. Just write new values into them. In a function try to use only variables passed as parameters. -That would make it really hard to identify what's exactly in the variable *now*. And also where it comes from. A person with weak intuition would have to analyze the code line-by-line and track the changes through every code branch. +That would make it really hard to identify what's exactly in the variable *now*. And also where it comes from. The purpose is to develop the intuition and memory of a person reading the code. A person with weak intuition would have to analyze the code line-by-line and track the changes through every code branch. **An advanced variant of the approach is to covertly (!) replace the value with something alike in the middle of a loop or a function.** @@ -153,9 +153,9 @@ function ninjaFunction(elem) { } ``` -A fellow programmer who wants to work with `elem` in the second half of the function will be surprised... Only during the debugging, after examining the code they will find out that he's working with a clone! +A fellow programmer who wants to work with `elem` in the second half of the function will be surprised... Only during the debugging, after examining the code they will find out that they're working with a clone! -Deadly effective even against an experienced ninja. Seen in code regularly. +Seen in code regularly. Deadly effective even against an experienced ninja. ## Underscores for fun @@ -169,7 +169,8 @@ A smart ninja puts underscores at one spot of code and evades them at other plac Let everyone see how magnificent your entities are! Names like `superElement`, `megaFrame` and `niceItem` will definitely enlighten a reader. -Indeed, from one hand, something is written: `super..`, `mega..`, `nice..` But from the other hand -- that brings no details. A reader may decide to look for a hidden meaning and meditate for an hour or two. +Indeed, from one hand, something is written: `super..`, `mega..`, `nice..` But from the other hand -- that brings no details. A reader may decide to look for a hidden meaning and meditate for an hour or two of their paid working time. + ## Overlap outer variables @@ -178,7 +179,7 @@ When in the light, can't see anything in the darkness.
When in the darkness, can see everything in the light. ``` -Use same names for variables inside and outside a function. As simple. No efforts required. +Use same names for variables inside and outside a function. As simple. No efforts to invent new names. ```js let *!*user*/!* = authenticateUser(); @@ -195,7 +196,7 @@ function render() { A programmer who jumps inside the `render` will probably fail to notice that there's a local `user` shadowing the outer one. -Then he'll try to work with `user` assuming that it's the external variable, the result of `authenticateUser()`... The trap is sprung! Hello, debugger... +Then they'll try to work with `user` assuming that it's the external variable, the result of `authenticateUser()`... The trap is sprung! Hello, debugger... ## Side-effects everywhere! @@ -204,7 +205,7 @@ There are functions that look like they don't change anything. Like `isReady()`, **A really beautiful trick is to add a "useful" action to them, besides the main task.** -The expression of dazed surprise on the face of your colleague when they see a function named `is..`, `check..` or `find...` changing something -- will definitely broaden your boundaries of reason. +An expression of dazed surprise on the face of your colleague when they see a function named `is..`, `check..` or `find...` changing something -- will definitely broaden your boundaries of reason. **Another way to surprise is to return a non-standard result.** diff --git a/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md index 7b58f0bf1c..4d0571b9d8 100644 --- a/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md +++ b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md @@ -4,7 +4,7 @@ What we have here is actually 3 tests, but layed out as a single function with 3 Sometimes it's easier to write this way, but if an error occurs, it's much less obvious what went wrong. -If an error happens inside a complex execution flow, then we'll have to figure out the data at that point. We'll actually have to *debug the test*. +If an error happens in the middle of a complex execution flow, then we'll have to figure out the data at that point. We'll actually have to *debug the test*. It would be much better to break the test into multiple `it` blocks with clearly written inputs and outputs. 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 311a052809..4c2b1aa5e3 100644 --- a/1-js/03-code-quality/05-testing-mocha/article.md +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -1,10 +1,8 @@ -# Automated testing with mocha +# Automated testing with Mocha -Automated testing will be used in further tasks. +Automated testing will be used in further tasks, and it's also widely used in real projects. -It's actually a part of the "educational minimum" of a developer. - -## Why we need tests? +## Why do we need tests? When we write a function, we can usually imagine what it should do: which parameters give which results. @@ -20,15 +18,15 @@ For instance, we're creating a function `f`. Wrote some code, testing: `f(1)` wo That's very typical. When we develop something, we keep a lot of possible use cases in mind. But it's hard to expect a programmer to check all of them manually after every change. So it becomes easy to fix one thing and break another one. -**Automated testing means that tests are written separately, in addition to the code. They can be executed easily and check all the main use cases.** +**Automated testing means that tests are written separately, in addition to the code. They run our functions in various ways and compare results with the expected.** ## Behavior Driven Development (BDD) -Let's use a technique named [Behavior Driven Development](http://en.wikipedia.org/wiki/Behavior-driven_development) or, in short, BDD. That approach is used among many projects. BDD is not just about testing. That's more. +Let's start with a technique named [Behavior Driven Development](http://en.wikipedia.org/wiki/Behavior-driven_development) or, in short, BDD. **BDD is three things in one: tests AND documentation AND examples.** -Enough words. Let's see the example. +To understand BDD, we'll examine a practical case of development. ## Development of "pow": the spec @@ -38,7 +36,7 @@ That task is just an example: there's the `**` operator in JavaScript that can d Before creating the code of `pow`, we can imagine what the function should do and describe it. -Such description is called a *specification* or, in short, a spec, and looks like this: +Such description is called a *specification* or, in short, a spec, and contains descriptions of use cases together with tests for them, like this: ```js describe("pow", function() { @@ -53,17 +51,17 @@ describe("pow", function() { A spec has three main building blocks that you can see above: `describe("title", function() { ... })` -: What functionality we're describing. Uses to group "workers" -- the `it` blocks. In our case we're describing the function `pow`. +: What functionality we're describing? In our case we're describing the function `pow`. Used to group "workers" -- the `it` blocks. -`it("title", function() { ... })` +`it("use case description", function() { ... })` : In the title of `it` we *in a human-readable way* describe the particular use case, and the second argument is a function that tests it. `assert.equal(value1, value2)` : The code inside `it` block, if the implementation is correct, should execute without errors. - Functions `assert.*` are used to check whether `pow` works as expected. Right here we're using one of them -- `assert.equal`, it compares arguments and yields an error if they are not equal. Here it checks that the result of `pow(2, 3)` equals `8`. + Functions `assert.*` are used to check whether `pow` works as expected. Right here we're using one of them -- `assert.equal`, it compares arguments and yields an error if they are not equal. Here it checks that the result of `pow(2, 3)` equals `8`. There are other types of comparisons and checks, that we'll add later. - There are other types of comparisons and checks that we'll see further. +The specification can be executed, and it will run the test specified in `it` block. We'll see that later. ## The development flow @@ -71,7 +69,7 @@ The flow of development usually looks like this: 1. An initial spec is written, with tests for the most basic functionality. 2. An initial implementation is created. -3. To check whether it works, we run the testing framework [Mocha](http://mochajs.org/) (more details soon) that runs the spec. Errors are displayed. We make corrections until everything works. +3. To check whether it works, we run the testing framework [Mocha](https://mochajs.org/) (more details soon) that runs the spec. While the functionality is not complete, errors are displayed. We make corrections until everything works. 4. Now we have a working initial implementation with tests. 5. We add more use cases to the spec, probably not yet supported by the implementations. Tests start to fail. 6. Go to 3, update the implementation till tests give no errors. @@ -79,15 +77,17 @@ The flow of development usually looks like this: So, the development is *iterative*. We write the spec, implement it, make sure tests pass, then write more tests, make sure they work etc. At the end we have both a working implementation and tests for it. -In our case, the first step is complete: we have an initial spec for `pow`. So let's make an implementation. But before that let's make a "zero" run of the spec, just to see that tests are working (they will all fail). +Let's see this development flow in our practical case. + +The first step is already complete: we have an initial spec for `pow`. Now, before making the implementation, let's use a few JavaScript libraries to run the tests, just to see that they are working (they will all fail). ## The spec in action Here in the tutorial we'll be using the following JavaScript libraries for tests: -- [Mocha](http://mochajs.org/) -- the core framework: it provides common testing functions including `describe` and `it` and the main function that runs tests. -- [Chai](http://chaijs.com) -- the library with many assertions. It allows to use a lot of different assertions, for now we need only `assert.equal`. -- [Sinon](http://sinonjs.org/) -- a library to spy over functions, emulate built-in functions and more, we'll need it much later. +- [Mocha](https://mochajs.org/) -- the core framework: it provides common testing functions including `describe` and `it` and the main function that runs tests. +- [Chai](https://www.chaijs.com/) -- the library with many assertions. It allows to use a lot of different assertions, for now we need only `assert.equal`. +- [Sinon](https://sinonjs.org/) -- a library to spy over functions, emulate built-in functions and more, we'll need it much later. These libraries are suitable for both in-browser and server-side testing. Here we'll consider the browser variant. @@ -96,7 +96,7 @@ The full HTML page with these frameworks and `pow` spec: ```html src="index.html" ``` -The page can be divided into four parts: +The page can be divided into five parts: 1. The `` -- add third-party libraries and styles for tests. 2. The ` diff --git a/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js index 9a2f8fde7e..c803f0e619 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js @@ -4,8 +4,8 @@ describe("pow", function() { assert.equal(pow(2, 3), 8); }); - it("3 raised to power 3 is 27", function() { - assert.equal(pow(3, 3), 27); + it("3 raised to power 4 is 81", function() { + assert.equal(pow(3, 4), 81); }); }); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js index 10a032d03a..e5ce2ce433 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/pow-4.view/test.js @@ -1,6 +1,6 @@ describe("pow", function() { - describe("raises x to power n", function() { + describe("raises x to power 3", function() { function makeTest(x) { let expected = x * x * x; 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 a5a3459795..75ff5e99fd 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 @@ -1,6 +1,6 @@ describe("pow", function() { - describe("raises x to power n", function() { + describe("raises x to power 3", function() { function makeTest(x) { let expected = x * x * x; diff --git a/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js index a5a3459795..75ff5e99fd 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/pow-nan.view/test.js @@ -1,6 +1,6 @@ describe("pow", function() { - describe("raises x to power n", function() { + describe("raises x to power 3", function() { function makeTest(x) { let expected = x * x * x; diff --git a/1-js/03-code-quality/06-polyfills/article.md b/1-js/03-code-quality/06-polyfills/article.md index 907730fdc8..19404b7b5f 100644 --- a/1-js/03-code-quality/06-polyfills/article.md +++ b/1-js/03-code-quality/06-polyfills/article.md @@ -1,57 +1,91 @@ -# Polyfills +# Polyfills and transpilers -The JavaScript language steadily evolves. New proposals to the language appear regularly, they are analyzed and, if considered worthy, are appended to the list at and then progress to the [specification](http://www.ecma-international.org/publications/standards/Ecma-262.htm). +The JavaScript language steadily evolves. New proposals to the language appear regularly, they are analyzed and, if considered worthy, are appended to the list at and then progress to the [specification](https://www.ecma-international.org/publications-and-standards/standards/ecma-262/). Teams behind JavaScript engines have their own ideas about what to implement first. They may decide to implement proposals that are in draft and postpone things that are already in the spec, because they are less interesting or just harder to do. -So it's quite common for an engine to implement only the part of the standard. +So it's quite common for an engine to implement only part of the standard. -A good page to see the current state of support for language features is (it's big, we have a lot to study yet). +A good page to see the current state of support for language features is (it's big, we have a lot to study yet). -## Babel +As programmers, we'd like to use most recent features. The more good stuff - the better! -When we use modern features of the language, some engines may fail to support such code. Just as said, not all features are implemented everywhere. +On the other hand, how to make our modern code work on older engines that don't understand recent features yet? -Here Babel comes to the rescue. +There are two tools for that: -[Babel](https://babeljs.io) is a [transpiler](https://en.wikipedia.org/wiki/Source-to-source_compiler). It rewrites modern JavaScript code into the previous standard. +1. Transpilers. +2. Polyfills. -Actually, there are two parts in Babel: +Here, in this chapter, our purpose is to get the gist of how they work, and their place in web development. -1. First, the transpiler program, which rewrites the code. The developer runs it on their own computer. It rewrites the code into the older standard. And then the code is delivered to the website for users. Modern project build system like [webpack](http://webpack.github.io/) or [brunch](http://brunch.io/) provide means to run transpiler automatically on every code change, so that doesn't involve any time loss from our side. +## Transpilers -2. Second, the polyfill. +A [transpiler](https://en.wikipedia.org/wiki/Source-to-source_compiler) is a special piece of software that translates source code to another source code. It can parse ("read and understand") modern code and rewrite it using older syntax constructs, so that it'll also work in outdated engines. - The transpiler rewrites the code, so syntax features are covered. But for new functions we need to write a special script that implements them. JavaScript is a highly dynamic language, scripts may not just add new functions, but also modify built-in ones, so that they behave according to the modern standard. +E.g. JavaScript before year 2020 didn't have the "nullish coalescing operator" `??`. So, if a visitor uses an outdated browser, it may fail to understand the code like `height = height ?? 100`. - There's a term "polyfill" for scripts that "fill in" the gap and add missing implementations. +A transpiler would analyze our code and rewrite `height ?? 100` into `(height !== undefined && height !== null) ? height : 100`. - Two interesting polyfills are: - - [babel polyfill](https://babeljs.io/docs/usage/polyfill/) that supports a lot, but is big. - - [polyfill.io](http://polyfill.io) service that allows to load/construct polyfills on-demand, depending on the features we need. +```js +// before running the transpiler +height = height ?? 100; -So, we need to setup the transpiler and add the polyfill for old engines to support modern features. +// after running the transpiler +height = (height !== undefined && height !== null) ? height : 100; +``` -If we orient towards modern engines and do not use features except those supported everywhere, then we don't need to use Babel. +Now the rewritten code is suitable for older JavaScript engines. -## Examples in the tutorial +Usually, a developer runs the transpiler on their own computer, and then deploys the transpiled code to the server. +Speaking of names, [Babel](https://babeljs.io) is one of the most prominent transpilers out there. -````online -Most examples are runnable at-place, like this: +Modern project build systems, such as [webpack](https://webpack.js.org/), provide a means to run a transpiler automatically on every code change, so it's very easy to integrate into the development process. -```js run -alert('Press the "Play" button in the upper-right corner to run'); -``` +## Polyfills + +New language features may include not only syntax constructs and operators, but also built-in functions. + +For example, `Math.trunc(n)` is a function that "cuts off" the decimal part of a number, e.g `Math.trunc(1.23)` returns `1`. -Examples that use modern JS will work only if your browser supports it. -```` +In some (very outdated) JavaScript engines, there's no `Math.trunc`, so such code will fail. -```offline -As you're reading the offline version, examples are not runnable. But they usually work :) +As we're talking about new functions, not syntax changes, there's no need to transpile anything here. We just need to declare the missing function. + +A script that updates/adds new functions is called "polyfill". It "fills in" the gap and adds missing implementations. + +For this particular case, the polyfill for `Math.trunc` is a script that implements it, like this: + +```js +if (!Math.trunc) { // if no such function + // implement it + Math.trunc = function(number) { + // Math.ceil and Math.floor exist even in ancient JavaScript engines + // they are covered later in the tutorial + return number < 0 ? Math.ceil(number) : Math.floor(number); + }; +} ``` -[Chrome Canary](https://www.google.com/chrome/browser/canary.html) is good for all examples, but other modern browsers are mostly fine too. +JavaScript is a highly dynamic language. Scripts may add/modify any function, even built-in ones. + +Two interesting polyfill libraries are: +- [core js](https://github.com/zloirock/core-js) that supports a lot, allows to include only needed features. + + +## Summary + +In this chapter we'd like to motivate you to study modern and even "bleeding-edge" language features, even if they aren't yet well-supported by JavaScript engines. + +Just don't forget to use a transpiler (if using modern syntax or operators) and polyfills (to add functions that may be missing). They'll ensure that the code works. + +For example, later when you're familiar with JavaScript, you can setup a code build system based on [webpack](https://webpack.js.org/) with the [babel-loader](https://github.com/babel/babel-loader) plugin. + +Good resources that show the current state of support for various features: +- - for pure JavaScript. +- - for browser-related functions. + +P.S. Google Chrome is usually the most up-to-date with language features, try it if a tutorial demo fails. Most tutorial demos work with any modern browser though. -Note that on production we can use Babel to translate the code into suitable for less recent browsers, so there will be no such limitation, the code will run everywhere. diff --git a/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js b/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js index e7f63284f9..db3283e49a 100644 --- a/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js +++ b/1-js/04-object-basics/01-object/3-is-empty/_js.view/solution.js @@ -1,7 +1,7 @@ function isEmpty(obj) { for (let key in obj) { - // if the loop has started, there is a prorty + // if the loop has started, there is a property return false; } return true; -} \ No newline at end of file +} diff --git a/1-js/04-object-basics/01-object/3-is-empty/solution.md b/1-js/04-object-basics/01-object/3-is-empty/solution.md index b3f40e3d22..b876973b5e 100644 --- a/1-js/04-object-basics/01-object/3-is-empty/solution.md +++ b/1-js/04-object-basics/01-object/3-is-empty/solution.md @@ -1,10 +1 @@ Just loop over the object and `return false` immediately if there's at least one property. - -```js -function isEmpty(obj) { - for (let key in obj) { - return false; - } - return true; -} -``` diff --git a/1-js/04-object-basics/01-object/4-const-object/solution.md b/1-js/04-object-basics/01-object/4-const-object/solution.md deleted file mode 100644 index f73c2f92bb..0000000000 --- a/1-js/04-object-basics/01-object/4-const-object/solution.md +++ /dev/null @@ -1,19 +0,0 @@ -Sure, it works, no problem. - -The `const` only protects the variable itself from changing. - -In other words, `user` stores a reference to the object. And it can't be changed. But the content of the object can. - -```js run -const user = { - name: "John" -}; - -*!* -// works -user.name = "Pete"; -*/!* - -// error -user = 123; -``` diff --git a/1-js/04-object-basics/01-object/4-const-object/task.md b/1-js/04-object-basics/01-object/4-const-object/task.md deleted file mode 100644 index 95f7a64dea..0000000000 --- a/1-js/04-object-basics/01-object/4-const-object/task.md +++ /dev/null @@ -1,18 +0,0 @@ -importance: 5 - ---- - -# Constant objects? - -Is it possible to change an object declared with `const`, how do you think? - -```js -const user = { - name: "John" -}; - -*!* -// does it work? -user.name = "Pete"; -*/!* -``` diff --git a/1-js/04-object-basics/01-object/8-multiply-numeric/task.md b/1-js/04-object-basics/01-object/8-multiply-numeric/task.md index 33eb89220c..6878ca0881 100644 --- a/1-js/04-object-basics/01-object/8-multiply-numeric/task.md +++ b/1-js/04-object-basics/01-object/8-multiply-numeric/task.md @@ -2,9 +2,9 @@ importance: 3 --- -# Multiply numeric properties by 2 +# Multiply numeric property values by 2 -Create a function `multiplyNumeric(obj)` that multiplies all numeric properties of `obj` by `2`. +Create a function `multiplyNumeric(obj)` that multiplies all numeric property values of `obj` by `2`. For instance: diff --git a/1-js/04-object-basics/01-object/article.md b/1-js/04-object-basics/01-object/article.md index 3014f89608..0fe5979fa6 100644 --- a/1-js/04-object-basics/01-object/article.md +++ b/1-js/04-object-basics/01-object/article.md @@ -1,7 +1,7 @@ # Objects -As we know from the chapter , there are seven language types in JavaScript. Six of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever). +As we know from the chapter , there are eight data types in JavaScript. Seven of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store keyed collections of various data and more complex entities. In JavaScript, objects penetrate almost every aspect of the language. So we must understand them first before going in-depth anywhere else. @@ -9,7 +9,7 @@ An object can be created with figure brackets `{…}` with an optional list of * We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the key. It's easy to find a file by its name or add/remove a file. -![](object.png) +![](object.svg) An empty object ("empty cabinet") can be created using one of two syntaxes: @@ -18,7 +18,7 @@ let user = new Object(); // "object constructor" syntax let user = {}; // "object literal" syntax ``` -![](object-user-empty.png) +![](object-user-empty.svg) Usually, the figure brackets `{...}` are used. That declaration is called an *object literal*. @@ -42,14 +42,14 @@ In the `user` object, there are two properties: The resulting `user` object can be imagined as a cabinet with two signed files labeled "name" and "age". -![user object](object-user.png) +![user object](object-user.svg) -We can add, remove and read files from it any time. +We can add, remove and read files from it at any time. Property values are accessible using the dot notation: ```js -// get fields of the object: +// get property values of the object: alert( user.name ); // John alert( user.age ); // 30 ``` @@ -60,15 +60,15 @@ The value can be of any type. Let's add a boolean one: user.isAdmin = true; ``` -![user object 2](object-user-isadmin.png) +![user object 2](object-user-isadmin.svg) -To remove a property, we can use `delete` operator: +To remove a property, we can use the `delete` operator: ```js delete user.age; ``` -![user object 3](object-user-delete.png) +![user object 3](object-user-delete.svg) We can also use multiword property names, but then they must be quoted: @@ -80,7 +80,7 @@ let user = { }; ``` -![](object-user-props.png) +![](object-user-props.svg) The last property in the list may end with a comma: @@ -101,10 +101,11 @@ For multiword properties, the dot access doesn't work: user.likes birds = true ``` -That's because the dot requires the key to be a valid variable identifier. That is: no spaces and other limitations. +JavaScript doesn't understand that. It thinks that we address `user.likes`, and then gives a syntax error when comes across unexpected `birds`. -There's an alternative "square bracket notation" that works with any string: +The dot requires the key to be a valid variable identifier. That implies: contains no spaces, doesn't start with a digit and doesn't include special characters (`$` and `_` are allowed). +There's an alternative "square bracket notation" that works with any string: ```js run let user = {}; @@ -130,7 +131,7 @@ let key = "likes birds"; user[key] = true; ``` -Here, the variable `key` may be calculated at run-time or depend on the user input. And then we use it to access the property. That gives us a great deal of flexibility. The dot notation cannot be used in a similar way. +Here, the variable `key` may be calculated at run-time or depend on the user input. And then we use it to access the property. That gives us a great deal of flexibility. For instance: @@ -146,10 +147,21 @@ let key = prompt("What do you want to know about the user?", "name"); alert( user[key] ); // John (if enter "name") ``` +The dot notation cannot be used in a similar way: + +```js run +let user = { + name: "John", + age: 30 +}; + +let key = "name"; +alert( user.key ) // undefined +``` ### Computed properties -We can use square brackets in an object literal. That's called *computed properties*. +We can use square brackets in an object literal, when creating an object. That's called *computed properties*. For instance: @@ -189,49 +201,13 @@ let bag = { }; ``` -Square brackets are much more powerful than the dot notation. They allow any property names and variables. But they are also more cumbersome to write. +Square brackets are much more powerful than dot notation. They allow any property names and variables. But they are also more cumbersome to write. So most of the time, when property names are known and simple, the dot is used. And if we need something more complex, then we switch to square brackets. - - -````smart header="Reserved words are allowed as property names" -A variable cannot have a name equal to one of language-reserved words like "for", "let", "return" etc. - -But for an object property, there's no such restriction. Any name is fine: - -```js run -let obj = { - for: 1, - let: 2, - return: 3 -} - -alert( obj.for + obj.let + obj.return ); // 6 -``` - -Basically, any name is allowed, but there's a special one: `"__proto__"` that gets special treatment for historical reasons. For instance, we can't set it to a non-object value: - -```js run -let obj = {}; -obj.__proto__ = 5; -alert(obj.__proto__); // [object Object], didn't work as intended -``` - -As we see from the code, the assignment to a primitive `5` is ignored. - -That can become a source of bugs and even vulnerabilies if we intent to store arbitrary key-value pairs in an object, and allow a visitor to specify the keys. - -In that case the visitor may choose "__proto__" as the key, and the assignment logic will be ruined (as shown above). - -There is a way to make objects treat `__proto__` as a regular property, which we'll cover later, but first we need to know more about objects. -There's also another data structure [Map](info:map-set-weakmap-weakset), that we'll learn in the chapter , which supports arbitrary keys. -```` - - ## Property value shorthand -In real code we often use existing variables as values for property names. +In real code, we often use existing variables as values for property names. For instance: @@ -239,7 +215,7 @@ For instance: function makeUser(name, age) { return { name: name, - age: age + age: age, // ...other properties }; } @@ -257,7 +233,7 @@ function makeUser(name, age) { *!* return { name, // same as name: name - age // same as age: age + age, // same as age: age // ... }; */!* @@ -273,9 +249,57 @@ let user = { }; ``` -## Existence check -A notable objects feature is that it's possible to access any property. There will be no error if the property doesn't exist! Accessing a non-existing property just returns `undefined`. It provides a very common way to test whether the property exists -- to get it and compare vs undefined: +## Property names limitations + +As we already know, a variable cannot have a name equal to one of the language-reserved words like "for", "let", "return" etc. + +But for an object property, there's no such restriction: + +```js run +// these properties are all right +let obj = { + for: 1, + let: 2, + return: 3 +}; + +alert( obj.for + obj.let + obj.return ); // 6 +``` + +In short, there are no limitations on property names. They can be any strings or symbols (a special type for identifiers, to be covered later). + +Other types are automatically converted to strings. + +For instance, a number `0` becomes a string `"0"` when used as a property key: + +```js run +let obj = { + 0: "test" // same as "0": "test" +}; + +// both alerts access the same property (the number 0 is converted to string "0") +alert( obj["0"] ); // test +alert( obj[0] ); // test (same property) +``` + +There's a minor gotcha with a special property named `__proto__`. We can't set it to a non-object value: + +```js run +let obj = {}; +obj.__proto__ = 5; // assign a number +alert(obj.__proto__); // [object Object] - the value is an object, didn't work as intended +``` + +As we see from the code, the assignment to a primitive `5` is ignored. + +We'll cover the special nature of `__proto__` in [subsequent chapters](info:prototype-inheritance), and suggest the [ways to fix](info:prototype-methods) such behavior. + +## Property existence test, "in" operator + +A notable feature of objects in JavaScript, compared to many other languages, is that it's possible to access any property. There will be no error if the property doesn't exist! + +Reading a non-existing property just returns `undefined`. So we can easily test whether the property exists: ```js run let user = {}; @@ -283,7 +307,7 @@ let user = {}; alert( user.noSuchProperty === undefined ); // true means "no such property" ``` -There also exists a special operator `"in"` to check for the existence of a property. +There's also a special operator `"in"` for that. The syntax is: ```js @@ -301,17 +325,18 @@ alert( "blabla" in user ); // false, user.blabla doesn't exist Please note that on the left side of `in` there must be a *property name*. That's usually a quoted string. -If we omit quotes, that would mean a variable containing the actual name to be tested. For instance: +If we omit quotes, that means a variable should contain the actual name to be tested. For instance: ```js run let user = { age: 30 }; let key = "age"; -alert( *!*key*/!* in user ); // true, takes the name from key and checks for such property +alert( *!*key*/!* in user ); // true, property "age" exists ``` -````smart header="Using \"in\" for properties that store `undefined`" -Usually, the strict comparison `"=== undefined"` check works fine. But there's a special case when it fails, but `"in"` works correctly. +Why does the `in` operator exist? Isn't it enough to compare against `undefined`? + +Well, most of the time the comparison with `undefined` works fine. But there's a special case when it fails, but `"in"` works correctly. It's when an object property exists, but stores `undefined`: @@ -325,21 +350,19 @@ alert( obj.test ); // it's undefined, so - no such property? alert( "test" in obj ); // true, the property does exist! ``` - In the code above, the property `obj.test` technically exists. So the `in` operator works right. -Situations like this happen very rarely, because `undefined` is usually not assigned. We mostly use `null` for "unknown" or "empty" values. So the `in` operator is an exotic guest in the code. -```` +Situations like this happen very rarely, because `undefined` should not be explicitly assigned. We mostly use `null` for "unknown" or "empty" values. So the `in` operator is an exotic guest in the code. -## The "for..in" loop +## The "for..in" loop [#forin] To walk over all keys of an object, there exists a special form of the loop: `for..in`. This is a completely different thing from the `for(;;)` construct that we studied before. The syntax: ```js -for(key in object) { +for (key in object) { // executes the body for each key among object properties } ``` @@ -353,7 +376,7 @@ let user = { isAdmin: true }; -for(let key in user) { +for (let key in user) { // keys alert( key ); // name, age, isAdmin // values for the keys @@ -363,8 +386,7 @@ for(let key in user) { Note that all "for" constructs allow us to declare the looping variable inside the loop, like `let key` here. -Also, we could use another variable name here instead of `key`. For instance, `"for(let prop in obj)"` is also widely used. - +Also, we could use another variable name here instead of `key`. For instance, `"for (let prop in obj)"` is also widely used. ### Ordered like an object @@ -384,13 +406,13 @@ let codes = { }; *!* -for(let code in codes) { +for (let code in codes) { alert(code); // 1, 41, 44, 49 } */!* ``` -The object may be used to suggest a list of options to the user. If we're making a site mainly for German audience then we probably want `49` to be the first. +The object may be used to suggest a list of options to the user. If we're making a site mainly for a German audience then we probably want `49` to be the first. But if we run the code, we see a totally different picture: @@ -402,9 +424,10 @@ The phone codes go in the ascending sorted order, because they are integers. So ````smart header="Integer properties? What's that?" The "integer property" term here means a string that can be converted to-and-from an integer without a change. -So, "49" is an integer property name, because when it's transformed to an integer number and back, it's still the same. But "+49" and "1.2" are not: +So, `"49"` is an integer property name, because when it's transformed to an integer number and back, it's still the same. But `"+49"` and `"1.2"` are not: ```js run +// Number(...) explicitly converts to a number // Math.trunc is a built-in function that removes the decimal part alert( String(Math.trunc(Number("49"))) ); // "49", same, integer property alert( String(Math.trunc(Number("+49"))) ); // "49", not same "+49" ⇒ not integer property @@ -442,269 +465,13 @@ let codes = { "+1": "USA" }; -for(let code in codes) { +for (let code in codes) { alert( +code ); // 49, 41, 44, 1 } ``` Now it works as intended. -## Copying by reference - -One of the fundamental differences of objects vs primitives is that they are stored and copied "by reference". - -Primitive values: strings, numbers, booleans -- are assigned/copied "as a whole value". - -For instance: - -```js -let message = "Hello!"; -let phrase = message; -``` - -As a result we have two independent variables, each one is storing the string `"Hello!"`. - -![](variable-copy-value.png) - -Objects are not like that. - -**A variable stores not the object itself, but its "address in memory", in other words "a reference" to it.** - -Here's the picture for the object: - -```js -let user = { - name: "John" -}; -``` - -![](variable-contains-reference.png) - -Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it. - -**When an object variable is copied -- the reference is copied, the object is not duplicated.** - -If we imagine an object as a cabinet, then a variable is a key to it. Copying a variable duplicates the key, but not the cabinet itself. - -For instance: - -```js no-beautify -let user = { name: "John" }; - -let admin = user; // copy the reference -``` - -Now we have two variables, each one with the reference to the same object: - -![](variable-copy-reference.png) - -We can use any variable to access the cabinet and modify its contents: - -```js run -let user = { name: 'John' }; - -let admin = user; - -*!* -admin.name = 'Pete'; // changed by the "admin" reference -*/!* - -alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference -``` - -The example above demonstrates that there is only one object. As if we had a cabinet with two keys and used one of them (`admin`) to get into it. Then, if we later use the other key (`user`) we would see changes. - -### Comparison by reference - -The equality `==` and strict equality `===` operators for objects work exactly the same. - -**Two objects are equal only if they are the same object.** - -For instance, two variables reference the same object, they are equal: - -```js run -let a = {}; -let b = a; // copy the reference - -alert( a == b ); // true, both variables reference the same object -alert( a === b ); // true -``` - -And here two independent objects are not equal, even though both are empty: - -```js run -let a = {}; -let b = {}; // two independent objects - -alert( a == b ); // false -``` - -For comparisons like `obj1 > obj2` or for a comparison against a primitive `obj == 5`, objects are converted to primitives. We'll study how object conversions work very soon, but to tell the truth, such comparisons are necessary very rarely and usually are a result of a coding mistake. - -### Const object - -An object declared as `const` *can* be changed. - -For instance: - -```js run -const user = { - name: "John" -}; - -*!* -user.age = 25; // (*) -*/!* - -alert(user.age); // 25 -``` - -It might seem that the line `(*)` would cause an error, but no, there's totally no problem. That's because `const` fixes the value of `user` itself. And here `user` stores the reference to the same object all the time. The line `(*)` goes *inside* the object, it doesn't reassign `user`. - -The `const` would give an error if we try to set `user` to something else, for instance: - -```js run -const user = { - name: "John" -}; - -*!* -// Error (can't reassign user) -*/!* -user = { - name: "Pete" -}; -``` - -...But what if we want to make constant object properties? So that `user.age = 25` would give an error. That's possible too. We'll cover it in the chapter . - -## Cloning and merging, Object.assign - -So, copying an object variable creates one more reference to the same object. - -But what if we need to duplicate an object? Create an independent copy, a clone? - -That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. Actually, that's rarely needed. Copying by reference is good most of the time. - -But if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level. - -Like this: - -```js run -let user = { - name: "John", - age: 30 -}; - -*!* -let clone = {}; // the new empty object - -// let's copy all user properties into it -for (let key in user) { - clone[key] = user[key]; -} -*/!* - -// now clone is a fully independant clone -clone.name = "Pete"; // changed the data in it - -alert( user.name ); // still John in the original object -``` - -Also we can use the method [Object.assign](mdn:js/Object/assign) for that. - -The syntax is: - -```js -Object.assign(dest[, src1, src2, src3...]) -``` - -- Arguments `dest`, and `src1, ..., srcN` (can be as many as needed) are objects. -- It copies the properties of all objects `src1, ..., srcN` into `dest`. In other words, properties of all arguments starting from the 2nd are copied into the 1st. Then it returns `dest`. - -For instance, we can use it to merge several objects into one: -```js -let user = { name: "John" }; - -let permissions1 = { canView: true }; -let permissions2 = { canEdit: true }; - -*!* -// copies all properties from permissions1 and permissions2 into user -Object.assign(user, permissions1, permissions2); -*/!* - -// now user = { name: "John", canView: true, canEdit: true } -``` - -If the receiving object (`user`) already has the same named property, it will be overwritten: - -```js -let user = { name: "John" }; - -// overwrite name, add isAdmin -Object.assign(user, { name: "Pete", isAdmin: true }); - -// now user = { name: "Pete", isAdmin: true } -``` - -We also can use `Object.assign` to replace the loop for simple cloning: - -```js -let user = { - name: "John", - age: 30 -}; - -*!* -let clone = Object.assign({}, user); -*/!* -``` - -It copies all properties of `user` into the empty object and returns it. Actually, the same as the loop, but shorter. - -Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. What to do with them? - -Like this: -```js run -let user = { - name: "John", - sizes: { - height: 182, - width: 50 - } -}; - -alert( user.sizes.height ); // 182 -``` - -Now it's not enough to copy `clone.sizes = user.sizes`, because the `user.sizes` is an object, it will be copied by reference. So `clone` and `user` will share the same sizes: - -Like this: -```js run -let user = { - name: "John", - sizes: { - height: 182, - width: 50 - } -}; - -let clone = Object.assign({}, user); - -alert( user.sizes === clone.sizes ); // true, same object - -// user and clone share sizes -user.sizes.width++; // change a property from one place -alert(clone.sizes.width); // 51, see the result from the other one -``` - -To fix that, we should use the cloning loop that examines each value of `user[key]` and, if it's an object, then replicate its structure as well. That is called a "deep cloning". - -There's a standard algorithm for deep cloning that handles the case above and more complex cases, called the [Structured cloning algorithm](http://w3c.github.io/html/infrastructure.html#safe-passing-of-structured-data). In order not to reinvent the wheel, we can use a working implementation of it from the JavaScript library [lodash](https://lodash.com), the method is called [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). - - - ## Summary Objects are associative arrays with several special features. @@ -715,16 +482,12 @@ They store properties (key-value pairs), where: To access a property, we can use: - The dot notation: `obj.property`. -- Square brackets notation `obj["property"]`. Square brackets allow to take the key from a variable, like `obj[varWithKey]`. +- Square brackets notation `obj["property"]`. Square brackets allow taking the key from a variable, like `obj[varWithKey]`. Additional operators: - To delete a property: `delete obj.prop`. - To check if a property with the given key exists: `"key" in obj`. -- To iterate over an object: `for(let key in obj)` loop. - -Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object. All operations via copied references (like adding/removing properties) are performed on the same single object. - -To make a "real copy" (a clone) we can use `Object.assign` or [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). +- To iterate over an object: `for (let key in obj)` loop. What we've studied in this chapter is called a "plain object", or just `Object`. diff --git a/1-js/04-object-basics/01-object/object-user-delete.png b/1-js/04-object-basics/01-object/object-user-delete.png deleted file mode 100644 index 688158f9b0..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-delete.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-delete.svg b/1-js/04-object-basics/01-object/object-user-delete.svg new file mode 100644 index 0000000000..c5af7e7afc --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-delete.svg @@ -0,0 +1 @@ +nameisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-delete@2x.png b/1-js/04-object-basics/01-object/object-user-delete@2x.png deleted file mode 100644 index e1ef655414..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-delete@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-empty.png b/1-js/04-object-basics/01-object/object-user-empty.png deleted file mode 100644 index 483d072c6d..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-empty.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-empty.svg b/1-js/04-object-basics/01-object/object-user-empty.svg new file mode 100644 index 0000000000..99edb0269a --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-empty.svg @@ -0,0 +1 @@ +emptyuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-empty@2x.png b/1-js/04-object-basics/01-object/object-user-empty@2x.png deleted file mode 100644 index 8db894cb3b..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-empty@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.png b/1-js/04-object-basics/01-object/object-user-isadmin.png deleted file mode 100644 index 4e76eeb76a..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-isadmin.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.svg b/1-js/04-object-basics/01-object/object-user-isadmin.svg new file mode 100644 index 0000000000..e2cc0eaf15 --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-isadmin.svg @@ -0,0 +1 @@ +nameageisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-isadmin@2x.png b/1-js/04-object-basics/01-object/object-user-isadmin@2x.png deleted file mode 100644 index b409776903..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-isadmin@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-props.png b/1-js/04-object-basics/01-object/object-user-props.png deleted file mode 100644 index 2bfdfabdb3..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-props.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-props.svg b/1-js/04-object-basics/01-object/object-user-props.svg new file mode 100644 index 0000000000..b3d5c9b71b --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-props.svg @@ -0,0 +1 @@ +nameagelikes birdsuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-props@2x.png b/1-js/04-object-basics/01-object/object-user-props@2x.png deleted file mode 100644 index 4935b59ce8..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user-props@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user.png b/1-js/04-object-basics/01-object/object-user.png deleted file mode 100644 index 16179209f3..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user.svg b/1-js/04-object-basics/01-object/object-user.svg new file mode 100644 index 0000000000..f499fbc10f --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user.svg @@ -0,0 +1 @@ +nameageuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user@2x.png b/1-js/04-object-basics/01-object/object-user@2x.png deleted file mode 100644 index 7203895324..0000000000 Binary files a/1-js/04-object-basics/01-object/object-user@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object.png b/1-js/04-object-basics/01-object/object.png deleted file mode 100644 index f94d094a96..0000000000 Binary files a/1-js/04-object-basics/01-object/object.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object.svg b/1-js/04-object-basics/01-object/object.svg new file mode 100644 index 0000000000..47431a6e1f --- /dev/null +++ b/1-js/04-object-basics/01-object/object.svg @@ -0,0 +1 @@ +key1key2key3 \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object@2x.png b/1-js/04-object-basics/01-object/object@2x.png deleted file mode 100644 index 003c2f6eab..0000000000 Binary files a/1-js/04-object-basics/01-object/object@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-contains-reference.png b/1-js/04-object-basics/01-object/variable-contains-reference.png deleted file mode 100644 index d6e7fddff6..0000000000 Binary files a/1-js/04-object-basics/01-object/variable-contains-reference.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-contains-reference@2x.png b/1-js/04-object-basics/01-object/variable-contains-reference@2x.png deleted file mode 100644 index 145bad29af..0000000000 Binary files a/1-js/04-object-basics/01-object/variable-contains-reference@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.png b/1-js/04-object-basics/01-object/variable-copy-reference.png deleted file mode 100644 index 97510c4b21..0000000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-reference.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-reference@2x.png b/1-js/04-object-basics/01-object/variable-copy-reference@2x.png deleted file mode 100644 index a64238a52b..0000000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-reference@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-value.png b/1-js/04-object-basics/01-object/variable-copy-value.png deleted file mode 100644 index e21af09907..0000000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-value.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-value@2x.png b/1-js/04-object-basics/01-object/variable-copy-value@2x.png deleted file mode 100644 index 2f0b2f47d4..0000000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-value@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/article.md b/1-js/04-object-basics/02-garbage-collection/article.md deleted file mode 100644 index 27682ef40b..0000000000 --- a/1-js/04-object-basics/02-garbage-collection/article.md +++ /dev/null @@ -1,212 +0,0 @@ -# Garbage collection - -Memory management in JavaScript is performed automatically and invisibly to us. We create primitives, objects, functions... All that takes memory. - -What happens when something is not needed any more? How does the JavaScript engine discover it and clean it up? - -## Reachability - -The main concept of memory management in JavaScript is *reachability*. - -Simply put, "reachable" values are those that are accessible or usable somehow. They are guaranteed to be stored in memory. - -1. There's a base set of inherently reachable values, that cannot be deleted for obvious reasons. - - For instance: - - - Local variables and parameters of the current function. - - Variables and parameters for other functions on the current chain of nested calls. - - Global variables. - - (there are some other, internal ones as well) - - These values are called *roots*. - -2. Any other value is considered reachable if it's reachable from a root by a reference or by a chain of references. - - For instance, if there's an object in a local variable, and that object has a property referencing another object, that object is considered reachable. And those that it references are also reachable. Detailed examples to follow. - -There's a background process in the JavaScript engine that is called [garbage collector](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)). It monitors all objects and removes those that have become unreachable. - -## A simple example - -Here's the simplest example: - -```js -// user has a reference to the object -let user = { - name: "John" -}; -``` - -![](memory-user-john.png) - -Here the arrow depicts an object reference. The global variable `"user"` references the object `{name: "John"}` (we'll call it John for brevity). The `"name"` property of John stores a primitive, so it's painted inside the object. - -If the value of `user` is overwritten, the reference is lost: - -```js -user = null; -``` - -![](memory-user-john-lost.png) - -Now John becomes unreachable. There's no way to access it, no references to it. Garbage collector will junk the data and free the memory. - -## Two references - -Now let's imagine we copied the reference from `user` to `admin`: - -```js -// user has a reference to the object -let user = { - name: "John" -}; - -*!* -let admin = user; -*/!* -``` - -![](memory-user-john-admin.png) - -Now if we do the same: -```js -user = null; -``` - -...Then the object is still reachable via `admin` global variable, so it's in memory. If we overwrite `admin` too, then it can be removed. - -## Interlinked objects - -Now a more complex example. The family: - -```js -function marry(man, woman) { - woman.husband = man; - man.wife = woman; - - return { - father: man, - mother: woman - } -} - -let family = marry({ - name: "John" -}, { - name: "Ann" -}); -``` - -Function `marry` "marries" two objects by giving them references to each other and returns a new object that contains them both. - -The resulting memory structure: - -![](family.png) - -As of now, all objects are reachable. - -Now let's remove two references: - -```js -delete family.father; -delete family.mother.husband; -``` - -![](family-delete-refs.png) - -It's not enough to delete only one of these two references, because all objects would still be reachable. - -But if we delete both, then we can see that John has no incoming reference any more: - -![](family-no-father.png) - -Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible. - -After garbage collection: - -![](family-no-father-2.png) - -## Unreachable island - -It is possible that the whole island of interlinked objects becomes unreachable and is removed from the memory. - -The source object is the same as above. Then: - -```js -family = null; -``` - -The in-memory picture becomes: - -![](family-no-family.png) - -This example demonstrates how important the concept of reachability is. - -It's obvious that John and Ann are still linked, both have incoming references. But that's not enough. - -The former `"family"` object has been unlinked from the root, there's no reference to it any more, so the whole island becomes unreachable and will be removed. - -## Internal algorithms - -The basic garbage collection algorithm is called "mark-and-sweep". - -The following "garbage collection" steps are regularly performed: - -- The garbage collector takes roots and "marks" (remembers) them. -- Then it visits and "marks" all references from them. -- Then it visits marked objects and marks *their* references. All visited objects are remembered, so as not to visit the same object twice in the future. -- ...And so on until there are unvisited references (reachable from the roots). -- All objects except marked ones are removed. - -For instance, let our object structure look like this: - -![](garbage-collection-1.png) - -We can clearly see an "unreachable island" to the right side. Now let's see how "mark-and-sweep" garbage collector deals with it. - -The first step marks the roots: - -![](garbage-collection-2.png) - -Then their references are marked: - -![](garbage-collection-3.png) - -...And their references, while possible: - -![](garbage-collection-4.png) - -Now the objects that could not be visited in the process are considered unreachable and will be removed: - -![](garbage-collection-5.png) - -That's the concept of how garbage collection works. - -JavaScript engines apply many optimizations to make it run faster and not affect the execution. - -Some of the optimizations: - -- **Generational collection** -- objects are split into two sets: "new ones" and "old ones". Many objects appear, do their job and die fast, they can be cleaned up aggressively. Those that survive for long enough, become "old" and are examined less often. -- **Incremental collection** -- if there are many objects, and we try to walk and mark the whole object set at once, it may take some time and introduce visible delays in the execution. So the engine tries to split the garbage collection into pieces. Then the pieces are executed one by one, separately. That requires some extra bookkeeping between them to track changes, but we have many tiny delays instead of a big one. -- **Idle-time collection** -- the garbage collector tries to run only while the CPU is idle, to reduce the possible effect on the execution. - -There are other optimizations and flavours of garbage collection algorithms. As much as I'd like to describe them here, I have to hold off, because different engines implement different tweaks and techniques. And, what's even more important, things change as engines develop, so going deeper "in advance", without a real need is probably not worth that. Unless, of course, it is a matter of pure interest, then there will be some links for you below. - -## Summary - -The main things to know: - -- Garbage collection is performed automatically. We cannot force or prevent it. -- Objects are retained in memory while they are reachable. -- Being referenced is not the same as being reachable (from a root): a pack of interlinked objects can become unreachable as a whole. - -Modern engines implement advanced algorithms of garbage collection. - -A general book "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones et al) covers some of them. - -If you are familiar with low-level programming, the more detailed information about V8 garbage collector is in the article [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). - -[V8 blog](http://v8project.blogspot.com/) also publishes articles about changes in memory management from time to time. Naturally, to learn the garbage collection, you'd better prepare by learning about V8 internals in general and read the blog of [Vyacheslav Egorov](http://mrale.ph) who worked as one of V8 engineers. I'm saying: "V8", because it is best covered with articles in the internet. For other engines, many approaches are similar, but garbage collection differs in many aspects. - -In-depth knowledge of engines is good when you need low-level optimizations. It would be wise to plan that as the next step after you're familiar with the language. diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png deleted file mode 100644 index 5c10f0e47e..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png b/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png deleted file mode 100644 index 24d29630e5..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.png b/1-js/04-object-basics/02-garbage-collection/family-no-family.png deleted file mode 100644 index a4ce30a354..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-family.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png deleted file mode 100644 index 0d9949839e..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png b/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png deleted file mode 100644 index e24dba5b57..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png deleted file mode 100644 index a6c4ee36a3..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.png b/1-js/04-object-basics/02-garbage-collection/family-no-father.png deleted file mode 100644 index df14624bc0..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png deleted file mode 100644 index 5ab4b37920..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family.png b/1-js/04-object-basics/02-garbage-collection/family.png deleted file mode 100644 index dbbc01d2f5..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family@2x.png b/1-js/04-object-basics/02-garbage-collection/family@2x.png deleted file mode 100644 index 64b4619ba1..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png deleted file mode 100644 index 423191778e..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png deleted file mode 100644 index 223ea32a12..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png deleted file mode 100644 index da63d39693..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png deleted file mode 100644 index 1f614e3e6d..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png deleted file mode 100644 index e77144c1d7..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png deleted file mode 100644 index 37e349b627..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png deleted file mode 100644 index 110e0d9c4b..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png deleted file mode 100644 index c09d75f9d1..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png deleted file mode 100644 index bc4ea9670e..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png deleted file mode 100644 index 0ab697e683..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection.png deleted file mode 100644 index 013d0a48a4..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png deleted file mode 100644 index 681f29e4bd..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png deleted file mode 100644 index 29c4fcbea8..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png deleted file mode 100644 index 2f80f19a21..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png deleted file mode 100644 index cdc1d49049..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png deleted file mode 100644 index d58afdb582..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john.png deleted file mode 100644 index 3ba5730de9..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png deleted file mode 100644 index 5aa81bb0c0..0000000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-object-copy/article.md b/1-js/04-object-basics/02-object-copy/article.md new file mode 100644 index 0000000000..e80f748ab0 --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/article.md @@ -0,0 +1,325 @@ +# Object references and copying + +One of the fundamental differences of objects versus primitives is that objects are stored and copied "by reference", whereas primitive values: strings, numbers, booleans, etc -- are always copied "as a whole value". + +That's easy to understand if we look a bit under the hood of what happens when we copy a value. + +Let's start with a primitive, such as a string. + +Here we put a copy of `message` into `phrase`: + +```js +let message = "Hello!"; +let phrase = message; +``` + +As a result we have two independent variables, each one storing the string `"Hello!"`. + +![](variable-copy-value.svg) + +Quite an obvious result, right? + +Objects are not like that. + +**A variable assigned to an object stores not the object itself, but its "address in memory" -- in other words "a reference" to it.** + +Let's look at an example of such a variable: + +```js +let user = { + name: "John" +}; +``` + +And here's how it's actually stored in memory: + +![](variable-contains-reference.svg) + +The object is stored somewhere in memory (at the right of the picture), while the `user` variable (at the left) has a "reference" to it. + +We may think of an object variable, such as `user`, like a sheet of paper with the address of the object on it. + +When we perform actions with the object, e.g. take a property `user.name`, the JavaScript engine looks at what's at that address and performs the operation on the actual object. + +Now here's why it's important. + +**When an object variable is copied, the reference is copied, but the object itself is not duplicated.** + +For instance: + +```js no-beautify +let user = { name: "John" }; + +let admin = user; // copy the reference +``` + +Now we have two variables, each storing a reference to the same object: + +![](variable-copy-reference.svg) + +As you can see, there's still one object, but now with two variables that reference it. + +We can use either variable to access the object and modify its contents: + +```js run +let user = { name: 'John' }; + +let admin = user; + +*!* +admin.name = 'Pete'; // changed by the "admin" reference +*/!* + +alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference +``` + +It's as if we had a cabinet with two keys and used one of them (`admin`) to get into it and make changes. Then, if we later use another key (`user`), we are still opening the same cabinet and can access the changed contents. + +## Comparison by reference + +Two objects are equal only if they are the same object. + +For instance, here `a` and `b` reference the same object, thus they are equal: + +```js run +let a = {}; +let b = a; // copy the reference + +alert( a == b ); // true, both variables reference the same object +alert( a === b ); // true +``` + +And here two independent objects are not equal, even though they look alike (both are empty): + +```js run +let a = {}; +let b = {}; // two independent objects + +alert( a == b ); // false +``` + +For comparisons like `obj1 > obj2` or for a comparison against a primitive `obj == 5`, objects are converted to primitives. We'll study how object conversions work very soon, but to tell the truth, such comparisons are needed very rarely -- usually they appear as a result of a programming mistake. + +````smart header="Const objects can be modified" +An important side effect of storing objects as references is that an object declared as `const` *can* be modified. + +For instance: + +```js run +const user = { + name: "John" +}; + +*!* +user.name = "Pete"; // (*) +*/!* + +alert(user.name); // Pete +``` + +It might seem that the line `(*)` would cause an error, but it does not. The value of `user` is constant, it must always reference the same object, but properties of that object are free to change. + +In other words, the `const user` gives an error only if we try to set `user=...` as a whole. + +That said, if we really need to make constant object properties, it's also possible, but using totally different methods. We'll mention that in the chapter . +```` + +## Cloning and merging, Object.assign [#cloning-and-merging-object-assign] + +So, copying an object variable creates one more reference to the same object. + +But what if we need to duplicate an object? + +We can create a new object and replicate the structure of the existing one, by iterating over its properties and copying them on the primitive level. + +Like this: + +```js run +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = {}; // the new empty object + +// let's copy all user properties into it +for (let key in user) { + clone[key] = user[key]; +} +*/!* + +// now clone is a fully independent object with the same content +clone.name = "Pete"; // changed the data in it + +alert( user.name ); // still John in the original object +``` + +We can also use the method [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign). + +The syntax is: + +```js +Object.assign(dest, ...sources) +``` + +- The first argument `dest` is a target object. +- Further arguments is a list of source objects. + +It copies the properties of all source objects into the target `dest`, and then returns it as the result. + +For example, we have `user` object, let's add a couple of permissions to it: + +```js run +let user = { name: "John" }; + +let permissions1 = { canView: true }; +let permissions2 = { canEdit: true }; + +*!* +// copies all properties from permissions1 and permissions2 into user +Object.assign(user, permissions1, permissions2); +*/!* + +// now user = { name: "John", canView: true, canEdit: true } +alert(user.name); // John +alert(user.canView); // true +alert(user.canEdit); // true +``` + +If the copied property name already exists, it gets overwritten: + +```js run +let user = { name: "John" }; + +Object.assign(user, { name: "Pete" }); + +alert(user.name); // now user = { name: "Pete" } +``` + +We also can use `Object.assign` to perform a simple object cloning: + +```js run +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = Object.assign({}, user); +*/!* + +alert(clone.name); // John +alert(clone.age); // 30 +``` + +Here it copies all properties of `user` into the empty object and returns it. + +There are also other methods of cloning an object, e.g. using the [spread syntax](info:rest-parameters-spread) `clone = {...user}`, covered later in the tutorial. + +## Nested cloning + +Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. + +Like this: +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +alert( user.sizes.height ); // 182 +``` + +Now it's not enough to copy `clone.sizes = user.sizes`, because `user.sizes` is an object, and will be copied by reference, so `clone` and `user` will share the same sizes: + +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +let clone = Object.assign({}, user); + +alert( user.sizes === clone.sizes ); // true, same object + +// user and clone share sizes +user.sizes.width = 60; // change a property from one place +alert(clone.sizes.width); // 60, get the result from the other one +``` + +To fix that and make `user` and `clone` truly separate objects, we should use a cloning loop that examines each value of `user[key]` and, if it's an object, then replicate its structure as well. That is called a "deep cloning" or "structured cloning". There's [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) method that implements deep cloning. + + +### structuredClone + +The call `structuredClone(object)` clones the `object` with all nested properties. + +Here's how we can use it in our example: + +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +*!* +let clone = structuredClone(user); +*/!* + +alert( user.sizes === clone.sizes ); // false, different objects + +// user and clone are totally unrelated now +user.sizes.width = 60; // change a property from one place +alert(clone.sizes.width); // 50, not related +``` + +The `structuredClone` method can clone most data types, such as objects, arrays, primitive values. + +It also supports circular references, when an object property references the object itself (directly or via a chain or references). + +For instance: + +```js run +let user = {}; +// let's create a circular reference: +// user.me references the user itself +user.me = user; + +let clone = structuredClone(user); +alert(clone.me === clone); // true +``` + +As you can see, `clone.me` references the `clone`, not the `user`! So the circular reference was cloned correctly as well. + +Although, there are cases when `structuredClone` fails. + +For instance, when an object has a function property: + +```js run +// error +structuredClone({ + f: function() {} +}); +``` + +Function properties aren't supported. + +To handle such complex cases we may need to use a combination of cloning methods, write custom code or, to not reinvent the wheel, take an existing implementation, for instance [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) from the JavaScript library [lodash](https://lodash.com). + +## Summary + +Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object itself. + +All operations via copied references (like adding/removing properties) are performed on the same single object. + +To make a "real copy" (a clone) we can use `Object.assign` for the so-called "shallow copy" (nested objects are copied by reference) or a "deep cloning" function `structuredClone` or use a custom cloning implementation, such as [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). diff --git a/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg new file mode 100644 index 0000000000..267f045785 --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg @@ -0,0 +1 @@ +username \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg new file mode 100644 index 0000000000..a847fb200e --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg @@ -0,0 +1 @@ +useradminname \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/variable-copy-value.svg b/1-js/04-object-basics/02-object-copy/variable-copy-value.svg new file mode 100644 index 0000000000..0d6ca67bc6 --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/variable-copy-value.svg @@ -0,0 +1 @@ +"Hello!"message"Hello!"phrase \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/article.md b/1-js/04-object-basics/03-garbage-collection/article.md new file mode 100644 index 0000000000..1b576d6293 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/article.md @@ -0,0 +1,212 @@ +# Garbage collection + +Memory management in JavaScript is performed automatically and invisibly to us. We create primitives, objects, functions... All that takes memory. + +What happens when something is not needed any more? How does the JavaScript engine discover it and clean it up? + +## Reachability + +The main concept of memory management in JavaScript is *reachability*. + +Simply put, "reachable" values are those that are accessible or usable somehow. They are guaranteed to be stored in memory. + +1. There's a base set of inherently reachable values, that cannot be deleted for obvious reasons. + + For instance: + + - The currently executing function, its local variables and parameters. + - Other functions on the current chain of nested calls, their local variables and parameters. + - Global variables. + - (there are some other, internal ones as well) + + These values are called *roots*. + +2. Any other value is considered reachable if it's reachable from a root by a reference or by a chain of references. + + For instance, if there's an object in a global variable, and that object has a property referencing another object, *that* object is considered reachable. And those that it references are also reachable. Detailed examples to follow. + +There's a background process in the JavaScript engine that is called [garbage collector](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)). It monitors all objects and removes those that have become unreachable. + +## A simple example + +Here's the simplest example: + +```js +// user has a reference to the object +let user = { + name: "John" +}; +``` + +![](memory-user-john.svg) + +Here the arrow depicts an object reference. The global variable `"user"` references the object `{name: "John"}` (we'll call it John for brevity). The `"name"` property of John stores a primitive, so it's painted inside the object. + +If the value of `user` is overwritten, the reference is lost: + +```js +user = null; +``` + +![](memory-user-john-lost.svg) + +Now John becomes unreachable. There's no way to access it, no references to it. Garbage collector will junk the data and free the memory. + +## Two references + +Now let's imagine we copied the reference from `user` to `admin`: + +```js +// user has a reference to the object +let user = { + name: "John" +}; + +*!* +let admin = user; +*/!* +``` + +![](memory-user-john-admin.svg) + +Now if we do the same: +```js +user = null; +``` + +...Then the object is still reachable via `admin` global variable, so it must stay in memory. If we overwrite `admin` too, then it can be removed. + +## Interlinked objects + +Now a more complex example. The family: + +```js +function marry(man, woman) { + woman.husband = man; + man.wife = woman; + + return { + father: man, + mother: woman + } +} + +let family = marry({ + name: "John" +}, { + name: "Ann" +}); +``` + +Function `marry` "marries" two objects by giving them references to each other and returns a new object that contains them both. + +The resulting memory structure: + +![](family.svg) + +As of now, all objects are reachable. + +Now let's remove two references: + +```js +delete family.father; +delete family.mother.husband; +``` + +![](family-delete-refs.svg) + +It's not enough to delete only one of these two references, because all objects would still be reachable. + +But if we delete both, then we can see that John has no incoming reference any more: + +![](family-no-father.svg) + +Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible. + +After garbage collection: + +![](family-no-father-2.svg) + +## Unreachable island + +It is possible that the whole island of interlinked objects becomes unreachable and is removed from the memory. + +The source object is the same as above. Then: + +```js +family = null; +``` + +The in-memory picture becomes: + +![](family-no-family.svg) + +This example demonstrates how important the concept of reachability is. + +It's obvious that John and Ann are still linked, both have incoming references. But that's not enough. + +The former `"family"` object has been unlinked from the root, there's no reference to it any more, so the whole island becomes unreachable and will be removed. + +## Internal algorithms + +The basic garbage collection algorithm is called "mark-and-sweep". + +The following "garbage collection" steps are regularly performed: + +- The garbage collector takes roots and "marks" (remembers) them. +- Then it visits and "marks" all references from them. +- Then it visits marked objects and marks *their* references. All visited objects are remembered, so as not to visit the same object twice in the future. +- ...And so on until every reachable (from the roots) references are visited. +- All objects except marked ones are removed. + +For instance, let our object structure look like this: + +![](garbage-collection-1.svg) + +We can clearly see an "unreachable island" to the right side. Now let's see how "mark-and-sweep" garbage collector deals with it. + +The first step marks the roots: + +![](garbage-collection-2.svg) + +Then we follow their references and mark referenced objects: + +![](garbage-collection-3.svg) + +...And continue to follow further references, while possible: + +![](garbage-collection-4.svg) + +Now the objects that could not be visited in the process are considered unreachable and will be removed: + +![](garbage-collection-5.svg) + +We can also imagine the process as spilling a huge bucket of paint from the roots, that flows through all references and marks all reachable objects. The unmarked ones are then removed. + +That's the concept of how garbage collection works. JavaScript engines apply many optimizations to make it run faster and not introduce any delays into the code execution. + +Some of the optimizations: + +- **Generational collection** -- objects are split into two sets: "new ones" and "old ones". In typical code, many objects have a short life span: they appear, do their job and die fast, so it makes sense to track new objects and clear the memory from them if that's the case. Those that survive for long enough, become "old" and are examined less often. +- **Incremental collection** -- if there are many objects, and we try to walk and mark the whole object set at once, it may take some time and introduce visible delays in the execution. So the engine splits the whole set of existing objects into multiple parts. And then clear these parts one after another. There are many small garbage collections instead of a total one. That requires some extra bookkeeping between them to track changes, but we get many tiny delays instead of a big one. +- **Idle-time collection** -- the garbage collector tries to run only while the CPU is idle, to reduce the possible effect on the execution. + +There exist other optimizations and flavours of garbage collection algorithms. As much as I'd like to describe them here, I have to hold off, because different engines implement different tweaks and techniques. And, what's even more important, things change as engines develop, so studying deeper "in advance", without a real need is probably not worth that. Unless, of course, it is a matter of pure interest, then there will be some links for you below. + +## Summary + +The main things to know: + +- Garbage collection is performed automatically. We cannot force or prevent it. +- Objects are retained in memory while they are reachable. +- Being referenced is not the same as being reachable (from a root): a pack of interlinked objects can become unreachable as a whole, as we've seen in the example above. + +Modern engines implement advanced algorithms of garbage collection. + +A general book "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones et al) covers some of them. + +If you are familiar with low-level programming, more detailed information about V8's garbage collector is in the article [A tour of V8: Garbage Collection](https://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). + +The [V8 blog](https://v8.dev/) also publishes articles about changes in memory management from time to time. Naturally, to learn more about garbage collection, you'd better prepare by learning about V8 internals in general and read the blog of [Vyacheslav Egorov](https://mrale.ph) who worked as one of the V8 engineers. I'm saying: "V8", because it is best covered by articles on the internet. For other engines, many approaches are similar, but garbage collection differs in many aspects. + +In-depth knowledge of engines is good when you need low-level optimizations. It would be wise to plan that as the next step after you're familiar with the language. diff --git a/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg b/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg new file mode 100644 index 0000000000..a582ca64b7 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg @@ -0,0 +1 @@ +<global variable>ObjectObjectwifefamilyname: "John"name: "Ann"motherObjectfatherhusband \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family-no-family.svg b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg new file mode 100644 index 0000000000..c73dd6a48f --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg @@ -0,0 +1 @@ +<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg new file mode 100644 index 0000000000..6bd13c0e80 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg @@ -0,0 +1 @@ +Objectfamilyname: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family-no-father.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg new file mode 100644 index 0000000000..fd1f206073 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg @@ -0,0 +1 @@ +ObjectObjectwifefamilyname: "John"name: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family.svg b/1-js/04-object-basics/03-garbage-collection/family.svg new file mode 100644 index 0000000000..fd05348740 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family.svg @@ -0,0 +1 @@ +ObjectObjectfatherwifefamilyname: "John"name: "Ann"motherObjecthusband<global variable> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg new file mode 100644 index 0000000000..5cac52e9a9 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg new file mode 100644 index 0000000000..7dd3a693a8 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg new file mode 100644 index 0000000000..1060577874 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg new file mode 100644 index 0000000000..bd485adeeb --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg new file mode 100644 index 0000000000..2d85432bc5 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg @@ -0,0 +1 @@ +<global>unreachables \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg new file mode 100644 index 0000000000..1913243548 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg @@ -0,0 +1 @@ +username: "John"Objectadmin<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg new file mode 100644 index 0000000000..07914a9ca0 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg @@ -0,0 +1 @@ +name: "John"Objectuser: null<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg new file mode 100644 index 0000000000..15bd51afb1 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg @@ -0,0 +1 @@ +username: "John"Object<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/03-symbol/article.md deleted file mode 100644 index ba39894163..0000000000 --- a/1-js/04-object-basics/03-symbol/article.md +++ /dev/null @@ -1,273 +0,0 @@ - -# Symbol type - -By specification, object property keys may be either of string type, or of symbol type. Not numbers, not booleans, only strings or symbols, these two types. - -Till now we've only seen strings. Now let's see the advantages that symbols can give us. - -## Symbols - -"Symbol" value represents a unique identifier. - -A value of this type can be created using `Symbol()`: - -```js -// id is a new symbol -let id = Symbol(); -``` - -We can also give symbol a description (also called a symbol name), mostly useful for debugging purposes: - -```js -// id is a symbol with the description "id" -let id = Symbol("id"); -``` - -Symbols are guaranteed to be unique. Even if we create many symbols with the same description, they are different values. The description is just a label that doesn't affect anything. - -For instance, here are two symbols with the same description -- they are not equal: - -```js run -let id1 = Symbol("id"); -let id2 = Symbol("id"); - -*!* -alert(id1 == id2); // false -*/!* -``` - -If you are familiar with Ruby or another language that also has some sort of "symbols" -- please don't be misguided. JavaScript symbols are different. - -````warn header="Symbols don't auto-convert to a string" -Most values in JavaScript support implicit conversion to a string. For instance, we can `alert` almost any value, and it will work. Symbols are special. They don't auto-convert. - -For instance, this `alert` will show an error: - -```js run -let id = Symbol("id"); -*!* -alert(id); // TypeError: Cannot convert a Symbol value to a string -*/!* -``` - -If we really want to show a symbol, we need to call `.toString()` on it, like here: -```js run -let id = Symbol("id"); -*!* -alert(id.toString()); // Symbol(id), now it works -*/!* -``` - -That's a "language guard" against messing up, because strings and symbols are fundamentally different and should not occasionally convert one into another. -```` - -## "Hidden" properties - -Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. - -For instance, if we want to store an "identifier" for the object `user`, we can use a symbol as a key for it: - -```js run -let user = { name: "John" }; -let id = Symbol("id"); - -user[id] = "ID Value"; -alert( user[id] ); // we can access the data using the symbol as the key -``` - -What's the benefit over using `Symbol("id")` over a string `"id"`? - -Let's make the example a bit deeper to see that. - -Imagine that another script wants to have its own "id" property inside `user`, for its own purposes. That may be another JavaScript library, so the scripts are completely unaware of each other. - -Then that script can create its own `Symbol("id")`, like this: - -```js -// ... -let id = Symbol("id"); - -user[id] = "Their id value"; -``` - -There will be no conflict, because symbols are always different, even if they have the same name. - -Now note that if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: - -```js run -let user = { name: "John" }; - -// our script uses "id" property -user.id = "ID Value"; - -// ...if later another script the uses "id" for its purposes... - -user.id = "Their id value" -// boom! overwritten! it did not mean to harm the colleague, but did it! -``` - -### Symbols in a literal - -If we want to use a symbol in an object literal, we need square brackets. - -Like this: - -```js -let id = Symbol("id"); - -let user = { - name: "John", -*!* - [id]: 123 // not just "id: 123" -*/!* -}; -``` -That's because we need the value from the variable `id` as the key, not the string "id". - -### Symbols are skipped by for..in - -Symbolic properties do not participate in `for..in` loop. - -For instance: - -```js run -let id = Symbol("id"); -let user = { - name: "John", - age: 30, - [id]: 123 -}; - -*!* -for (let key in user) alert(key); // name, age (no symbols) -*/!* - -// the direct access by the symbol works -alert( "Direct: " + user[id] ); -``` - -That's a part of the general "hiding" concept. If another script or a library loops over our object, it won't unexpectedly access a symbolic property. - -In contrast, [Object.assign](mdn:js/Object/assign) copies both string and symbol properties: - -```js run -let id = Symbol("id"); -let user = { - [id]: 123 -}; - -let clone = Object.assign({}, user); - -alert( clone[id] ); // 123 -``` - -There's no paradox here. That's by design. The idea is that when we clone an object or merge objects, we usually want *all* properties to be copied (including symbols like `id`). - -````smart header="Property keys of other types are coerced to strings" -We can only use strings or symbols as keys in objects. Other types are converted to strings. - -For instance, a number `0` becomes a string `"0"` when used as a property key: - -```js run -let obj = { - 0: "test" // same as "0": "test" -}; - -// both alerts access the same property (the number 0 is converted to string "0") -alert( obj["0"] ); // test -alert( obj[0] ); // test (same property) -``` -```` - -## Global symbols - -As we've seen, usually all symbols are different, even if they have the same names. But sometimes we want same-named symbols to be same entities. - -For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. - -To achieve that, there exists a *global symbol registry*. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol. - -In order to create or read a symbol in the registry, use `Symbol.for(key)`. - -That call checks the global registry, and if there's a symbol described as `key`, then returns it, otherwise creates a new symbol `Symbol(key)` and stores it in the registry by the given `key`. - -For instance: - -```js run -// read from the global registry -let id = Symbol.for("id"); // if the symbol did not exist, it is created - -// read it again -let idAgain = Symbol.for("id"); - -// the same symbol -alert( id === idAgain ); // true -``` - -Symbols inside the registry are called *global symbols*. If we want an application-wide symbol, accessible everywhere in the code -- that's what they are for. - -```smart header="That sounds like Ruby" -In some programming languages, like Ruby, there's a single symbol per name. - -In JavaScript, as we can see, that's right for global symbols. -``` - -### Symbol.keyFor - -For global symbols, not only `Symbol.for(key)` returns a symbol by name, but there's a reverse call: `Symbol.keyFor(sym)`, that does the reverse: returns a name by a global symbol. - -For instance: - -```js run -let sym = Symbol.for("name"); -let sym2 = Symbol.for("id"); - -// get name from symbol -alert( Symbol.keyFor(sym) ); // name -alert( Symbol.keyFor(sym2) ); // id -``` - -The `Symbol.keyFor` internally uses the global symbol registry to look up the key for the symbol. So it doesn't work for non-global symbols. If the symbol is not global, it won't be able to find it and return `undefined`. - -For instance: - -```js run -alert( Symbol.keyFor(Symbol.for("name")) ); // name, global symbol - -alert( Symbol.keyFor(Symbol("name2")) ); // undefined, the argument isn't a global symbol -``` - -## System symbols - -There exist many "system" symbols that JavaScript uses internally, and we can use them to fine-tune various aspects of our objects. - -They are listed in the specification in the [Well-known symbols](https://tc39.github.io/ecma262/#sec-well-known-symbols) table: - -- `Symbol.hasInstance` -- `Symbol.isConcatSpreadable` -- `Symbol.iterator` -- `Symbol.toPrimitive` -- ...and so on. - -For instance, `Symbol.toPrimitive` allows us to describe object to primitive conversion. We'll see its use very soon. - -Other symbols will also become familiar when we study the corresponding language features. - -## Summary - -`Symbol` is a primitive type for unique identifiers. - -Symbols are created with `Symbol()` call with an optional description. - -Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` return exactly the same symbol. - -Symbols have two main use cases: - -1. "Hidden" object properties. - If we want to add a property into an object that "belongs" to another script or a library, we can create a symbol and use it as a property key. A symbolic property does not appear in `for..in`, so it won't be occasionally listed. Also it won't be accessed directly, because another script does not have our symbol, so it will not occasionally intervene into its actions. - - So we can "covertly" hide something into objects that we need, but others should not see, using symbolic properties. - -2. There are many system symbols used by JavaScript which are accessible as `Symbol.*`. We can use them to alter some built-in behaviors. For instance, later in the tutorial we'll use `Symbol.iterator` for [iterables](info:iterable), `Symbol.toPrimitive` to setup [object-to-primitive conversion](info:object-toprimitive) and so on. - -Technically, symbols are not 100% hidden. There is a built-in method [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) that allows us to get all symbols. Also there is a method named [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns *all* keys of an object including symbolic ones. So they are not really hidden. But most libraries, built-in methods and syntax constructs adhere to a common agreement that they are. And the one who explicitly calls the aforementioned methods probably understands well what he's doing. diff --git a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md b/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md deleted file mode 100644 index e2e87de7c9..0000000000 --- a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md +++ /dev/null @@ -1,43 +0,0 @@ -**Error**! - -Try it: - -```js run -let user = { - name: "John", - go: function() { alert(this.name) } -} - -(user.go)() // error! -``` - -The error message in most browsers does not give understanding what went wrong. - -**The error appears because a semicolon is missing after `user = {...}`.** - -JavaScript does not assume a semicolon before a bracket `(user.go)()`, so it reads the code like: - -```js no-beautify -let user = { go:... }(user.go)() -``` - -Then we can also see that such a joint expression is syntactically a call of the object `{ go: ... }` as a function with the argument `(user.go)`. And that also happens on the same line with `let user`, so the `user` object has not yet even been defined, hence the error. - -If we insert the semicolon, all is fine: - -```js run -let user = { - name: "John", - go: function() { alert(this.name) } -}*!*;*/!* - -(user.go)() // John -``` - -Please note that brackets around `(user.go)` do nothing here. Usually they setup the order of operations, but here the dot `.` works first anyway, so there's no effect. Only the semicolon thing matters. - - - - - - diff --git a/1-js/04-object-basics/04-object-methods/3-why-this/solution.md b/1-js/04-object-basics/04-object-methods/3-why-this/solution.md deleted file mode 100644 index 89bc0d722a..0000000000 --- a/1-js/04-object-basics/04-object-methods/3-why-this/solution.md +++ /dev/null @@ -1,22 +0,0 @@ - -Here's the explanations. - -1. That's a regular object method call. - -2. The same, brackets do not change the order of operations here, the dot is first anyway. - -3. Here we have a more complex call `(expression).method()`. The call works as if it were split into two lines: - - ```js no-beautify - f = obj.go; // calculate the expression - f(); // call what we have - ``` - - Here `f()` is executed as a function, without `this`. - -4. The similar thing as `(3)`, to the left of the dot `.` we have an expression. - -To explain the behavior of `(3)` and `(4)` we need to recall that property accessors (dot or square brackets) return a value of the Reference Type. - -Any operation on it except a method call (like assignment `=` or `||`) turns it into an ordinary value, which does not carry the information allowing to set `this`. - diff --git a/1-js/04-object-basics/04-object-methods/3-why-this/task.md b/1-js/04-object-basics/04-object-methods/3-why-this/task.md deleted file mode 100644 index f22de29ccd..0000000000 --- a/1-js/04-object-basics/04-object-methods/3-why-this/task.md +++ /dev/null @@ -1,26 +0,0 @@ -importance: 3 - ---- - -# Explain the value of "this" - -In the code below we intend to call `user.go()` method 4 times in a row. - -But calls `(1)` and `(2)` works differently from `(3)` and `(4)`. Why? - -```js run no-beautify -let obj, method; - -obj = { - go: function() { alert(this); } -}; - -obj.go(); // (1) [object Object] - -(obj.go)(); // (2) [object Object] - -(method = obj.go)(); // (3) undefined - -(obj.go || obj.stop)(); // (4) undefined -``` - 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 f5773ec2cd..f33c9310ee 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 @@ -7,21 +7,32 @@ function makeUser() { name: "John", ref: this }; -}; +} let user = makeUser(); alert( user.ref.name ); // Error: Cannot read property 'name' of undefined ``` -That's because rules that set `this` do not look at object literals. +That's because rules that set `this` do not look at object definition. Only the moment of call matters. -Here the value of `this` inside `makeUser()` is `undefined`, because it is called as a function, not as a method. +Here the value of `this` inside `makeUser()` is `undefined`, because it is called as a function, not as a method with "dot" syntax. -And the object literal itself has no effect on `this`. The value of `this` is one for the whole function, code blocks and object literals do not affect it. +The value of `this` is one for the whole function, code blocks and object literals do not affect it. So `ref: this` actually takes current `this` of the function. +We can rewrite the function and return the same `this` with `undefined` value: + +```js run +function makeUser(){ + return this; // this time there's no object literal +} + +alert( makeUser().name ); // Error: Cannot read property 'name' of undefined +``` +As you can see the result of `alert( makeUser().name )` is the same as the result of `alert( user.ref.name )` from the previous example. + Here's the opposite case: ```js run @@ -34,7 +45,7 @@ function makeUser() { } */!* }; -}; +} let user = makeUser(); @@ -42,5 +53,3 @@ alert( user.ref().name ); // John ``` Now it works, because `user.ref()` is a method. And the value of `this` is set to the object before dot `.`. - - 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 4784b082c0..c6f8f96583 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 @@ -14,7 +14,7 @@ function makeUser() { 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 1f71eda4c6..4decb76dc1 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 @@ -15,6 +15,11 @@ describe("calculator", function() { afterEach(function() { prompt.restore(); }); + + it('the read get two values and saves them as object properties', function () { + assert.equal(calculator.a, 2); + assert.equal(calculator.b, 3); + }); it("the sum is 5", function() { assert.equal(calculator.sum(), 5); diff --git a/1-js/04-object-basics/04-object-methods/7-calculator/solution.md b/1-js/04-object-basics/04-object-methods/7-calculator/solution.md index 22c4bf187c..459997624e 100644 --- a/1-js/04-object-basics/04-object-methods/7-calculator/solution.md +++ b/1-js/04-object-basics/04-object-methods/7-calculator/solution.md @@ -1,6 +1,5 @@ - -```js run demo +```js run demo solution let calculator = { sum() { return this.a + this.b; @@ -20,4 +19,3 @@ calculator.read(); alert( calculator.sum() ); alert( calculator.mul() ); ``` - 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 aa22608ece..82d0da0300 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 Create an object `calculator` with three methods: -- `read()` prompts for two values and saves them as object properties. +- `read()` prompts for two values and saves them as object properties with names `a` and `b` respectively. - `sum()` returns the sum of saved values. - `mul()` multiplies saved values and returns the result. @@ -21,4 +21,3 @@ alert( calculator.mul() ); ``` [demo] - 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 a2b17fcc4f..b4f2459b7b 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 @@ -32,6 +32,14 @@ describe('Ladder', function() { it('down().up().up().up() ', function() { assert.equal(ladder.down().up().up().up().step, 2); }); + + it('showStep() should return 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 41edd72369..f215461ddd 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,6 +1,6 @@ The solution is to return the object itself from every call. -```js run +```js run demo let ladder = { step: 0, up() { @@ -21,20 +21,19 @@ let ladder = { return this; */!* } -} +}; -ladder.up().up().down().up().down().showStep(); // 1 +ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0 ``` We also can write a single call per line. For long chains it's more readable: -```js +```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 a989846f52..7d2ef8c15f 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 # Chaining -There's a `ladder` object that allows to go up and down: +There's a `ladder` object that allows you to go up and down: ```js let ladder = { @@ -21,19 +21,21 @@ let ladder = { }; ``` -Now, if we need to make several calls in sequence, can do it like this: +Now, if we need to make several calls in sequence, we can do it like this: ```js ladder.up(); ladder.up(); ladder.down(); ladder.showStep(); // 1 +ladder.down(); +ladder.showStep(); // 0 ``` -Modify the code of `up` and `down` to make the calls chainable, like this: +Modify the code of `up`, `down`, and `showStep` to make the calls chainable, like this: ```js -ladder.up().up().down().showStep(); // 1 +ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0 ``` -Such approach is widely used across JavaScript libraries. +Such an approach is widely used across JavaScript libraries. 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 f6e8728f19..cea2b6a706 100644 --- a/1-js/04-object-basics/04-object-methods/article.md +++ b/1-js/04-object-basics/04-object-methods/article.md @@ -15,7 +15,7 @@ Actions are represented in JavaScript by functions in properties. ## Method examples -For the start, let's teach the `user` to say hello: +For a start, let's teach the `user` to say hello: ```js run let user = { @@ -32,11 +32,11 @@ user.sayHi = function() { user.sayHi(); // Hello! ``` -Here we've just used a Function Expression to create the function and assign it to the property `user.sayHi` of the object. +Here we've just used a Function Expression to create a function and assign it to the property `user.sayHi` of the object. -Then we can call it. The user can now speak! +Then we can call it as `user.sayHi()`. The user can now speak! -A function that is the property of an object is called its *method*. +A function that is a property of an object is called its *method*. So, here we've got a method `sayHi` of the object `user`. @@ -51,7 +51,7 @@ let user = { // first, declare function sayHi() { alert("Hello!"); -}; +} // then add as a method user.sayHi = sayHi; @@ -61,9 +61,9 @@ user.sayHi(); // Hello! ``` ```smart header="Object-oriented programming" -When we write our code using objects to represent entities, that's called an [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), in short: "OOP". +When we write our code using objects to represent entities, that's called [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), in short: "OOP". -OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E.Gamma, R.Helm, R.Johnson, J.Vissides or "Object-Oriented Analysis and Design with Applications" by G.Booch, and more. We'll scratch the surface of that topic later in the chapter . +OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E. Gamma, R. Helm, R. Johnson, J. Vissides or "Object-Oriented Analysis and Design with Applications" by G. Booch, and more. ``` ### Method shorthand @@ -72,16 +72,16 @@ There exists a shorter syntax for methods in an object literal: ```js // these objects do the same -let user = { +user = { sayHi: function() { alert("Hello"); } }; // method shorthand looks better, right? -let user = { +user = { *!* - sayHi() { // same as "sayHi: function()" + sayHi() { // same as "sayHi: function(){...}" */!* alert("Hello"); } @@ -90,7 +90,7 @@ let user = { As demonstrated, we can omit `"function"` and just write `sayHi()`. -To tell the truth, the notations are not fully identical. There are subtle differences related to object inheritance (to be covered later), but for now they do not matter. In almost all cases the shorter syntax is preferred. +To tell the truth, the notations are not fully identical. There are subtle differences related to object inheritance (to be covered later), but for now they do not matter. In almost all cases, the shorter syntax is preferred. ## "this" in methods @@ -111,6 +111,7 @@ let user = { sayHi() { *!* + // "this" is the "current object" alert(this.name); */!* } @@ -159,16 +160,18 @@ let user = { let admin = user; user = null; // overwrite to make things obvious -admin.sayHi(); // Whoops! inside sayHi(), the old name is used! error! +*!* +admin.sayHi(); // TypeError: Cannot read property 'name' of null +*/!* ``` If we used `this.name` instead of `user.name` inside the `alert`, then the code would work. ## "this" is not bound -In JavaScript, "this" keyword behaves unlike most other programming languages. First, it can be used in any function. +In JavaScript, keyword `this` behaves unlike most other programming languages. It can be used in any function, even if it's not a method of an object. -There's no syntax error in the code like that: +There's no syntax error in the following example: ```js function sayHi() { @@ -176,9 +179,9 @@ function sayHi() { } ``` -The value of `this` is evaluated during the run-time. And it can be anything. +The value of `this` is evaluated during the run-time, depending on the context. -For instance, the same function may have different "this" when called from different objects: +For instance, here the same function is assigned to two different objects and has different "this" in the calls: ```js run let user = { name: "John" }; @@ -189,7 +192,7 @@ function sayHi() { } *!* -// use the same functions in two objects +// use the same function in two objects user.f = sayHi; admin.f = sayHi; */!* @@ -202,7 +205,10 @@ admin.f(); // Admin (this == admin) admin['f'](); // Admin (dot or square brackets access the method – doesn't matter) ``` -Actually, we can call the function without an object at all: +The rule is simple: if `obj.f()` is called, then `this` is `obj` during the call of `f`. So it's either `user` or `admin` in the example above. + +````smart header="Calling without an object: `this == undefined`" +We can even call the function without an object at all: ```js run function sayHi() { @@ -214,110 +220,21 @@ sayHi(); // undefined In this case `this` is `undefined` in strict mode. If we try to access `this.name`, there will be an error. -In non-strict mode (if one forgets `use strict`) the value of `this` in such case will be the *global object* (`window` in a browser, we'll get to it later). This is a historical behavior that `"use strict"` fixes. +In non-strict mode the value of `this` in such case will be the *global object* (`window` in a browser, we'll get to it later in the chapter [](info:global-object)). This is a historical behavior that `"use strict"` fixes. -Please note that usually a call of a function that uses `this` without an object is not normal, but rather a programming mistake. If a function has `this`, then it is usually meant to be called in the context of an object. +Usually such call is a programming error. If there's `this` inside a function, it expects to be called in an object context. +```` ```smart header="The consequences of unbound `this`" If you come from another programming language, then you are probably used to the idea of a "bound `this`", where methods defined in an object always have `this` referencing that object. -In JavaScript `this` is "free", its value is evaluated at call-time and does not depend on where the method was declared, but rather on what's the object "before the dot". +In JavaScript `this` is "free", its value is evaluated at call-time and does not depend on where the method was declared, but rather on what object is "before the dot". -The concept of run-time evaluated `this` has both pluses and minuses. On the one hand, a function can be reused for different objects. On the other hand, greater flexibility opens a place for mistakes. +The concept of run-time evaluated `this` has both pluses and minuses. On the one hand, a function can be reused for different objects. On the other hand, the greater flexibility creates more possibilities for mistakes. -Here our position is not to judge whether this language design decision is good or bad. We'll understand how to work with it, how to get benefits and evade problems. +Here our position is not to judge whether this language design decision is good or bad. We'll understand how to work with it, how to get benefits and avoid problems. ``` -## Internals: Reference Type - -```warn header="In-depth language feature" -This section covers an advanced topic, to understand certain edge-cases better. - -If you want to go on faster, it can be skipped or postponed. -``` - -An intricate method call can lose `this`, for instance: - -```js run -let user = { - name: "John", - hi() { alert(this.name); }, - bye() { alert("Bye"); } -}; - -user.hi(); // John (the simple call works) - -*!* -// now let's call user.hi or user.bye depending on the name -(user.name == "John" ? user.hi : user.bye)(); // Error! -*/!* -``` - -On the last line there is a ternary operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`. - -The method is immediately called with parentheses `()`. But it doesn't work right! - -You can see that the call results in an error, cause the value of `"this"` inside the call becomes `undefined`. - -This works (object dot method): -```js -user.hi(); -``` - -This doesn't (evaluated method): -```js -(user.name == "John" ? user.hi : user.bye)(); // Error! -``` - -Why? If we want to understand why it happens, let's get under the hood of how `obj.method()` call works. - -Looking closely, we may notice two operations in `obj.method()` statement: - -1. First, the dot `'.'` retrieves the property `obj.method`. -2. Then parentheses `()` execute it. - -So, how does the information about `this` gets passed from the first part to the second one? - -If we put these operations on separate lines, then `this` will be lost for sure: - -```js run -let user = { - name: "John", - hi() { alert(this.name); } -} - -*!* -// split getting and calling the method in two lines -let hi = user.hi; -hi(); // Error, because this is undefined -*/!* -``` - -Here `hi = user.hi` puts the function into the variable, and then on the last line it is completely standalone, and so there's no `this`. - -**To make `user.hi()` calls work, JavaScript uses a trick -- the dot `'.'` returns not a function, but a value of the special [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).** - -The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language. - -The value of Reference Type is a three-value combination `(base, name, strict)`, where: - -- `base` is the object. -- `name` is the property. -- `strict` is true if `use strict` is in effect. - -The result of a property access `user.hi` is not a function, but a value of Reference Type. For `user.hi` in strict mode it is: - -```js -// Reference Type value -(user, "hi", true) -``` - -When parentheses `()` are called on the Reference Type, they receive the full information about the object and its method, and can set the right `this` (`=user` in this case). - -Any other operation like assignment `hi = user.hi` discards the reference type as a whole, takes the value of `user.hi` (a function) and passes it on. So any further operation "loses" `this`. - -So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj[method]()` syntax (they do the same here). Later in this tutorial, we will learn various ways to solve this problem such as [func.bind()](/bind#solution-2-bind). - ## Arrow functions have no "this" Arrow functions are special: they don't have their "own" `this`. If we reference `this` from such a function, it's taken from the outer "normal" function. @@ -347,7 +264,7 @@ That's a special feature of arrow functions, it's useful when we actually do not The value of `this` is defined at run-time. - When a function is declared, it may use `this`, but that `this` has no value until the function is called. -- That function can be copied between objects. +- A function can be copied between objects. - When a function is called in the "method" syntax: `object.method()`, the value of `this` during the call is `object`. Please note that arrow functions are special: they have no `this`. When `this` is accessed inside an arrow function, it is taken from outside. diff --git a/1-js/04-object-basics/05-object-toprimitive/article.md b/1-js/04-object-basics/05-object-toprimitive/article.md deleted file mode 100644 index a44cf4f4d4..0000000000 --- a/1-js/04-object-basics/05-object-toprimitive/article.md +++ /dev/null @@ -1,236 +0,0 @@ - -# Object to primitive conversion - -What happens when objects are added `obj1 + obj2`, subtracted `obj1 - obj2` or printed using `alert(obj)`? - -There are special methods in objects that do the conversion. - -In the chapter we've seen the rules for numeric, string and boolean conversions of primitives. But we left a gap for objects. Now, as we know about methods and symbols it becomes possible to close it. - -For objects, there's no to-boolean conversion, because all objects are `true` in a boolean context. So there are only string and numeric conversions. - -The numeric conversion happens when we subtract objects or apply mathematical functions. For instance, `Date` objects (to be covered in the chapter ) can be subtracted, and the result of `date1 - date2` is the time difference between two dates. - -As for the string conversion -- it usually happens when we output an object like `alert(obj)` and in similar contexts. - -## ToPrimitive - -When an object is used in the context where a primitive is required, for instance, in an `alert` or mathematical operations, it's converted to a primitive value using the `ToPrimitive` algorithm ([specification](https://tc39.github.io/ecma262/#sec-toprimitive)). - -That algorithm allows us to customize the conversion using a special object method. - -Depending on the context, the conversion has a so-called "hint". - -There are three variants: - -`"string"` -: When an operation expects a string, for object-to-string conversions, like `alert`: - - ```js - // output - alert(obj); - - // using object as a property key - anotherObj[obj] = 123; - ``` - -`"number"` -: When an operation expects a number, for object-to-number conversions, like maths: - - ```js - // explicit conversion - let num = Number(obj); - - // maths (except binary plus) - let n = +obj; // unary plus - let delta = date1 - date2; - - // less/greater comparison - let greater = user1 > user2; - ``` - -`"default"` -: Occurs in rare cases when the operator is "not sure" what type to expect. - - For instance, binary plus `+` can work both with strings (concatenates them) and numbers (adds them), so both strings and numbers would do. Or when an object is compared using `==` with a string, number or a symbol. - - ```js - // binary plus - let total = car1 + car2; - - // obj == string/number/symbol - if (user == 1) { ... }; - ``` - - The greater/less operator `<>` can work with both strings and numbers too. Still, it uses "number" hint, not "default". That's for historical reasons. - - In practice, all built-in objects except for one case (`Date` object, we'll learn it later) implement `"default"` conversion the same way as `"number"`. And probably we should do the same. - -Please note -- there are only three hints. It's that simple. There is no "boolean" hint (all objects are `true` in boolean context) or anything else. And if we treat `"default"` and `"number"` the same, like most built-ins do, then there are only two conversions. - -**To do the conversion, JavaScript tries to find and call three object methods:** - -1. Call `obj[Symbol.toPrimitive](hint)` if the method exists, -2. Otherwise if hint is `"string"` - - try `obj.toString()` and `obj.valueOf()`, whatever exists. -3. Otherwise if hint is `"number"` or `"default"` - - try `obj.valueOf()` and `obj.toString()`, whatever exists. - -## Symbol.toPrimitive - -Let's start from the first method. There's a built-in symbol named `Symbol.toPrimitive` that should be used to name the conversion method, like this: - -```js -obj[Symbol.toPrimitive] = function(hint) { - // return a primitive value - // hint = one of "string", "number", "default" -} -``` - -For instance, here `user` object implements it: - -```js run -let user = { - name: "John", - money: 1000, - - [Symbol.toPrimitive](hint) { - alert(`hint: ${hint}`); - return hint == "string" ? `{name: "${this.name}"}` : this.money; - } -}; - -// conversions demo: -alert(user); // hint: string -> {name: "John"} -alert(+user); // hint: number -> 1000 -alert(user + 500); // hint: default -> 1500 -``` - -As we can see from the code, `user` becomes a self-descriptive string or a money amount depending on the conversion. The single method `user[Symbol.toPrimitive]` handles all conversion cases. - - -## toString/valueOf - -Methods `toString` and `valueOf` come from ancient times. They are not symbols (symbols did not exist that long ago), but rather "regular" string-named methods. They provide an alternative "old-style" way to implement the conversion. - -If there's no `Symbol.toPrimitive` then JavaScript tries to find them and try in the order: - -- `toString -> valueOf` for "string" hint. -- `valueOf -> toString` otherwise. - -For instance, here `user` does the same as above using a combination of `toString` and `valueOf`: - -```js run -let user = { - name: "John", - money: 1000, - - // for hint="string" - toString() { - return `{name: "${this.name}"}`; - }, - - // for hint="number" or "default" - valueOf() { - return this.money; - } - -}; - -alert(user); // toString -> {name: "John"} -alert(+user); // valueOf -> 1000 -alert(user + 500); // valueOf -> 1500 -``` - -Often we want a single "catch-all" place to handle all primitive conversions. In this case we can implement `toString` only, like this: - -```js run -let user = { - name: "John", - - toString() { - return this.name; - } -}; - -alert(user); // toString -> John -alert(user + 500); // toString -> John500 -``` - -In the absence of `Symbol.toPrimitive` and `valueOf`, `toString` will handle all primitive conversions. - - -## ToPrimitive and ToString/ToNumber - -The important thing to know about all primitive-conversion methods is that they do not necessarily return the "hinted" primitive. - -There is no control whether `toString()` returns exactly a string, or whether `Symbol.toPrimitive` method returns a number for a hint "number". - -**The only mandatory thing: these methods must return a primitive.** - -An operation that initiated the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary. - -For instance: - -- Mathematical operations (except binary plus) perform `ToNumber` conversion: - - ```js run - let obj = { - toString() { // toString handles all conversions in the absence of other methods - return "2"; - } - }; - - alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2 - ``` - -- Binary plus checks the primitive -- if it's a string, then it does concatenation, otherwise it performs `ToNumber` and works with numbers. - - String example: - ```js run - let obj = { - toString() { - return "2"; - } - }; - - alert(obj + 2); // 22 (ToPrimitive returned string => concatenation) - ``` - - Number example: - ```js run - let obj = { - toString() { - return true; - } - }; - - alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber) - ``` - -```smart header="Historical notes" -For historical reasons, methods `toString` or `valueOf` *should* return a primitive: if any of them returns an object, then there's no error, but that object is ignored (like if the method didn't exist). - -In contrast, `Symbol.toPrimitive` *must* return a primitive, otherwise, there will be an error. -``` - -## Summary - -The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value. - -There are 3 types (hints) of it: -- `"string"` (for `alert` and other string conversions) -- `"number"` (for maths) -- `"default"` (few operators) - -The specification describes explicitly which operator uses which hint. There are very few operators that "don't know what to expect" and use the `"default"` hint. Usually for built-in objects `"default"` hint is handled the same way as `"number"`, so in practice the last two are often merged together. - -The conversion algorithm is: - -1. Call `obj[Symbol.toPrimitive](hint)` if the method exists, -2. Otherwise if hint is `"string"` - - try `obj.toString()` and `obj.valueOf()`, whatever exists. -3. Otherwise if hint is `"number"` or `"default"` - - try `obj.valueOf()` and `obj.toString()`, whatever exists. - -In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for all conversions that return a "human-readable" representation of an object, for logging or debugging purposes. 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 79a29b0d32..7d8edd7cab 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 @@ -2,7 +2,7 @@ Yes, it's possible. If a function returns an object then `new` returns it instead of `this`. -So thay can, for instance, return the same externally defined object `obj`: +So they can, for instance, return the same externally defined object `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 8c1fea8ebb..e932a201ad 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,14 +4,14 @@ importance: 2 # Two functions – one object -Is it possible to create functions `A` and `B` such as `new A()==new B()`? +Is it possible to create functions `A` and `B` so that `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/solution.md b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/solution.md index e5583c5d01..86bb65416d 100644 --- a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/solution.md +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/solution.md @@ -1,5 +1,3 @@ - - ```js run demo function Calculator() { 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 60e7c373e0..c862bec40d 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 @@ -6,7 +6,7 @@ importance: 5 Create a constructor function `Calculator` that creates objects with 3 methods: -- `read()` asks for two values using `prompt` and remembers them in object properties. +- `read()` prompts for two values and saves them as object properties with names `a` and `b` respectively. - `sum()` returns the sum of these properties. - `mul()` returns the multiplication product of these properties. 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 3362b5b4bc..c2c44881e8 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 @@ -17,8 +17,10 @@ Here's the demo of the code: ```js let accumulator = new Accumulator(1); // initial value 1 + accumulator.read(); // adds the user-entered value accumulator.read(); // adds the user-entered value + alert(accumulator.value); // shows the sum of these values ``` 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 5fe6e5d90c..a335464f1e 100644 --- a/1-js/04-object-basics/06-constructor-new/article.md +++ b/1-js/04-object-basics/06-constructor-new/article.md @@ -1,6 +1,6 @@ # Constructor, operator "new" -The regular `{...}` syntax allows to create one object. But often we need to create many similar objects, like multiple users or menu items and so on. +The regular `{...}` syntax allows us to create one object. But often we need to create many similar objects, like multiple users or menu items and so on. That can be done using constructor functions and the `"new"` operator. @@ -27,7 +27,7 @@ alert(user.name); // Jack alert(user.isAdmin); // false ``` -When a function is executed as `new User(...)`, it does the following steps: +When a function is executed with `new`, it does the following steps: 1. A new empty object is created and assigned to `this`. 2. The function body executes. Usually it modifies `this`, adds new properties to it. @@ -51,7 +51,7 @@ function User(name) { } ``` -So the result of `new User("Jack")` is the same object as: +So `let user = new User("Jack")` gives the same result as: ```js let user = { @@ -64,13 +64,14 @@ Now if we want to create other users, we can call `new User("Ann")`, `new User(" That's the main purpose of constructors -- to implement reusable object creation code. -Let's note once again -- technically, any function can be used as a constructor. That is: any function can be run with `new`, and it will execute the algorithm above. The "capital letter first" is a common agreement, to make it clear that a function is to be run with `new`. +Let's note once again -- technically, any function (except arrow functions, as they don't have `this`) can be used as a constructor. It can be run with `new`, and it will execute the algorithm above. The "capital letter first" is a common agreement, to make it clear that a function is to be run with `new`. ````smart header="new function() { ... }" -If we have many lines of code all about creation of a single complex object, we can wrap them in constructor function, like this: +If we have many lines of code all about creation of a single complex object, we can wrap them in an immediately called constructor function, like this: ```js -let user = new function() { +// create a function and immediately call it with new +let user = new function() { this.name = "John"; this.isAdmin = false; @@ -80,10 +81,10 @@ let user = new function() { }; ``` -The constructor can't be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code that constructs the single object, without future reuse. +This constructor can't be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code that constructs the single object, without future reuse. ```` -## Dual-syntax constructors: new.target +## Constructor mode test: new.target ```smart header="Advanced stuff" The syntax from this section is rarely used, skip it unless you want to know everything. @@ -91,7 +92,7 @@ The syntax from this section is rarely used, skip it unless you want to know eve Inside a function, we can check whether it was called with `new` or without it, using a special `new.target` property. -It is empty for regular calls and equals the function if called with `new`: +It is undefined for regular calls and equals the function if called with `new`: ```js run function User() { @@ -109,7 +110,9 @@ new User(); // function User { ... } */!* ``` -That can be used to allow both `new` and regular calls to work the same. That is, create the same object: +That can be used inside the function to know whether it was called with `new`, "in constructor mode", or without it, "in regular mode". + +We can also make both `new` and regular calls to do the same, like this: ```js run function User(name) { @@ -134,7 +137,7 @@ Usually, constructors do not have a `return` statement. Their task is to write a But if there is a `return` statement, then the rule is simple: -- If `return` is called with object, then it is returned instead of `this`. +- If `return` is called with an object, then the object is returned instead of `this`. - If `return` is called with a primitive, it's ignored. In other words, `return` with an object returns that object, in all other cases `this` is returned. @@ -146,10 +149,10 @@ function BigUser() { this.name = "John"; - return { name: "Godzilla" }; // <-- returns an object + return { name: "Godzilla" }; // <-- returns this object } -alert( new BigUser().name ); // Godzilla, got that object ^^ +alert( new BigUser().name ); // Godzilla, got that object ``` And here's an example with an empty `return` (or we could place a primitive after it, doesn't matter): @@ -159,10 +162,7 @@ function SmallUser() { this.name = "John"; - return; // finishes the execution, returns this - - // ... - + return; // <-- returns this } alert( new SmallUser().name ); // John @@ -171,7 +171,7 @@ alert( new SmallUser().name ); // John Usually constructors don't have a `return` statement. Here we mention the special behavior with returning objects mainly for the sake of completeness. ````smart header="Omitting parentheses" -By the way, we can omit parentheses after `new`, if it has no arguments: +By the way, we can omit parentheses after `new`: ```js let user = new User; // <-- no parentheses @@ -213,6 +213,8 @@ john = { */ ``` +To create complex objects, there's a more advanced syntax, [classes](info:classes), that we'll cover later. + ## Summary - Constructor functions or, briefly, constructors, are regular functions, but there's a common agreement to name them with capital letter first. @@ -225,5 +227,5 @@ JavaScript provides constructor functions for many built-in language objects: li ```smart header="Objects, we'll be back!" In this chapter we only cover the basics about objects and constructors. They are essential for learning more about data types and functions in the next chapters. -After we learn that, in the chapter we return to objects and cover them in-depth, including inheritance and classes. +After we learn that, we return to objects and cover them in-depth in the chapters and . ``` diff --git a/1-js/04-object-basics/07-optional-chaining/article.md b/1-js/04-object-basics/07-optional-chaining/article.md new file mode 100644 index 0000000000..4c60294234 --- /dev/null +++ b/1-js/04-object-basics/07-optional-chaining/article.md @@ -0,0 +1,233 @@ + +# Optional chaining '?.' + +[recent browser="new"] + +The optional chaining `?.` is a safe way to access nested object properties, even if an intermediate property doesn't exist. + +## The "non-existing property" problem + +If you've just started to read the tutorial and learn JavaScript, maybe the problem hasn't touched you yet, but it's quite common. + +As an example, let's say we have `user` objects that hold the information about our users. + +Most of our users have addresses in `user.address` property, with the street `user.address.street`, but some did not provide them. + +In such case, when we attempt to get `user.address.street`, and the user happens to be without an address, we get an error: + +```js run +let user = {}; // a user without "address" property + +alert(user.address.street); // Error! +``` + +That's the expected result. JavaScript works like this. As `user.address` is `undefined`, an attempt to get `user.address.street` fails with an error. + +In many practical cases we'd prefer to get `undefined` instead of an error here (meaning "no street"). + +...and another example. In Web development, we can get an object that corresponds to a web page element using a special method call, such as `document.querySelector('.elem')`, and it returns `null` when there's no such element. + +```js run +// document.querySelector('.elem') is null if there's no element +let html = document.querySelector('.elem').innerHTML; // error if it's null +``` + +Once again, if the element doesn't exist, we'll get an error accessing `.innerHTML` property of `null`. And in some cases, when the absence of the element is normal, we'd like to avoid the error and just accept `html = null` as the result. + +How can we do this? + +The obvious solution would be to check the value using `if` or the conditional operator `?`, before accessing its property, like this: + +```js +let user = {}; + +alert(user.address ? user.address.street : undefined); +``` + +It works, there's no error... But it's quite inelegant. As you can see, the `"user.address"` appears twice in the code. + +Here's how the same would look for `document.querySelector`: + +```js run +let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null; +``` + +We can see that the element search `document.querySelector('.elem')` is actually called twice here. Not good. + +For more deeply nested properties, it becomes even uglier, as more repetitions are required. + +E.g. let's get `user.address.street.name` in a similar fashion. + +```js +let user = {}; // user has no address + +alert(user.address ? user.address.street ? user.address.street.name : null : null); +``` + +That's just awful, one may even have problems understanding such code. + +There's a little better way to write it, using the `&&` operator: + +```js run +let user = {}; // user has no address + +alert( user.address && user.address.street && user.address.street.name ); // undefined (no error) +``` + +AND'ing the whole path to the property ensures that all components exist (if not, the evaluation stops), but also isn't ideal. + +As you can see, property names are still duplicated in the code. E.g. in the code above, `user.address` appears three times. + +That's why the optional chaining `?.` was added to the language. To solve this problem once and for all! + +## Optional chaining + +The optional chaining `?.` stops the evaluation if the value before `?.` is `undefined` or `null` and returns `undefined`. + +**Further in this article, for brevity, we'll be saying that something "exists" if it's not `null` and not `undefined`.** + +In other words, `value?.prop`: +- works as `value.prop`, if `value` exists, +- otherwise (when `value` is `undefined/null`) it returns `undefined`. + +Here's the safe way to access `user.address.street` using `?.`: + +```js run +let user = {}; // user has no address + +alert( user?.address?.street ); // undefined (no error) +``` + +The code is short and clean, there's no duplication at all. + +Here's an example with `document.querySelector`: + +```js run +let html = document.querySelector('.elem')?.innerHTML; // will be undefined, if there's no element +``` + +Reading the address with `user?.address` works even if `user` object doesn't exist: + +```js run +let user = null; + +alert( user?.address ); // undefined +alert( user?.address.street ); // undefined +``` + +Please note: the `?.` syntax makes optional the value before it, but not any further. + +E.g. in `user?.address.street.name` the `?.` allows `user` to safely be `null/undefined` (and returns `undefined` in that case), but that's only for `user`. Further properties are accessed in a regular way. If we want some of them to be optional, then we'll need to replace more `.` with `?.`. + +```warn header="Don't overuse the optional chaining" +We should use `?.` only where it's ok that something doesn't exist. + +For example, if according to our code logic `user` object must exist, but `address` is optional, then we should write `user.address?.street`, but not `user?.address?.street`. + +Then, if `user` happens to be undefined, we'll see a programming error about it and fix it. Otherwise, if we overuse `?.`, coding errors can be silenced where not appropriate, and become more difficult to debug. +``` + +````warn header="The variable before `?.` must be declared" +If there's no variable `user` at all, then `user?.anything` triggers an error: + +```js run +// ReferenceError: user is not defined +user?.address; +``` +The variable must be declared (e.g. `let/const/var user` or as a function parameter). The optional chaining works only for declared variables. +```` + +## Short-circuiting + +As it was said before, the `?.` immediately stops ("short-circuits") the evaluation if the left part doesn't exist. + +So, if there are any further function calls or operations to the right of `?.`, they won't be made. + +For instance: + +```js run +let user = null; +let x = 0; + +user?.sayHi(x++); // no "user", so the execution doesn't reach sayHi call and x++ + +alert(x); // 0, value not incremented +``` + +## Other variants: ?.(), ?.[] + +The optional chaining `?.` is not an operator, but a special syntax construct, that also works with functions and square brackets. + +For example, `?.()` is used to call a function that may not exist. + +In the code below, some of our users have `admin` method, and some don't: + +```js run +let userAdmin = { + admin() { + alert("I am admin"); + } +}; + +let userGuest = {}; + +*!* +userAdmin.admin?.(); // I am admin +*/!* + +*!* +userGuest.admin?.(); // nothing happens (no such method) +*/!* +``` + +Here, in both lines we first use the dot (`userAdmin.admin`) to get `admin` property, because we assume that the `user` object exists, so it's safe read from it. + +Then `?.()` checks the left part: if the `admin` function exists, then it runs (that's so for `userAdmin`). Otherwise (for `userGuest`) the evaluation stops without errors. + +The `?.[]` syntax also works, if we'd like to use brackets `[]` to access properties instead of dot `.`. Similar to previous cases, it allows to safely read a property from an object that may not exist. + +```js run +let key = "firstName"; + +let user1 = { + firstName: "John" +}; + +let user2 = null; + +alert( user1?.[key] ); // John +alert( user2?.[key] ); // undefined +``` + +Also we can use `?.` with `delete`: + +```js run +delete user?.name; // delete user.name if user exists +``` + +````warn header="We can use `?.` for safe reading and deleting, but not writing" +The optional chaining `?.` has no use on the left side of an assignment. + +For example: +```js run +let user = null; + +user?.name = "John"; // Error, doesn't work +// because it evaluates to: undefined = "John" +``` + +```` + +## Summary + +The optional chaining `?.` syntax has three forms: + +1. `obj?.prop` -- returns `obj.prop` if `obj` exists, otherwise `undefined`. +2. `obj?.[prop]` -- returns `obj[prop]` if `obj` exists, otherwise `undefined`. +3. `obj.method?.()` -- calls `obj.method()` if `obj.method` exists, otherwise returns `undefined`. + +As we can see, all of them are straightforward and simple to use. The `?.` checks the left part for `null/undefined` and allows the evaluation to proceed if it's not so. + +A chain of `?.` allows to safely access nested properties. + +Still, we should apply `?.` carefully, only where it's acceptable, according to our code logic, that the left part doesn't exist. So that it won't hide programming errors from us, if they occur. diff --git a/1-js/04-object-basics/08-symbol/article.md b/1-js/04-object-basics/08-symbol/article.md new file mode 100644 index 0000000000..10a98af0a3 --- /dev/null +++ b/1-js/04-object-basics/08-symbol/article.md @@ -0,0 +1,289 @@ + +# Symbol type + +By specification, only two primitive types may serve as object property keys: + +- string type, or +- symbol type. + +Otherwise, if one uses another type, such as number, it's autoconverted to string. So that `obj[1]` is the same as `obj["1"]`, and `obj[true]` is the same as `obj["true"]`. + +Until now we've been using only strings. + +Now let's explore symbols, see what they can do for us. + +## Symbols + +A "symbol" represents a unique identifier. + +A value of this type can be created using `Symbol()`: + +```js +let id = Symbol(); +``` + +Upon creation, we can give symbols a description (also called a symbol name), mostly useful for debugging purposes: + +```js +// id is a symbol with the description "id" +let id = Symbol("id"); +``` + +Symbols are guaranteed to be unique. Even if we create many symbols with exactly the same description, they are different values. The description is just a label that doesn't affect anything. + +For instance, here are two symbols with the same description -- they are not equal: + +```js run +let id1 = Symbol("id"); +let id2 = Symbol("id"); + +*!* +alert(id1 == id2); // false +*/!* +``` + +If you are familiar with Ruby or another language that also has some sort of "symbols" -- please don't be misguided. JavaScript symbols are different. + +So, to summarize, a symbol is a "primitive unique value" with an optional description. Let's see where we can use them. + +````warn header="Symbols don't auto-convert to a string" +Most values in JavaScript support implicit conversion to a string. For instance, we can `alert` almost any value, and it will work. Symbols are special. They don't auto-convert. + +For instance, this `alert` will show an error: + +```js run +let id = Symbol("id"); +*!* +alert(id); // TypeError: Cannot convert a Symbol value to a string +*/!* +``` + +That's a "language guard" against messing up, because strings and symbols are fundamentally different and should not accidentally convert one into another. + +If we really want to show a symbol, we need to explicitly call `.toString()` on it, like here: + +```js run +let id = Symbol("id"); +*!* +alert(id.toString()); // Symbol(id), now it works +*/!* +``` + +Or get `symbol.description` property to show the description only: + +```js run +let id = Symbol("id"); +*!* +alert(id.description); // id +*/!* +``` + +```` + +## "Hidden" properties + + +Symbols allow us to create "hidden" properties of an object, that no other part of code can accidentally access or overwrite. + +For instance, if we're working with `user` objects, that belong to a third-party code. We'd like to add identifiers to them. + +Let's use a symbol key for it: + +```js run +let user = { // belongs to another code + name: "John" +}; + +let id = Symbol("id"); + +user[id] = 1; + +alert( user[id] ); // we can access the data using the symbol as the key +``` + +What's the benefit of using `Symbol("id")` over a string `"id"`? + +As `user` objects belong to another codebase, it's unsafe to add fields to them, since we might affect pre-defined behavior in that other codebase. However, symbols cannot be accessed accidentally. The third-party code won't be aware of newly defined symbols, so it's safe to add symbols to the `user` objects. + +Also, imagine that another script wants to have its own identifier inside `user`, for its own purposes. + +Then that script can create its own `Symbol("id")`, like this: + +```js +// ... +let id = Symbol("id"); + +user[id] = "Their id value"; +``` + +There will be no conflict between our and their identifiers, because symbols are always different, even if they have the same name. + +...But if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: + +```js +let user = { name: "John" }; + +// Our script uses "id" property +user.id = "Our id value"; + +// ...Another script also wants "id" for its purposes... + +user.id = "Their id value" +// Boom! overwritten by another script! +``` + +### Symbols in an object literal + +If we want to use a symbol in an object literal `{...}`, we need square brackets around it. + +Like this: + +```js +let id = Symbol("id"); + +let user = { + name: "John", +*!* + [id]: 123 // not "id": 123 +*/!* +}; +``` +That's because we need the value from the variable `id` as the key, not the string "id". + +### Symbols are skipped by for..in + +Symbolic properties do not participate in `for..in` loop. + +For instance: + +```js run +let id = Symbol("id"); +let user = { + name: "John", + age: 30, + [id]: 123 +}; + +*!* +for (let key in user) alert(key); // name, age (no symbols) +*/!* + +// the direct access by the symbol works +alert( "Direct: " + user[id] ); // Direct: 123 +``` + +[Object.keys(user)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys) also ignores them. That's a part of the general "hiding symbolic properties" principle. If another script or a library loops over our object, it won't unexpectedly access a symbolic property. + +In contrast, [Object.assign](mdn:js/Object/assign) copies both string and symbol properties: + +```js run +let id = Symbol("id"); +let user = { + [id]: 123 +}; + +let clone = Object.assign({}, user); + +alert( clone[id] ); // 123 +``` + +There's no paradox here. That's by design. The idea is that when we clone an object or merge objects, we usually want *all* properties to be copied (including symbols like `id`). + +## Global symbols + +As we've seen, usually all symbols are different, even if they have the same name. But sometimes we want same-named symbols to be same entities. For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. + +To achieve that, there exists a *global symbol registry*. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol. + +In order to read (create if absent) a symbol from the registry, use `Symbol.for(key)`. + +That call checks the global registry, and if there's a symbol described as `key`, then returns it, otherwise creates a new symbol `Symbol(key)` and stores it in the registry by the given `key`. + +For instance: + +```js run +// read from the global registry +let id = Symbol.for("id"); // if the symbol did not exist, it is created + +// read it again (maybe from another part of the code) +let idAgain = Symbol.for("id"); + +// the same symbol +alert( id === idAgain ); // true +``` + +Symbols inside the registry are called *global symbols*. If we want an application-wide symbol, accessible everywhere in the code -- that's what they are for. + +```smart header="That sounds like Ruby" +In some programming languages, like Ruby, there's a single symbol per name. + +In JavaScript, as we can see, that's true for global symbols. +``` + +### Symbol.keyFor + +We have seen that for global symbols, `Symbol.for(key)` returns a symbol by name. To do the opposite -- return a name by global symbol -- we can use: `Symbol.keyFor(sym)`: + +For instance: + +```js run +// get symbol by name +let sym = Symbol.for("name"); +let sym2 = Symbol.for("id"); + +// get name by symbol +alert( Symbol.keyFor(sym) ); // name +alert( Symbol.keyFor(sym2) ); // id +``` + +The `Symbol.keyFor` internally uses the global symbol registry to look up the key for the symbol. So it doesn't work for non-global symbols. If the symbol is not global, it won't be able to find it and returns `undefined`. + +That said, all symbols have the `description` property. + +For instance: + +```js run +let globalSymbol = Symbol.for("name"); +let localSymbol = Symbol("name"); + +alert( Symbol.keyFor(globalSymbol) ); // name, global symbol +alert( Symbol.keyFor(localSymbol) ); // undefined, not global + +alert( localSymbol.description ); // name +``` + +## System symbols + +There exist many "system" symbols that JavaScript uses internally, and we can use them to fine-tune various aspects of our objects. + +They are listed in the specification in the [Well-known symbols](https://tc39.github.io/ecma262/#sec-well-known-symbols) table: + +- `Symbol.hasInstance` +- `Symbol.isConcatSpreadable` +- `Symbol.iterator` +- `Symbol.toPrimitive` +- ...and so on. + +For instance, `Symbol.toPrimitive` allows us to describe object to primitive conversion. We'll see its use very soon. + +Other symbols will also become familiar when we study the corresponding language features. + +## Summary + +`Symbol` is a primitive type for unique identifiers. + +Symbols are created with `Symbol()` call with an optional description (name). + +Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` with the same `key` return exactly the same symbol. + +Symbols have two main use cases: + +1. "Hidden" object properties. + + If we want to add a property into an object that "belongs" to another script or a library, we can create a symbol and use it as a property key. A symbolic property does not appear in `for..in`, so it won't be accidentally processed together with other properties. Also it won't be accessed directly, because another script does not have our symbol. So the property will be protected from accidental use or overwrite. + + So we can "covertly" hide something into objects that we need, but others should not see, using symbolic properties. + +2. There are many system symbols used by JavaScript which are accessible as `Symbol.*`. We can use them to alter some built-in behaviors. For instance, later in the tutorial we'll use `Symbol.iterator` for [iterables](info:iterable), `Symbol.toPrimitive` to setup [object-to-primitive conversion](info:object-toprimitive) and so on. + +Technically, symbols are not 100% hidden. There is a built-in method [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) that allows us to get all symbols. Also there is a method named [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns *all* keys of an object including symbolic ones. But most libraries, built-in functions and syntax constructs don't use these methods. diff --git a/1-js/04-object-basics/09-object-toprimitive/article.md b/1-js/04-object-basics/09-object-toprimitive/article.md new file mode 100644 index 0000000000..fa68da583b --- /dev/null +++ b/1-js/04-object-basics/09-object-toprimitive/article.md @@ -0,0 +1,280 @@ + +# Object to primitive conversion + +What happens when objects are added `obj1 + obj2`, subtracted `obj1 - obj2` or printed using `alert(obj)`? + +JavaScript doesn't allow you to customize how operators work on objects. Unlike some other programming languages, such as Ruby or C++, we can't implement a special object method to handle addition (or other operators). + +In case of such operations, objects are auto-converted to primitives, and then the operation is carried out over these primitives and results in a primitive value. + +That's an important limitation: the result of `obj1 + obj2` (or another math operation) can't be another object! + +E.g. we can't make objects representing vectors or matrices (or achievements or whatever), add them and expect a "summed" object as the result. Such architectural feats are automatically "off the board". + +So, because we can't technically do much here, there's no maths with objects in real projects. When it happens, with rare exceptions, it's because of a coding mistake. + +In this chapter we'll cover how an object converts to primitive and how to customize it. + +We have two purposes: + +1. It will allow us to understand what's going on in case of coding mistakes, when such an operation happened accidentally. +2. There are exceptions, where such operations are possible and look good. E.g. subtracting or comparing dates (`Date` objects). We'll come across them later. + +## Conversion rules + +In the chapter we've seen the rules for numeric, string and boolean conversions of primitives. But we left a gap for objects. Now, as we know about methods and symbols it becomes possible to fill it. + +1. There's no conversion to boolean. All objects are `true` in a boolean context, as simple as that. There exist only numeric and string conversions. +2. The numeric conversion happens when we subtract objects or apply mathematical functions. For instance, `Date` objects (to be covered in the chapter ) can be subtracted, and the result of `date1 - date2` is the time difference between two dates. +3. As for the string conversion -- it usually happens when we output an object with `alert(obj)` and in similar contexts. + +We can implement string and numeric conversion by ourselves, using special object methods. + +Now let's get into technical details, because it's the only way to cover the topic in-depth. + +## Hints + +How does JavaScript decide which conversion to apply? + +There are three variants of type conversion, that happen in various situations. They're called "hints", as described in the [specification](https://tc39.github.io/ecma262/#sec-toprimitive): + +`"string"` +: For an object-to-string conversion, when we're doing an operation on an object that expects a string, like `alert`: + + ```js + // output + alert(obj); + + // using object as a property key + anotherObj[obj] = 123; + ``` + +`"number"` +: For an object-to-number conversion, like when we're doing maths: + + ```js + // explicit conversion + let num = Number(obj); + + // maths (except binary plus) + let n = +obj; // unary plus + let delta = date1 - date2; + + // less/greater comparison + let greater = user1 > user2; + ``` + + Most built-in mathematical functions also include such conversion. + +`"default"` +: Occurs in rare cases when the operator is "not sure" what type to expect. + + For instance, binary plus `+` can work both with strings (concatenates them) and numbers (adds them). So if a binary plus gets an object as an argument, it uses the `"default"` hint to convert it. + + Also, if an object is compared using `==` with a string, number or a symbol, it's also unclear which conversion should be done, so the `"default"` hint is used. + + ```js + // binary plus uses the "default" hint + let total = obj1 + obj2; + + // obj == number uses the "default" hint + if (user == 1) { ... }; + ``` + + The greater and less comparison operators, such as `<` `>`, can work with both strings and numbers too. Still, they use the `"number"` hint, not `"default"`. That's for historical reasons. + +In practice though, things are a bit simpler. + +All built-in objects except for one case (`Date` object, we'll learn it later) implement `"default"` conversion the same way as `"number"`. And we probably should do the same. + +Still, it's important to know about all 3 hints, soon we'll see why. + +**To do the conversion, JavaScript tries to find and call three object methods:** + +1. Call `obj[Symbol.toPrimitive](hint)` - the method with the symbolic key `Symbol.toPrimitive` (system symbol), if such method exists, +2. Otherwise if hint is `"string"` + - try calling `obj.toString()` or `obj.valueOf()`, whatever exists. +3. Otherwise if hint is `"number"` or `"default"` + - try calling `obj.valueOf()` or `obj.toString()`, whatever exists. + +## Symbol.toPrimitive + +Let's start from the first method. There's a built-in symbol named `Symbol.toPrimitive` that should be used to name the conversion method, like this: + +```js +obj[Symbol.toPrimitive] = function(hint) { + // here goes the code to convert this object to a primitive + // it must return a primitive value + // hint = one of "string", "number", "default" +}; +``` + +If the method `Symbol.toPrimitive` exists, it's used for all hints, and no more methods are needed. + +For instance, here `user` object implements it: + +```js run +let user = { + name: "John", + money: 1000, + + [Symbol.toPrimitive](hint) { + alert(`hint: ${hint}`); + return hint == "string" ? `{name: "${this.name}"}` : this.money; + } +}; + +// conversions demo: +alert(user); // hint: string -> {name: "John"} +alert(+user); // hint: number -> 1000 +alert(user + 500); // hint: default -> 1500 +``` + +As we can see from the code, `user` becomes a self-descriptive string or a money amount, depending on the conversion. The single method `user[Symbol.toPrimitive]` handles all conversion cases. + +## toString/valueOf + +If there's no `Symbol.toPrimitive` then JavaScript tries to find methods `toString` and `valueOf`: + +- For the `"string"` hint: call `toString` method, and if it doesn't exist or if it returns an object instead of a primitive value, then call `valueOf` (so `toString` has the priority for string conversions). +- For other hints: call `valueOf`, and if it doesn't exist or if it returns an object instead of a primitive value, then call `toString` (so `valueOf` has the priority for maths). + +Methods `toString` and `valueOf` come from ancient times. They are not symbols (symbols did not exist that long ago), but rather "regular" string-named methods. They provide an alternative "old-style" way to implement the conversion. + +These methods must return a primitive value. If `toString` or `valueOf` returns an object, then it's ignored (same as if there were no method). + +By default, a plain object has following `toString` and `valueOf` methods: + +- The `toString` method returns a string `"[object Object]"`. +- The `valueOf` method returns the object itself. + +Here's the demo: + +```js run +let user = {name: "John"}; + +alert(user); // [object Object] +alert(user.valueOf() === user); // true +``` + +So if we try to use an object as a string, like in an `alert` or so, then by default we see `[object Object]`. + +The default `valueOf` is mentioned here only for the sake of completeness, to avoid any confusion. As you can see, it returns the object itself, and so is ignored. Don't ask me why, that's for historical reasons. So we can assume it doesn't exist. + +Let's implement these methods to customize the conversion. + +For instance, here `user` does the same as above using a combination of `toString` and `valueOf` instead of `Symbol.toPrimitive`: + +```js run +let user = { + name: "John", + money: 1000, + + // for hint="string" + toString() { + return `{name: "${this.name}"}`; + }, + + // for hint="number" or "default" + valueOf() { + return this.money; + } + +}; + +alert(user); // toString -> {name: "John"} +alert(+user); // valueOf -> 1000 +alert(user + 500); // valueOf -> 1500 +``` + +As we can see, the behavior is the same as the previous example with `Symbol.toPrimitive`. + +Often we want a single "catch-all" place to handle all primitive conversions. In this case, we can implement `toString` only, like this: + +```js run +let user = { + name: "John", + + toString() { + return this.name; + } +}; + +alert(user); // toString -> John +alert(user + 500); // toString -> John500 +``` + +In the absence of `Symbol.toPrimitive` and `valueOf`, `toString` will handle all primitive conversions. + +### A conversion can return any primitive type + +The important thing to know about all primitive-conversion methods is that they do not necessarily return the "hinted" primitive. + +There is no control whether `toString` returns exactly a string, or whether `Symbol.toPrimitive` method returns a number for the hint `"number"`. + +The only mandatory thing: these methods must return a primitive, not an object. + +```smart header="Historical notes" +For historical reasons, if `toString` or `valueOf` returns an object, there's no error, but such value is ignored (like if the method didn't exist). That's because in ancient times there was no good "error" concept in JavaScript. + +In contrast, `Symbol.toPrimitive` is stricter, it *must* return a primitive, otherwise there will be an error. +``` + +## Further conversions + +As we know already, many operators and functions perform type conversions, e.g. multiplication `*` converts operands to numbers. + +If we pass an object as an argument, then there are two stages of calculations: +1. The object is converted to a primitive (using the rules described above). +2. If necessary for further calculations, the resulting primitive is also converted. + +For instance: + +```js run +let obj = { + // toString handles all conversions in the absence of other methods + toString() { + return "2"; + } +}; + +alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number +``` + +1. The multiplication `obj * 2` first converts the object to primitive (that's a string `"2"`). +2. Then `"2" * 2` becomes `2 * 2` (the string is converted to number). + +Binary plus will concatenate strings in the same situation, as it gladly accepts a string: + +```js run +let obj = { + toString() { + return "2"; + } +}; + +alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation +``` + +## Summary + +The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value. + +There are 3 types (hints) of it: +- `"string"` (for `alert` and other operations that need a string) +- `"number"` (for maths) +- `"default"` (few operators, usually objects implement it the same way as `"number"`) + +The specification describes explicitly which operator uses which hint. + +The conversion algorithm is: + +1. Call `obj[Symbol.toPrimitive](hint)` if the method exists, +2. Otherwise if hint is `"string"` + - try calling `obj.toString()` or `obj.valueOf()`, whatever exists. +3. Otherwise if hint is `"number"` or `"default"` + - try calling `obj.valueOf()` or `obj.toString()`, whatever exists. + +All these methods must return a primitive to work (if defined). + +In practice, it's often enough to implement only `obj.toString()` as a "catch-all" method for string conversions that should return a "human-readable" representation of an object, for logging or debugging purposes. diff --git a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md index a169f7769e..fd22a46539 100644 --- a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md +++ b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md @@ -6,26 +6,19 @@ let str = "Hello"; str.test = 5; // (*) -alert(str.test); +alert(str.test); ``` -There may be two kinds of result: -1. `undefined` -2. An error. +Depending on whether you have `use strict` or not, the result may be: +1. `undefined` (no strict mode) +2. An error (strict mode). Why? Let's replay what's happening at line `(*)`: 1. When a property of `str` is accessed, a "wrapper object" is created. -2. The operation with the property is carried out on it. So, the object gets the `test` property. -3. The operation finishes and the "wrapper object" disappears. - -So, on the last line, `str` has no trace of the property. A new wrapper object for every object operation on a string. - -Some browsers though may decide to further limit the programmer and disallow to assign properties to primitives at all. That's why in practice we can also see errors at line `(*)`. It's a little bit farther from the specification though. +2. In strict mode, writing into it is an error. +3. Otherwise, the operation with the property is carried on, the object gets the `test` property, but after that the "wrapper object" disappears, so in the last line `str` has no trace of the property. **This example clearly shows that primitives are not objects.** -They just can not store data. - -All property/method operations are performed with the help of temporary objects. - +They can't store additional data. diff --git a/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md index 50c781ea5d..208f84cc75 100644 --- a/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md +++ b/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md @@ -15,4 +15,4 @@ str.test = 5; alert(str.test); ``` -How do you think, will it work? What will be shown? +What do you think, will it work? What will be shown? 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 11253a8498..69e7196e97 100644 --- a/1-js/05-data-types/01-primitives-methods/article.md +++ b/1-js/05-data-types/01-primitives-methods/article.md @@ -1,18 +1,20 @@ # Methods of primitives -JavaScript allows us to work with primitives (strings, numbers etc) as if they were objects. +JavaScript allows us to work with primitives (strings, numbers, etc.) as if they were objects. They also provide methods to call as such. We will study those soon, but first we'll see how it works because, of course, primitives are not objects (and here we will make it even clearer). -They also provide methods to call as such. We will study those soon, but first we'll see how it works, because, of course, primitives are not objects (and here we will make it even more clear). - -Let's look at the key distinction between primitives and objects. +Let's look at the key distinctions between primitives and objects. A primitive +- Is a value of a primitive type. +- There are 7 primitive types: `string`, `number`, `bigint`, `boolean`, `symbol`, `null` and `undefined`. + An object -: Is capable of storing multiple values as properties. -Can be created with `{}`, for instance: `{name: "John", age: 30}`. There are other kinds of objects in JavaScript, e.g. functions are objects. -One of the best things about objects is that we can store a function as one of its properties: +- Is capable of storing multiple values as properties. +- Can be created with `{}`, for instance: `{name: "John", age: 30}`. There are other kinds of objects in JavaScript: functions, for example, are objects. + +One of the best things about objects is that we can store a function as one of its properties. ```js run let john = { @@ -27,28 +29,28 @@ john.sayHi(); // Hi buddy! So here we've made an object `john` with the method `sayHi`. -Many built-in objects already exist, such as those that work with dates, errors, HTML elements etc. They have different properties and methods. +Many built-in objects already exist, such as those that work with dates, errors, HTML elements, etc. They have different properties and methods. But, these features come with a cost! -Objects are "heavier" than primitives. They require additional resources to support the internal machinery. But as properties and methods are very useful in programming, JavaScript engines try to optimize them to reduce the additional burden. +Objects are "heavier" than primitives. They require additional resources to support the internal machinery. ## A primitive as an object Here's the paradox faced by the creator of JavaScript: -- There are many things one would want to do with a primitive like a string or a number. It would be great to access them as methods. +- There are many things one would want to do with a primitive, like a string or a number. It would be great to access them using methods. - Primitives must be as fast and lightweight as possible. The solution looks a little bit awkward, but here it is: 1. Primitives are still primitive. A single value, as desired. 2. The language allows access to methods and properties of strings, numbers, booleans and symbols. -3. When this happens, a special "object wrapper" is created that provides the extra functionality, and then is destroyed. +3. In order for that to work, a special "object wrapper" that provides the extra functionality is created, and then is destroyed. -The "object wrappers" are different for each primitive type and are called: `String`, `Number`, `Boolean` and `Symbol`. Thus, they provide different sets of methods. +The "object wrappers" are different for each primitive type and are called: `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. Thus, they provide different sets of methods. -For instance, there exists a method [str.toUpperCase()](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) that returns a capitalized string. +For instance, there exists a string method [str.toUpperCase()](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) that returns a capitalized `str`. Here's how it works: @@ -80,31 +82,32 @@ We'll see more specific methods in chapters and . ````warn header="Constructors `String/Number/Boolean` are for internal use only" -Some languages like Java allow us to create "wrapper objects" for primitives explicitly using a syntax like `new Number(1)` or `new Boolean(false)`. +Some languages like Java allow us to explicitly create "wrapper objects" for primitives using a syntax like `new Number(1)` or `new Boolean(false)`. In JavaScript, that's also possible for historical reasons, but highly **unrecommended**. Things will go crazy in several places. For instance: ```js run -alert( typeof 1 ); // "number" +alert( typeof 0 ); // "number" -alert( typeof new Number(1) ); // "object"! +alert( typeof new Number(0) ); // "object"! ``` -And because what follows, `zero`, is an object, the alert will show up: +Objects are always truthy in `if`, so here the alert will show up: ```js run let zero = new Number(0); if (zero) { // zero is true, because it's an object - alert( "zero is truthy?!?" ); + alert( "zero is truthy!?!" ); } ``` -On the other hand, using the same functions `String/Number/Boolean` without `new` is a totally sane and useful thing. They convert a value to the corresponding type: to a string, a number, or a boolean (primitive). +On the other hand, using the same functions `String/Number/Boolean` without `new` is totally fine and useful thing. They convert a value to the corresponding type: to a string, a number, or a boolean (primitive). For example, this is entirely valid: + ```js let num = Number("123"); // convert a string to number ``` 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 a17a4671a1..4bcd74512a 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 @@ Note that `63.5` has no precision loss at all. That's because the decimal part ` ```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/3-repeat-until-number/_js.view/test.js b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js index 219fa8068e..6bd0123dba 100644 --- a/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js +++ b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js @@ -18,7 +18,7 @@ describe("readNumber", function() { assert.strictEqual(readNumber(), 0); }); - it("continues the loop unti meets a number", function() { + it("continues the loop until meets a number", function() { prompt.onCall(0).returns("not a number"); prompt.onCall(1).returns("not a number again"); prompt.onCall(2).returns("1"); @@ -35,4 +35,4 @@ describe("readNumber", function() { assert.isNull(readNumber()); }); -}); \ No newline at end of file +}); diff --git a/1-js/05-data-types/02-number/8-random-min-max/solution.md b/1-js/05-data-types/02-number/8-random-min-max/solution.md index 348f9e34bc..8736c3d561 100644 --- a/1-js/05-data-types/02-number/8-random-min-max/solution.md +++ b/1-js/05-data-types/02-number/8-random-min-max/solution.md @@ -2,7 +2,7 @@ We need to "map" all values from the interval 0..1 into values from `min` to `ma That can be done in two stages: -1. If we multiply a random number from 0..1 by `max-min`, then it the interval of possible values increases `0..1` to `0..max-min`. +1. If we multiply a random number from 0..1 by `max-min`, then the interval of possible values increases `0..1` to `0..max-min`. 2. Now if we add `min`, the possible interval becomes from `min` to `max`. The function: diff --git a/1-js/05-data-types/02-number/9-random-int-min-max/task.md b/1-js/05-data-types/02-number/9-random-int-min-max/task.md index 29341b2af0..4ac7b5fbbd 100644 --- a/1-js/05-data-types/02-number/9-random-int-min-max/task.md +++ b/1-js/05-data-types/02-number/9-random-int-min-max/task.md @@ -12,9 +12,9 @@ Any number from the interval `min..max` must appear with the same probability. Examples of its work: ```js -alert( random(1, 5) ); // 1 -alert( random(1, 5) ); // 3 -alert( random(1, 5) ); // 5 +alert( randomInteger(1, 5) ); // 1 +alert( randomInteger(1, 5) ); // 3 +alert( randomInteger(1, 5) ); // 5 ``` You can use the solution of the [previous task](info:task/random-min-max) as the base. diff --git a/1-js/05-data-types/02-number/article.md b/1-js/05-data-types/02-number/article.md index b4a3962b3a..96a7b622a3 100644 --- a/1-js/05-data-types/02-number/article.md +++ b/1-js/05-data-types/02-number/article.md @@ -1,8 +1,12 @@ # Numbers -All numbers in JavaScript are stored in 64-bit format [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), also known as "double precision". +In modern JavaScript, there are two types of numbers: -Let's recap and expand upon what we currently know about them. +1. Regular numbers in JavaScript are stored in 64-bit format [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754), also known as "double precision floating point numbers". These are numbers that we're using most of the time, and we'll talk about them in this chapter. + +2. BigInt numbers represent integers of arbitrary length. They are sometimes needed because a regular integer number can't safely exceed (253-1) or be less than -(253-1), as we mentioned earlier in the chapter . As bigints are used in a few special areas, we devote them to a special chapter . + +So here we'll talk about regular numbers. Let's expand our knowledge of them. ## More ways to write a number @@ -12,46 +16,56 @@ Imagine we need to write 1 billion. The obvious way is: let billion = 1000000000; ``` -But in real life we usually avoid writing a long string of zeroes as it's easy to mistype. Also, we are lazy. We will usually write something like `"1bn"` for a billion or `"7.3bn"` for 7 billion 300 million. The same is true for most large numbers. +We also can use underscore `_` as the separator: + +```js +let billion = 1_000_000_000; +``` + +Here the underscore `_` plays the role of the "[syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar)", it makes the number more readable. The JavaScript engine simply ignores `_` between digits, so it's exactly the same one billion as above. -In JavaScript, we shorten a number by appending the letter `"e"` to the number and specifying the zeroes count: +In real life though, we try to avoid writing long sequences of zeroes. We're too lazy for that. We'll try to write something like `"1bn"` for a billion or `"7.3bn"` for 7 billion 300 million. The same is true for most large numbers. + +In JavaScript, we can shorten a number by appending the letter `"e"` to it and specifying the zeroes count: ```js run let billion = 1e9; // 1 billion, literally: 1 and 9 zeroes -alert( 7.3e9 ); // 7.3 billions (7,300,000,000) +alert( 7.3e9 ); // 7.3 billions (same as 7300000000 or 7_300_000_000) ``` -In other words, `"e"` multiplies the number by `1` with the given zeroes count. +In other words, `e` multiplies the number by `1` with the given zeroes count. ```js -1e3 = 1 * 1000 -1.23e6 = 1.23 * 1000000 +1e3 === 1 * 1000; // e3 means *1000 +1.23e6 === 1.23 * 1000000; // e6 means *1000000 ``` - -Now let's write something very small. Say, 1 microsecond (one millionth of a second): +Now let's write something very small. Say, 1 microsecond (one-millionth of a second): ```js -let ms = 0.000001; +let mсs = 0.000001; ``` -Just like before, using `"e"` can help. If we'd like to avoid writing the zeroes explicitly, we could say: +Just like before, using `"e"` can help. If we'd like to avoid writing the zeroes explicitly, we could write the same as: ```js -let ms = 1e-6; // six zeroes to the left from 1 +let mcs = 1e-6; // five zeroes to the left from 1 ``` -If we count the zeroes in `0.000001`, there are 6 of them. So naturally it's `1e-6`. +If we count the zeroes in `0.000001`, there are 6 of them. So naturally it's `1e-6`. In other words, a negative number after `"e"` means a division by 1 with the given number of zeroes: ```js // -3 divides by 1 with 3 zeroes -1e-3 = 1 / 1000 (=0.001) +1e-3 === 1 / 1000; // 0.001 // -6 divides by 1 with 6 zeroes -1.23e-6 = 1.23 / 1000000 (=0.00000123) +1.23e-6 === 1.23 / 1000000; // 0.00000123 + +// an example with a bigger number +1234e-2 === 1234 / 100; // 12.34, decimal point moves 2 times ``` ### Hex, binary and octal numbers @@ -89,13 +103,13 @@ alert( num.toString(16) ); // ff alert( num.toString(2) ); // 11111111 ``` -The `base` can vary from `2` to `36`. By default it's `10`. +The `base` can vary from `2` to `36`. By default, it's `10`. Common use cases for this are: - **base=16** is used for hex colors, character encodings etc, digits can be `0..9` or `A..F`. - **base=2** is mostly for debugging bitwise operations, digits can be `0` or `1`. -- **base=36** is the maximum, digits can be `0..9` or `A..Z`. The whole latin alphabet is used to represent a number. A funny, but useful case for `36` is when we need to turn a long numeric identifier into something shorter, for example to make a short url. Can simply represent it in the numeral system with base `36`: +- **base=36** is the maximum, digits can be `0..9` or `A..Z`. The whole Latin alphabet is used to represent a number. A funny, but useful case for `36` is when we need to turn a long numeric identifier into something shorter, for example, to make a short url. Can simply represent it in the numeral system with base `36`: ```js run alert( 123456..toString(36) ); // 2n9c @@ -107,6 +121,7 @@ Please note that two dots in `123456..toString(36)` is not a typo. If we want to If we placed a single dot: `123456.toString(36)`, then there would be an error, because JavaScript syntax implies the decimal part after the first dot. And if we place one more dot, then JavaScript knows that the decimal part is empty and now goes the method. Also could write `(123456).toString(36)`. + ``` ## Rounding @@ -122,7 +137,7 @@ There are several built-in functions for rounding: : Rounds up: `3.1` becomes `4`, and `-1.1` becomes `-1`. `Math.round` -: Rounds to the nearest integer: `3.1` becomes `3`, `3.6` becomes `4` and `-1.1` becomes `-1`. +: Rounds to the nearest integer: `3.1` becomes `3`, `3.6` becomes `4`. In the middle cases `3.5` rounds up to `4`, and `-3.5` rounds up to `-3`. `Math.trunc` (not supported by Internet Explorer) : Removes anything after the decimal point without rounding: `3.1` becomes `3`, `-1.1` becomes `-1`. @@ -132,8 +147,10 @@ Here's the table to summarize the differences between them: | | `Math.floor` | `Math.ceil` | `Math.round` | `Math.trunc` | |---|---------|--------|---------|---------| |`3.1`| `3` | `4` | `3` | `3` | +|`3.5`| `3` | `4` | `4` | `3` | |`3.6`| `3` | `4` | `4` | `3` | |`-1.1`| `-2` | `-1` | `-1` | `-1` | +|`-1.5`| `-2` | `-1` | `-1` | `-1` | |`-1.6`| `-2` | `-1` | `-2` | `-1` | @@ -149,11 +166,11 @@ There are two ways to do so: ```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. The method [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) rounds the number to `n` digits after the point and returns a string representation of the result. - + ```js run let num = 12.34; alert( num.toFixed(1) ); // "12.3" @@ -166,34 +183,34 @@ There are two ways to do so: alert( num.toFixed(1) ); // "12.4" ``` - Please note that result of `toFixed` is a string. If the decimal part is shorter than required, zeroes are appended to the end: + Please note that the result of `toFixed` is a string. If the decimal part is shorter than required, zeroes are appended to the end: ```js run let num = 12.34; - alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits + alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits ``` - We can convert it to a number using the unary plus or a `Number()` call: `+num.toFixed(5)`. + We can convert it to a number using the unary plus or a `Number()` call, e.g. write `+num.toFixed(5)`. ## Imprecise calculations -Internally, a number is represented in 64-bit format [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), so there are exactly 64 bits to store a number: 52 of them are used to store the digits, 11 of them store the position of the decimal point (they are zero for integer numbers), and 1 bit is for the sign. +Internally, a number is represented in 64-bit format [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754), so there are exactly 64 bits to store a number: 52 of them are used to store the digits, 11 of them store the position of the decimal point, and 1 bit is for the sign. -If a number is too big, it would overflow the 64-bit storage, potentially giving an infinity: +If a number is really huge, it may overflow the 64-bit storage and become a special numeric value `Infinity`: ```js run -alert( 1e500 ); // Infinity +alert( 1e500 ); // Infinity ``` What may be a little less obvious, but happens quite often, is the loss of precision. -Consider this (falsy!) test: +Consider this (falsy!) equality test: ```js run alert( 0.1 + 0.2 == 0.3 ); // *!*false*/!* ``` -That's right, if we check whether the sum of `0.1` and `0.2` is `0.3`, we get `false`. +That's right, if we check whether the sum of `0.1` and `0.2` is `0.3`, we get `false`. Strange! What is it then if not `0.3`? @@ -201,19 +218,25 @@ Strange! What is it then if not `0.3`? alert( 0.1 + 0.2 ); // 0.30000000000000004 ``` -Ouch! There are more consequences than an incorrect comparison here. Imagine you're making an e-shopping site and the visitor puts `$0.10` and `$0.20` goods into their chart. The order total will be `$0.30000000000000004`. That would surprise anyone. +Ouch! Imagine you're making an e-shopping site and the visitor puts `$0.10` and `$0.20` goods into their cart. The order total will be `$0.30000000000000004`. That would surprise anyone. But why does this happen? -A number is stored in memory in its binary form, a sequence of ones and zeroes. But fractions like `0.1`, `0.2` that look simple in the decimal numeric system are actually unending fractions in their binary form. +A number is stored in memory in its binary form, a sequence of bits - ones and zeroes. But fractions like `0.1`, `0.2` that look simple in the decimal numeric system are actually unending fractions in their binary form. -In other words, what is `0.1`? It is one divided by ten `1/10`, one-tenth. In decimal numeral system such numbers are easily representable. Compare it to one-third: `1/3`. It becomes an endless fraction `0.33333(3)`. +```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 +``` + +What is `0.1`? It is one divided by ten `1/10`, one-tenth. In the decimal numeral system, such numbers are easily representable. Compare it to one-third: `1/3`. It becomes an endless fraction `0.33333(3)`. So, division by powers `10` is guaranteed to work well in the decimal system, but division by `3` is not. For the same reason, in the binary numeral system, the division by powers of `2` is guaranteed to work, but `1/10` becomes an endless binary fraction. There's just no way to store *exactly 0.1* or *exactly 0.2* using the binary system, just like there is no way to store one-third as a decimal fraction. -The numeric format IEEE-754 solves this by rounding to the nearest possible number. These rounding rules normally don't allow us to see that "tiny precision loss", so the number shows up as `0.3`. But beware, the loss still exists. +The numeric format IEEE-754 solves this by rounding to the nearest possible number. These rounding rules normally don't allow us to see that "tiny precision loss", but it exists. We can see this in action: ```js run @@ -227,40 +250,39 @@ That's why `0.1 + 0.2` is not exactly `0.3`. ```smart header="Not only JavaScript" The same issue exists in many other programming languages. -PHP, Java, C, Perl, Ruby give exactly the same result, because they are based on the same numeric format. +PHP, Java, C, Perl, and Ruby give exactly the same result, because they are based on the same numeric format. ``` -Can we work around the problem? Sure, there're a number of ways: - -1. We can round the result with the help of a method [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed): +Can we work around the problem? Sure, the most reliable method is to round the result with the help of a method [toFixed(n)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed): - ```js run - let sum = 0.1 + 0.2; - alert( sum.toFixed(2) ); // 0.30 - ``` +```js run +let sum = 0.1 + 0.2; +alert( sum.toFixed(2) ); // "0.30" +``` - Please note that `toFixed` always returns a string. It ensures that it has 2 digits after the decimal point. That's actually convenient if we have an e-shopping and need to show `$0.30`. For other cases, we can use the unary plus to coerce it into a number: +Please note that `toFixed` always returns a string. It ensures that it has 2 digits after the decimal point. That's actually convenient if we have an e-shopping and need to show `$0.30`. For other cases, we can use the unary plus to coerce it into a number: - ```js run - let sum = 0.1 + 0.2; - alert( +sum.toFixed(2) ); // 0.3 - ``` +```js run +let sum = 0.1 + 0.2; +alert( +sum.toFixed(2) ); // 0.3 +``` -2. We can temporarily turn numbers into integers for the maths and then revert it back. It works like this: +We also can temporarily multiply the numbers by 100 (or a bigger number) to turn them into integers, do the maths, and then divide back. Then, as we're doing maths with integers, the error somewhat decreases, but we still get it on division: - ```js run - alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 - ``` +```js run +alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 +alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001 +``` - This works because when we do `0.1 * 10 = 1` and `0.2 * 10 = 2` then both numbers become integers, and there's no precision loss. +So, the multiply/divide approach reduces the error, but doesn't remove it totally. -3. If we were dealing with a shop, then the most radical solution would be to store all prices in cents and use no fractions at all. But what if we apply a discount of 30%? In practice, totally evading fractions is rarely feasible, so the solutions above help avoid this pitfall. +Sometimes we could try to evade fractions at all. Like if we're dealing with a shop, then we can store prices in cents instead of dollars. But what if we apply a discount of 30%? In practice, totally evading fractions is rarely possible. Just round them to cut "tails" when needed. ````smart header="The funny thing" Try running this: ```js run -// Hello! I'm a self-increasing number! +// Hello! I'm a self-increasing number! alert( 9999999999999999 ); // shows 10000000000000000 ``` @@ -272,18 +294,16 @@ JavaScript doesn't trigger an error in such events. It does its best to fit the ```smart header="Two zeroes" Another funny consequence of the internal representation of numbers is the existence of two zeroes: `0` and `-0`. -That's because a sign is represented by a single bit, so every number can be positive or negative, including a zero. +That's because a sign is represented by a single bit, so it can be set or not set for any number including a zero. -In most cases the distinction is unnoticeable, because operators are suited to treat them as the same. +In most cases, the distinction is unnoticeable, because operators are suited to treat them as the same. ``` - - ## Tests: isFinite and isNaN Remember these two special numeric values? -- `Infinite` (and `-Infinite`) is a special numeric value that is greater (less) than anything. +- `Infinity` (and `-Infinity`) is a special numeric value that is greater (less) than anything. - `NaN` represents an error. They belong to the type `number`, but are not "normal" numbers, so there are special functions to check for them: @@ -296,7 +316,7 @@ They belong to the type `number`, but are not "normal" numbers, so there are spe alert( isNaN("str") ); // true ``` - But do we need this function? Can't we just use the comparison `=== NaN`? Sorry, but the answer is no. The value `NaN` is unique in that it does not equal anything, including itself: + But do we need this function? Can't we just use the comparison `=== NaN`? Unfortunately not. The value `NaN` is unique in that it does not equal anything, including itself: ```js run alert( NaN === NaN ); // false @@ -320,18 +340,46 @@ let num = +prompt("Enter a number", ''); alert( isFinite(num) ); ``` -Please note that an empty or a space-only string is treated as `0` in all numeric functions including `isFinite`. +Please note that an empty or a space-only string is treated as `0` in all numeric functions including `isFinite`. + +````smart header="`Number.isNaN` and `Number.isFinite`" +[Number.isNaN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) and [Number.isFinite](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite) methods are the more "strict" versions of `isNaN` and `isFinite` functions. They do not autoconvert their argument into a number, but check if it belongs to the `number` type instead. + +- `Number.isNaN(value)` returns `true` if the argument belongs to the `number` type and it is `NaN`. In any other case, it returns `false`. + + ```js run + alert( Number.isNaN(NaN) ); // true + alert( Number.isNaN("str" / 2) ); // true + + // Note the difference: + alert( Number.isNaN("str") ); // false, because "str" belongs to the string type, not the number type + alert( isNaN("str") ); // true, because isNaN converts string "str" into a number and gets NaN as a result of this conversion + ``` + +- `Number.isFinite(value)` returns `true` if the argument belongs to the `number` type and it is not `NaN/Infinity/-Infinity`. In any other case, it returns `false`. + + ```js run + alert( Number.isFinite(123) ); // true + alert( Number.isFinite(Infinity) ); // false + alert( Number.isFinite(2 / 0) ); // false -```smart header="Compare with `Object.is`" + // Note the difference: + alert( Number.isFinite("123") ); // false, because "123" belongs to the string type, not the number type + alert( isFinite("123") ); // true, because isFinite converts string "123" into a number 123 + ``` -There is a special built-in method [Object.is](mdn:js/Object/is) that compares values like `===`, but is more reliable for two edge cases: +In a way, `Number.isNaN` and `Number.isFinite` are simpler and more straightforward than `isNaN` and `isFinite` functions. In practice though, `isNaN` and `isFinite` are mostly used, as they're shorter to write. +```` -1. It works with `NaN`: `Object.is(NaN, NaN) === true`, that's a good thing. -2. Values `0` and `-0` are different: `Object.is(0, -0) === false`, it rarely matters, but these values technically are different. +```smart header="Comparison with `Object.is`" +There is a special built-in method `Object.is` that compares values like `===`, but is more reliable for two edge cases: -In all other cases, `Object.is(a, b)` is the same as `a === b`. +1. It works with `NaN`: `Object.is(NaN, NaN) === true`, that's a good thing. +2. Values `0` and `-0` are different: `Object.is(0, -0) === false`, technically that's correct because internally the number has a sign bit that may be different even if all other bits are zeroes. -This way of comparison is often used in JavaScript specification. When an internal algorithm needs to compare two values for being exactly the same, it uses `Object.is` (internally called [SameValue](https://tc39.github.io/ecma262/#sec-samevalue)). +In all other cases, `Object.is(a, b)` is the same as `a === b`. + +We mention `Object.is` here, because it's often used in JavaScript specification. When an internal algorithm needs to compare two values for being exactly the same, it uses `Object.is` (internally called [SameValue](https://tc39.github.io/ecma262/#sec-samevalue)). ``` @@ -345,11 +393,11 @@ alert( +"100px" ); // NaN The sole exception is spaces at the beginning or at the end of the string, as they are ignored. -But in real life we often have values in units, like `"100px"` or `"12pt"` in CSS. Also in many countries the currency symbol goes after the amount, so we have `"19€"` and would like to extract a numeric value out of that. +But in real life, we often have values in units, like `"100px"` or `"12pt"` in CSS. Also in many countries, the currency symbol goes after the amount, so we have `"19€"` and would like to extract a numeric value out of that. That's what `parseInt` and `parseFloat` are for. -They "read" a number from a string until they can. In case of an error, the gathered number is returned. The function `parseInt` returns an integer, whilst `parseFloat` will return a floating-point number: +They "read" a number from a string until they can't. In case of an error, the gathered number is returned. The function `parseInt` returns an integer, whilst `parseFloat` will return a floating-point number: ```js run alert( parseInt('100px') ); // 100 @@ -383,7 +431,7 @@ JavaScript has a built-in [Math](https://developer.mozilla.org/en/docs/Web/JavaS A few examples: `Math.random()` -: Returns a random number from 0 to 1 (not including 1) +: Returns a random number from 0 to 1 (not including 1). ```js run alert( Math.random() ); // 0.1234567894322 @@ -391,8 +439,8 @@ A few examples: alert( Math.random() ); // ... (any random numbers) ``` -`Math.max(a, b, c...)` / `Math.min(a, b, c...)` -: Returns the greatest/smallest from the arbitrary number of arguments. +`Math.max(a, b, c...)` and `Math.min(a, b, c...)` +: Returns the greatest and smallest from the arbitrary number of arguments. ```js run alert( Math.max(3, 5, -10, 0, 1) ); // 5 @@ -400,30 +448,37 @@ A few examples: ``` `Math.pow(n, power)` -: Returns `n` raised the given power +: Returns `n` raised to the given power. ```js run alert( Math.pow(2, 10) ); // 2 in power 10 = 1024 ``` -There are more functions and constants in `Math` object, including trigonometry, which you can find in the [docs for the Math](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math) object. +There are more functions and constants in `Math` object, including trigonometry, which you can find in the [docs for the Math object](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math). ## Summary -To write big numbers: +To write numbers with many zeroes: -- Append `"e"` with the zeroes count to the number. Like: `123e6` is `123` with 6 zeroes. -- A negative number after `"e"` causes the number to be divided by 1 with given zeroes. That's for one-millionth or such. +- Append `"e"` with the zeroes count to the number. Like: `123e6` is the same as `123` with 6 zeroes `123000000`. +- A negative number after `"e"` causes the number to be divided by 1 with given zeroes. E.g. `123e-6` means `0.000123` (`123` millionths). For different numeral systems: -- Can write numbers directly in hex (`0x`), octal (`0o`) and binary (`0b`) systems -- `parseInt(str, base)` parses an integer from any numeral system with base: `2 ≤ base ≤ 36`. +- Can write numbers directly in hex (`0x`), octal (`0o`) and binary (`0b`) systems. +- `parseInt(str, base)` parses the string `str` into an integer in numeral system with given `base`, `2 ≤ base ≤ 36`. - `num.toString(base)` converts a number to a string in the numeral system with the given `base`. +For regular number tests: + +- `isNaN(value)` converts its argument to a number and then tests it for being `NaN` +- `Number.isNaN(value)` checks whether its argument belongs to the `number` type, and if so, tests it for being `NaN` +- `isFinite(value)` converts its argument to a number and then tests it for not being `NaN/Infinity/-Infinity` +- `Number.isFinite(value)` checks whether its argument belongs to the `number` type, and if so, tests it for not being `NaN/Infinity/-Infinity` + For converting values like `12pt` and `100px` to a number: -- Use `parseInt/parseFloat` for the "soft" conversion, which reads a number from a string and then returns the value they could read before the error. +- Use `parseInt/parseFloat` for the "soft" conversion, which reads a number from a string and then returns the value they could read before the error. For fractions: @@ -432,6 +487,4 @@ For fractions: More mathematical functions: -- See the [Math](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math) object when you need them. The library is very small, but can cover basic needs. - - +- See the [Math](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math) object when you need them. The library is very small but can cover basic needs. diff --git a/1-js/05-data-types/03-string/1-ucfirst/solution.md b/1-js/05-data-types/03-string/1-ucfirst/solution.md index 4809cf1238..be5dd2aafe 100644 --- a/1-js/05-data-types/03-string/1-ucfirst/solution.md +++ b/1-js/05-data-types/03-string/1-ucfirst/solution.md @@ -6,16 +6,11 @@ But we can make a new string based on the existing one, with the uppercased firs let newStr = str[0].toUpperCase() + str.slice(1); ``` -There's a small problem though. If `str` is empty, then `str[0]` is undefined, so we'll get an error. +There's a small problem though. If `str` is empty, then `str[0]` is `undefined`, and as `undefined` doesn't have the `toUpperCase()` method, we'll get an error. -There are two variants here: +The easiest way out is to add a test for an empty string, like this: -1. Use `str.charAt(0)`, as it always returns a string (maybe empty). -2. Add a test for an empty string. - -Here's the 2nd variant: - -```js run +```js run demo function ucFirst(str) { if (!str) return str; @@ -24,4 +19,3 @@ function ucFirst(str) { alert( ucFirst("john") ); // John ``` - diff --git a/1-js/05-data-types/03-string/1-ucfirst/task.md b/1-js/05-data-types/03-string/1-ucfirst/task.md index c0e6ecac44..ed8a1e6a71 100644 --- a/1-js/05-data-types/03-string/1-ucfirst/task.md +++ b/1-js/05-data-types/03-string/1-ucfirst/task.md @@ -2,7 +2,7 @@ importance: 5 --- -# Uppercast the first character +# Uppercase the first character Write a function `ucFirst(str)` that returns the string `str` with the uppercased first character, for instance: diff --git a/1-js/05-data-types/03-string/2-check-spam/solution.md b/1-js/05-data-types/03-string/2-check-spam/solution.md index 2468916f1f..de8dde57d3 100644 --- a/1-js/05-data-types/03-string/2-check-spam/solution.md +++ b/1-js/05-data-types/03-string/2-check-spam/solution.md @@ -1,6 +1,6 @@ -To make the search case-insensitive, let's bring the stirng to lower case and then search: +To make the search case-insensitive, let's bring the string to lower case and then search: -```js run +```js run demo function checkSpam(str) { let lowerStr = str.toLowerCase(); diff --git a/1-js/05-data-types/03-string/2-check-spam/task.md b/1-js/05-data-types/03-string/2-check-spam/task.md index d073adc059..98b5dd8a04 100644 --- a/1-js/05-data-types/03-string/2-check-spam/task.md +++ b/1-js/05-data-types/03-string/2-check-spam/task.md @@ -4,7 +4,7 @@ importance: 5 # Check for spam -Write a function `checkSpam(str)` that returns `true` if `str` contains 'viagra' or 'XXX', otherwise `false. +Write a function `checkSpam(str)` that returns `true` if `str` contains 'viagra' or 'XXX', otherwise `false`. The function must be case-insensitive: 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 a49b709096..d51672ae64 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,11 +1,10 @@ The maximal length must be `maxlength`, so we need to cut it a little shorter, to give space for the ellipsis. -Note that there is actually a single unicode character for an ellipsis. That's not three dots. +Note that there is actually a single Unicode character for an ellipsis. That's not three dots. -```js run +```js run demo function truncate(str, maxlength) { - return (str.length > maxlength) ? + return (str.length > maxlength) ? str.slice(0, maxlength - 1) + '…' : str; } ``` - diff --git a/1-js/05-data-types/03-string/3-truncate/task.md b/1-js/05-data-types/03-string/3-truncate/task.md index 6382029f40..c99a5f15a1 100644 --- a/1-js/05-data-types/03-string/3-truncate/task.md +++ b/1-js/05-data-types/03-string/3-truncate/task.md @@ -11,7 +11,7 @@ The result of the function should be the truncated (if needed) string. For instance: ```js -truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…" +truncate("What I'd like to tell on this topic is:", 20) == "What I'd like to te…" -truncate("Hi everyone!", 20) = "Hi everyone!" +truncate("Hi everyone!", 20) == "Hi everyone!" ``` diff --git a/1-js/05-data-types/03-string/4-extract-currency/solution.md b/1-js/05-data-types/03-string/4-extract-currency/solution.md index 8b13789179..e69de29bb2 100644 --- a/1-js/05-data-types/03-string/4-extract-currency/solution.md +++ b/1-js/05-data-types/03-string/4-extract-currency/solution.md @@ -1 +0,0 @@ - diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md index 676675732c..60ce2b6f0b 100644 --- a/1-js/05-data-types/03-string/article.md +++ b/1-js/05-data-types/03-string/article.md @@ -17,7 +17,7 @@ let double = "double-quoted"; let backticks = `backticks`; ``` -Single and double quotes are essentially the same. Backticks, however, allow us to embed any expression into the string, including function calls: +Single and double quotes are essentially the same. Backticks, however, allow us to embed any expression into the string, by wrapping it in `${…}`: ```js run function sum(a, b) { @@ -39,60 +39,61 @@ let guestList = `Guests: alert(guestList); // a list of guests, multiple lines ``` -If we try to use single or double quotes in the same way, there will be an error: +Looks natural, right? But single or double quotes do not work this way. + +If we use them and try to use multiple lines, there'll be an error: + ```js run -let guestList = "Guests: // Error: Unexpected token ILLEGAL +let guestList = "Guests: // Error: Unexpected token ILLEGAL * John"; ``` -Single and double quotes come from ancient times of language creation when the need for multiline strings was not taken into account. Backticks appeared much later and thus are more versatile. - -Backticks also allow us to specify a "template function" before the first backtick. The syntax is: func`string`. The function `func` is called automatically, receives the string and embedded expressions and can process them. You can read more about it in the [docs](mdn:/JavaScript/Reference/Template_literals#Tagged_template_literals). This is called "tagged templates". This feature makes it easier to wrap strings into custom templating or other functionality, but it is rarely used. +Single and double quotes come from ancient times of language creation, when the need for multiline strings was not taken into account. Backticks appeared much later and thus are more versatile. +Backticks also allow us to specify a "template function" before the first backtick. The syntax is: func`string`. The function `func` is called automatically, receives the string and embedded expressions and can process them. This feature is called "tagged templates", it's rarely seen, but you can read about it in the MDN: [Template literals](mdn:/JavaScript/Reference/Template_literals#Tagged_templates). ## Special characters -It is still possible to create multiline strings with single quotes by using a so-called "newline character", written as `\n`, which denotes a line break: +It is still possible to create multiline strings with single and double quotes by using a so-called "newline character", written as `\n`, which denotes a line break: ```js run let guestList = "Guests:\n * John\n * Pete\n * Mary"; -alert(guestList); // a multiline list of guests +alert(guestList); // a multiline list of guests, same as above ``` -For example, these two lines describe the same: +As a simpler example, these two lines are equal, just written differently: ```js run -alert( "Hello\nWorld" ); // two lines using a "newline symbol" +let str1 = "Hello\nWorld"; // two lines using a "newline symbol" // two lines using a normal newline and backticks -alert( `Hello -World` ); +let str2 = `Hello +World`; + +alert(str1 == str2); // true ``` -There are other, less common "special" characters as well. Here's the list: +There are other, less common special characters: | Character | Description | |-----------|-------------| -|`\b`|Backspace| -|`\f`|Form feed| |`\n`|New line| -|`\r`|Carriage return| +|`\r`|In Windows text files a combination of two characters `\r\n` represents a new break, while on non-Windows OS it's just `\n`. That's for historical reasons, most Windows software also understands `\n`. | +|`\'`, `\"`, \\`|Quotes| +|`\\`|Backslash| |`\t`|Tab| -|`\uNNNN`|A unicode symbol with the hex code `NNNN`, for instance `\u00A9` -- is a unicode for the copyright symbol `©`. It must be exactly 4 hex digits. | -|`\u{NNNNNNNN}`|Some rare characters are encoded with two unicode symbols, taking up to 4 bytes. This long unicode requires braces around it.| +|`\b`, `\f`, `\v`| Backspace, Form Feed, Vertical Tab -- mentioned for completeness, coming from old times, not used nowadays (you can forget them right now). | -Examples with unicode: +As you can see, all special characters start with a backslash character `\`. It is also called an "escape character". + +Because it's so special, if we need to show an actual backslash `\` within the string, we need to double it: ```js run -alert( "\u00A9" ); // © -alert( "\u{20331}" ); // 佫, a rare chinese hieroglyph (long unicode) -alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long unicode) +alert( `The backslash: \\` ); // The backslash: \ ``` -All special characters start with a backslash character `\`. It is also called an "escape character". - -We would also use it if we want to insert a quote into the string. +So-called "escaped" quotes `\'`, `\"`, \\` are used to insert a quote into the same-quoted string. For instance: @@ -102,25 +103,16 @@ alert( 'I*!*\'*/!*m the Walrus!' ); // *!*I'm*/!* the Walrus! As you can see, we have to prepend the inner quote by the backslash `\'`, because otherwise it would indicate the string end. -Of course, that refers only to the quotes that are same as the enclosing ones. So, as a more elegant solution, we could switch to double quotes or backticks instead: +Of course, only the quotes that are the same as the enclosing ones need to be escaped. So, as a more elegant solution, we could switch to double quotes or backticks instead: ```js run -alert( `I'm the Walrus!` ); // I'm the Walrus! +alert( "I'm the Walrus!" ); // I'm the Walrus! ``` -Note that the backslash `\` serves for the correct reading of the string by JavaScript, then disappears. The in-memory string has no `\`. You can clearly see that in `alert` from the examples above. - -But what if we need to show an actual backslash `\` within the string? - -That's possible, but we need to double it like `\\`: - -```js run -alert( `The backslash: \\` ); // The backslash: \ -``` +Besides these special characters, there's also a special notation for Unicode codes `\u…`, it's rarely used and is covered in the optional chapter about [Unicode](info:unicode). ## String length - The `length` property has the string length: ```js run @@ -132,33 +124,36 @@ Note that `\n` is a single "special" character, so the length is indeed `3`. ```warn header="`length` is a property" People with a background in some other languages sometimes mistype by calling `str.length()` instead of just `str.length`. That doesn't work. -Please note that `str.length` is a numeric property, not a function. There is no need to add brackets after it. +Please note that `str.length` is a numeric property, not a function. There is no need to add parenthesis after it. Not `.length()`, but `.length`. ``` ## Accessing characters -To get a character at position `pos`, use square brackets `[pos]` or call the method [str.charAt(pos)](mdn:js/String/charAt). The first character starts from the zero position: +To get a character at position `pos`, use square brackets `[pos]` or call the method [str.at(pos)](mdn:js/String/at). The first character starts from the zero position: ```js run let str = `Hello`; // the first character alert( str[0] ); // H -alert( str.charAt(0) ); // H +alert( str.at(0) ); // H // the last character alert( str[str.length - 1] ); // o +alert( str.at(-1) ); ``` -The square brackets are a modern way of getting a character, while `charAt` exists mostly for historical reasons. +As you can see, the `.at(pos)` method has a benefit of allowing negative position. If `pos` is negative, then it's counted from the end of the string. -The only difference between them is that if no character is found, `[]` returns `undefined`, and `charAt` returns an empty string: +So `.at(-1)` means the last character, and `.at(-2)` is the one before it, etc. + +The square brackets always return `undefined` for negative indexes, for instance: ```js run let str = `Hello`; -alert( str[1000] ); // undefined -alert( str.charAt(1000) ); // '' (an empty string) +alert( str[-2] ); // undefined +alert( str.at(-2) ); // l ``` We can also iterate over characters using `for..of`: @@ -189,7 +184,7 @@ For instance: ```js run let str = 'Hi'; -str = 'h' + str[1]; // replace the string +str = 'h' + str[1]; // replace the string alert( str ); // hi ``` @@ -207,7 +202,7 @@ alert( 'Interface'.toLowerCase() ); // interface Or, if we want a single character lowercased: -```js +```js run alert( 'Interface'[0].toLowerCase() ); // 'i' ``` @@ -232,7 +227,7 @@ alert( str.indexOf('widget') ); // -1, not found, the search is case-sensitive alert( str.indexOf("id") ); // 1, "id" is found at the position 1 (..idget with id) ``` -The optional second parameter allows us to search starting from the given position. +The optional second parameter allows us to start searching from a given position. For instance, the first occurrence of `"id"` is at position `1`. To look for the next occurrence, let's start the search from position `2`: @@ -242,10 +237,8 @@ let str = 'Widget with id'; alert( str.indexOf('id', 2) ) // 12 ``` - If we're interested in all occurrences, we can run `indexOf` in a loop. Every new call is made with the position after the previous match: - ```js run let str = 'As sly as a fox, as strong as an ox'; @@ -275,8 +268,8 @@ while ((pos = str.indexOf(target, pos + 1)) != -1) { */!* ``` -```smart header="`str.lastIndexOf(pos)`" -There is also a similar method [str.lastIndexOf(pos)](mdn:js/String/lastIndexOf) that searches from the end of a string to its beginning. +```smart header="`str.lastIndexOf(substr, position)`" +There is also a similar method [str.lastIndexOf(substr, position)](mdn:js/String/lastIndexOf) that searches from the end of a string to its beginning. It would list the occurrences in the reverse order. ``` @@ -305,41 +298,6 @@ if (str.indexOf("Widget") != -1) { } ``` -````smart header="The bitwise NOT trick" -One of the old tricks used here is the [bitwise NOT](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Bitwise_NOT) `~` operator. It converts the number to a 32-bit integer (removes the decimal part if exists) and then reverses all bits in its binary representation. - -For 32-bit integers the call `~n` means exactly the same as `-(n+1)` (due to IEEE-754 format). - -For instance: - -```js run -alert( ~2 ); // -3, the same as -(2+1) -alert( ~1 ); // -2, the same as -(1+1) -alert( ~0 ); // -1, the same as -(0+1) -*!* -alert( ~-1 ); // 0, the same as -(-1+1) -*/!* -``` - -As we can see, `~n` is zero only if `n == -1`. - -So, the test `if ( ~str.indexOf("...") )` is truthy that the result of `indexOf` is not `-1`. In other words, when there is a match. - -People use it to shorten `indexOf` checks: - -```js run -let str = "Widget"; - -if (~str.indexOf("Widget")) { - alert( 'Found it!' ); // works -} -``` - -It is usually not recommended to use language features in a non-obvious way, but this particular trick is widely used in old code, so we should understand it. - -Just remember: `if (~str.indexOf(...))` reads as "if found". -```` - ### includes, startsWith, endsWith The more modern method [str.includes(substr, pos)](mdn:js/String/includes) returns `true/false` depending on whether `str` contains `substr` within. @@ -355,15 +313,15 @@ alert( "Hello".includes("Bye") ); // false The optional second argument of `str.includes` is the position to start searching from: ```js run -alert( "Midget".includes("id") ); // true -alert( "Midget".includes("id", 3) ); // false, from position 3 there is no "id" +alert( "Widget".includes("id") ); // true +alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id" ``` The methods [str.startsWith](mdn:js/String/startsWith) and [str.endsWith](mdn:js/String/endsWith) do exactly what they say: ```js run -alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid" -alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get" +alert( "*!*Wid*/!*get".startsWith("Wid") ); // true, "Widget" starts with "Wid" +alert( "Wid*!*get*/!*".endsWith("get") ); // true, "Widget" ends with "get" ``` ## Getting a substring @@ -385,7 +343,7 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and ```js run let str = "st*!*ringify*/!*"; - alert( str.slice(2) ); // ringify, from the 2nd position till the end + alert( str.slice(2) ); // 'ringify', from the 2nd position till the end ``` Negative values for `start/end` are also possible. They mean the position is counted from the string end: @@ -394,18 +352,16 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and let str = "strin*!*gif*/!*y"; // start at the 4th position from the right, end at the 1st from the right - alert( str.slice(-4, -1) ); // gif + alert( str.slice(-4, -1) ); // 'gif' ``` - `str.substring(start [, end])` -: Returns the part of the string *between* `start` and `end`. +: Returns the part of the string *between* `start` and `end` (not including `end`). - This is almost the same as `slice`, but it allows `start` to be greater than `end`. + This is almost the same as `slice`, but it allows `start` to be greater than `end` (in this case it simply swaps `start` and `end` values). For instance: - ```js run let str = "st*!*ring*/!*ify"; @@ -421,7 +377,6 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and Negative arguments are (unlike slice) not supported, they are treated as `0`. - `str.substr(start [, length])` : Returns the part of the string from `start`, with the given `length`. @@ -429,29 +384,32 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and ```js run let str = "st*!*ring*/!*ify"; - alert( str.substr(2, 4) ); // ring, from the 2nd position get 4 characters + alert( str.substr(2, 4) ); // 'ring', from the 2nd position get 4 characters ``` The first argument may be negative, to count from the end: ```js run let str = "strin*!*gi*/!*fy"; - alert( str.substr(-4, 2) ); // gi, from the 4th position get 2 characters + alert( str.substr(-4, 2) ); // 'gi', from the 4th position get 2 characters ``` + This method resides in the [Annex B](https://tc39.es/ecma262/#sec-string.prototype.substr) of the language specification. It means that only browser-hosted Javascript engines should support it, and it's not recommended to use it. In practice, it's supported everywhere. + Let's recap these methods to avoid any confusion: | method | selects... | negatives | |--------|-----------|-----------| | `slice(start, end)` | from `start` to `end` (not including `end`) | allows negatives | -| `substring(start, end)` | between `start` and `end` | negative values mean `0` | +| `substring(start, end)` | between `start` and `end` (not including `end`)| negative values mean `0` | | `substr(start, length)` | from `start` get `length` characters | allows negative `start` | - ```smart header="Which one to choose?" All of them can do the job. Formally, `substr` has a minor drawback: it is described not in the core JavaScript specification, but in Annex B, which covers browser-only features that exist mainly for historical reasons. So, non-browser environments may fail to support it. But in practice it works everywhere. -The author finds themself using `slice` almost all the time. +Of the other two variants, `slice` is a little bit more flexible, it allows negative arguments and shorter to write. + +So, for practical use it's enough to remember only `slice`. ``` ## Comparing strings @@ -474,17 +432,18 @@ Although, there are some oddities. This may lead to strange results if we sort these country names. Usually people would expect `Zealand` to come after `Österreich` in the list. -To understand what happens, let's review the internal representation of strings in JavaScript. +To understand what happens, we should be aware that strings in Javascript are encoded using [UTF-16](https://en.wikipedia.org/wiki/UTF-16). That is: each character has a corresponding numeric code. -All strings are encoded using [UTF-16](https://en.wikipedia.org/wiki/UTF-16). That is: each character has a corresponding numeric code. There are special methods that allow to get the character for the code and back. +There are special methods that allow to get the character for the code and back: `str.codePointAt(pos)` -: Returns the code for the character at position `pos`: +: Returns a decimal number representing the code for the character at position `pos`: ```js run // different case letters have different codes - alert( "z".codePointAt(0) ); // 122 alert( "Z".codePointAt(0) ); // 90 + alert( "z".codePointAt(0) ); // 122 + alert( "z".codePointAt(0).toString(16) ); // 7a (if we need a hexadecimal value) ``` `String.fromCodePoint(code)` @@ -492,13 +451,7 @@ All strings are encoded using [UTF-16](https://en.wikipedia.org/wiki/UTF-16). Th ```js run alert( String.fromCodePoint(90) ); // Z - ``` - - We can also add unicode characters by their codes using `\u` followed by the hex code: - - ```js run - // 90 is 5a in hexadecimal system - alert( '\u005a' ); // Z + alert( String.fromCodePoint(0x5a) ); // Z (we can also use a hex value as an argument) ``` Now let's see the characters with codes `65..220` (the latin alphabet and a little bit extra) by making a string of them: @@ -510,35 +463,35 @@ for (let i = 65; i <= 220; i++) { str += String.fromCodePoint(i); } alert( str ); +// Output: // ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„ // ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ ``` -See? Capital characters go first, then a few special ones, then lowercase characters. +See? Capital characters go first, then a few special ones, then lowercase characters, and `Ö` near the end of the output. Now it becomes obvious why `a > Z`. The characters are compared by their numeric code. The greater code means that the character is greater. The code for `a` (97) is greater than the code for `Z` (90). - All lowercase letters go after uppercase letters because their codes are greater. -- Some letters like `Ö` stand apart from the main alphabet. Here, it's code is greater than anything from `a` to `z`. - +- Some letters like `Ö` stand apart from the main alphabet. Here, its code is greater than anything from `a` to `z`. -### Correct comparisons +### Correct comparisons [#correct-comparisons] -The "right" algorithm to do string comparisons is more complex than it may seem, because alphabets are different for different languages. The same-looking letter may be located differently in different alphabets. +The "right" algorithm to do string comparisons is more complex than it may seem, because alphabets are different for different languages. So, the browser needs to know the language to compare. -Luckily, all modern browsers (IE10- requires the additional library [Intl.JS](https://github.com/andyearnshaw/Intl.js/)) support the internationalization standard [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf). +Luckily, modern browsers support the internationalization standard [ECMA-402](https://www.ecma-international.org/publications-and-standards/standards/ecma-402/). It provides a special method to compare strings in different languages, following their rules. -The call [str.localeCompare(str2)](mdn:js/String/localeCompare): +The call [str.localeCompare(str2)](mdn:js/String/localeCompare) returns an integer indicating whether `str` is less, equal or greater than `str2` according to the language rules: -- Returns `1` if `str` is greater than `str2` according to the language rules. -- Returns `-1` if `str` is less than `str2`. -- Returns `0` if they are equal. +- Returns a negative number if `str` is less than `str2`. +- Returns a positive number if `str` is greater than `str2`. +- Returns `0` if they are equivalent. For instance: @@ -546,120 +499,13 @@ For instance: alert( 'Österreich'.localeCompare('Zealand') ); // -1 ``` -This method actually has two additional arguments specified in [the documentation](mdn:js/String/localeCompare), which allows it to specify the language (by default taken from the environment) and setup additional rules like case sensitivity or should `"a"` and `"á"` be treated as the same etc. - -## Internals, Unicode - -```warn header="Advanced knowledge" -The section goes deeper into string internals. This knowledge will be useful for you if you plan to deal with emoji, rare mathematical of hieroglyphs characters or other rare symbols. - -You can skip the section if you don't plan to support them. -``` - -### Surrogate pairs - -Most symbols have a 2-byte code. Letters in most european languages, numbers, and even most hieroglyphs, have a 2-byte representation. - -But 2 bytes only allow 65536 combinations and that's not enough for every possible symbol. So rare symbols are encoded with a pair of 2-byte characters called "a surrogate pair". - -The length of such symbols is `2`: - -```js run -alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X -alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY -alert( '𩷶'.length ); // 2, a rare chinese hieroglyph -``` - -Note that surrogate pairs did not exist at the time when JavaScript was created, and thus are not correctly processed by the language! - -We actually have a single symbol in each of the strings above, but the `length` shows a length of `2`. - -`String.fromCodePoint` and `str.codePointAt` are few rare methods that deal with surrogate pairs right. They recently appeared in the language. Before them, there were only [String.fromCharCode](mdn:js/String/fromCharCode) and [str.charCodeAt](mdn:js/String/charCodeAt). These methods are actually the same as `fromCodePoint/codePointAt`, but don't work with surrogate pairs. - -But, for instance, getting a symbol can be tricky, because surrogate pairs are treated as two characters: - -```js run -alert( '𝒳'[0] ); // strange symbols... -alert( '𝒳'[1] ); // ...pieces of the surrogate pair -``` - -Note that pieces of the surrogate pair have no meaning without each other. So the alerts in the example above actually display garbage. - -Technically, surrogate pairs are also detectable by their codes: if a character has the code in the interval of `0xd800..0xdbff`, then it is the first part of the surrogate pair. The next character (second part) must have the code in interval `0xdc00..0xdfff`. These intervals are reserved exclusively for surrogate pairs by the standard. - -In the case above: - -```js run -// charCodeAt is not surrogate-pair aware, so it gives codes for parts - -alert( '𝒳'.charCodeAt(0).toString(16) ); // d835, between 0xd800 and 0xdbff -alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3, between 0xdc00 and 0xdfff -``` - -You will find more ways to deal with surrogate pairs later in the chapter . There are probably special libraries for that too, but nothing famous enough to suggest here. - -### Diacritical marks and normalization - -In many languages there are symbols that are composed of the base character with a mark above/under it. - -For instance, the letter `a` can be the base character for: `àáâäãåā`. Most common "composite" character have their own code in the UTF-16 table. But not all of them, because there are too many possible combinations. - -To support arbitrary compositions, UTF-16 allows us to use several unicode characters. The base character and one or many "mark" characters that "decorate" it. - -For instance, if we have `S` followed by the special "dot above" character (code `\u0307`), it is shown as Ṡ. - -```js run -alert( 'S\u0307' ); // Ṡ -``` - -If we need an additional mark above the letter (or below it) -- no problem, just add the necessary mark character. - -For instance, if we append a character "dot below" (code `\u0323`), then we'll have "S with dots above and below": `Ṩ`. - -For example: - -```js run -alert( 'S\u0307\u0323' ); // Ṩ -``` - -This provides great flexibility, but also an interesting problem: two characters may visually look the same, but be represented with different unicode compositions. - -For instance: - -```js run -alert( 'S\u0307\u0323' ); // Ṩ, S + dot above + dot below -alert( 'S\u0323\u0307' ); // Ṩ, S + dot below + dot above - -alert( 'S\u0307\u0323' == 'S\u0323\u0307' ); // false -``` - -To solve this, there exists a "unicode normalization" algorithm that brings each string to the single "normal" form. - -It is implemented by [str.normalize()](mdn:js/String/normalize). - -```js run -alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true -``` - -It's funny that in our situation `normalize()` actually brings together a sequence of 3 characters to one: `\u1e68` (S with two dots). - -```js run -alert( "S\u0307\u0323".normalize().length ); // 1 - -alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true -``` - -In reality, this is not always the case. The reason being that the symbol `Ṩ` is "common enough", so UTF-16 creators included it in the main table and gave it the code. - -If you want to learn more about normalization rules and variants -- they are described in the appendix of the Unicode standard: [Unicode Normalization Forms](http://www.unicode.org/reports/tr15/), but for most practical purposes the information from this section is enough. - +This method actually has two additional arguments specified in [the documentation](mdn:js/String/localeCompare), which allows it to specify the language (by default taken from the environment, letter order depends on the language) and setup additional rules like case sensitivity or should `"a"` and `"á"` be treated as the same etc. ## Summary -- There are 3 types of quotes. Backticks allow a string to span multiple lines and embed expressions. -- Strings in JavaScript are encoded using UTF-16. -- We can use special characters like `\n` and insert letters by their unicode using `\u...`. -- To get a character, use: `[]`. +- There are 3 types of quotes. Backticks allow a string to span multiple lines and embed expressions `${…}`. +- We can use special characters, such as a line break `\n`. +- To get a character, use: `[]` or `at` method. - To get a substring, use: `slice` or `substring`. - To lowercase/uppercase a string, use: `toLowerCase/toUpperCase`. - To look for a substring, use: `indexOf`, or `includes/startsWith/endsWith` for simple checks. @@ -669,6 +515,8 @@ There are several other helpful methods in strings: - `str.trim()` -- removes ("trims") spaces from the beginning and end of the string. - `str.repeat(n)` -- repeats the string `n` times. -- ...and more. See the [manual](mdn:js/String) for details. +- ...and more to be found in the [manual](mdn:js/String). + +Strings also have methods for doing search/replace with regular expressions. But that's big topic, so it's explained in a separate tutorial section . -Strings also have methods for doing search/replace with regular expressions. But that topic deserves a separate chapter, so we'll return to that later. +Also, as of now it's important to know that strings are based on Unicode encoding, and hence there're issues with comparisons. There's more about Unicode in the chapter . diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js b/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js index 143ad53451..b44e76fe7e 100644 --- a/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js +++ b/1-js/05-data-types/04-array/10-maximal-subarray/_js.view/test.js @@ -30,4 +30,8 @@ describe("getMaxSubSum", function() { it("maximal subsum of [-1, -2] equals 0", function() { assert.equal(getMaxSubSum([-1, -2]), 0); }); -}); \ No newline at end of file + + it("maximal subsum of [2, -8, 5, -1, 2, -3, 2] equals 6", function() { + assert.equal(getMaxSubSum([2, -8, 5, -1, 2, -3, 2]), 6); + }); +}); 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 a2ba50bfc2..7e1ca3bde2 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 @@ -1,4 +1,4 @@ -# The slow solution +# Slow solution We can calculate all possible subsums. @@ -29,8 +29,8 @@ For instance, for `[-1, 2, 3, -9, 11]`: -9 -9 + 11 -// Starting from -11 --11 +// Starting from 11 +11 ``` The code is actually a nested loop: the external loop over array elements, and the internal counts subsums starting with the current element. @@ -57,9 +57,9 @@ alert( getMaxSubSum([1, 2, 3]) ); // 6 alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100 ``` -The solution has a time complexety of [O(n2)](https://en.wikipedia.org/wiki/Big_O_notation). In other words, if we increase the array size 2 times, the algorithm will work 4 times longer. +The solution has a time complexity of [O(n2)](https://en.wikipedia.org/wiki/Big_O_notation). In other words, if we increase the array size 2 times, the algorithm will work 4 times longer. -For big arrays (1000, 10000 or more items) such algorithms can lead to a seroius sluggishness. +For big arrays (1000, 10000 or more items) such algorithms can lead to serious sluggishness. # Fast solution @@ -67,7 +67,7 @@ Let's walk the array and keep the current partial sum of elements in the variabl If the description is too vague, please see the code, it's short enough: -```js run +```js run demo function getMaxSubSum(arr) { let maxSum = 0; let partialSum = 0; @@ -91,4 +91,4 @@ alert( getMaxSubSum([-1, -2, -3]) ); // 0 The algorithm requires exactly 1 array pass, so the time complexity is O(n). -You can find more detail information about the algorithm here: [Maximum subarray problem](http://en.wikipedia.org/wiki/Maximum_subarray_problem). If it's still not obvious why that works, then please trace the algorithm on the examples above, see how it works, that's better than any words. +You can find more detailed information about the algorithm here: [Maximum subarray problem](http://en.wikipedia.org/wiki/Maximum_subarray_problem). If it's still not obvious why that works, then please trace the algorithm on the examples above, see how it works, that's better than any words. 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 b5ffc4c293..f1a1d9f956 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 @@ -8,17 +8,17 @@ The input is an array of numbers, e.g. `arr = [1, -2, 3, 4, -9, 6]`. The task is: find the contiguous subarray of `arr` with the maximal sum of items. -Write the function `getMaxSubSum(arr)` that will find return that sum. +Write the function `getMaxSubSum(arr)` that will return that sum. -For instance: +For instance: ```js -getMaxSubSum([-1, *!*2, 3*/!*, -9]) = 5 (the sum of highlighted items) -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 (take all) +getMaxSubSum([-1, *!*2, 3*/!*, -9]) == 5 (the sum of highlighted items) +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 (take all) ``` If all items are negative, it means that we take none (the subarray is empty), so the sum is zero: @@ -27,4 +27,4 @@ If all items are negative, it means that we take none (the subarray is empty), s getMaxSubSum([-1, -2, -3]) = 0 ``` -Please try to think of a fast solution: [O(n2)](https://en.wikipedia.org/wiki/Big_O_notation) or even O(n) if you can. \ No newline at end of file +Please try to think of a fast solution: [O(n2)](https://en.wikipedia.org/wiki/Big_O_notation) or even O(n) if you can. diff --git a/1-js/05-data-types/04-array/2-create-array/solution.md b/1-js/05-data-types/04-array/2-create-array/solution.md index eec9055e7c..f032b55f0c 100644 --- a/1-js/05-data-types/04-array/2-create-array/solution.md +++ b/1-js/05-data-types/04-array/2-create-array/solution.md @@ -5,6 +5,6 @@ let styles = ["Jazz", "Blues"]; styles.push("Rock-n-Roll"); styles[Math.floor((styles.length - 1) / 2)] = "Classics"; alert( styles.shift() ); -styles.unshift("Rap", "Reggie"); +styles.unshift("Rap", "Reggae"); ``` 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 3e93007939..d4551c79cc 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 @@ -8,7 +8,7 @@ Let's try 5 array operations. 1. Create an array `styles` with items "Jazz" and "Blues". 2. Append "Rock-n-Roll" to the end. -3. Replace the value in the middle by "Classics". Your code for finding the middle value should work for any arrays with odd length. +3. Replace the value in the middle with "Classics". Your code for finding the middle value should work for any arrays with odd length. 4. Strip off the first value of the array and show it. 5. Prepend `Rap` and `Reggae` to the array. @@ -16,7 +16,7 @@ The array in the process: ```js no-beautify Jazz, Blues -Jazz, Bues, Rock-n-Roll +Jazz, Blues, Rock-n-Roll Jazz, Classics, Rock-n-Roll Classics, Rock-n-Roll Rap, Reggae, Classics, Rock-n-Roll 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 e994ae078b..3cb0317cf8 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(){...} ``` The array has 3 values: initially it had two, plus the function. 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 340c5feefc..f1e13499c3 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/6-calculator-extendable/_js.view/solution.js b/1-js/05-data-types/04-array/6-calculator-extendable/_js.view/solution.js deleted file mode 100644 index 50c40e8042..0000000000 --- a/1-js/05-data-types/04-array/6-calculator-extendable/_js.view/solution.js +++ /dev/null @@ -1,25 +0,0 @@ -function Calculator() { - - let methods = { - "-": (a, b) => a - b, - "+": (a, b) => a + b - }; - - this.calculate = function(str) { - - let split = str.split(' '), - a = +split[0], - op = split[1], - b = +split[2] - - if (!methods[op] || isNaN(a) || isNaN(b)) { - return NaN; - } - - return methods[op](a, b); - } - - this.addMethod = function(name, func) { - methods[name] = func; - }; -} diff --git a/1-js/05-data-types/04-array/6-calculator-extendable/solution.md b/1-js/05-data-types/04-array/6-calculator-extendable/solution.md deleted file mode 100644 index 41178663dd..0000000000 --- a/1-js/05-data-types/04-array/6-calculator-extendable/solution.md +++ /dev/null @@ -1,3 +0,0 @@ - -- Please note how methods are stored. They are simply added to the internal object. -- All tests and numeric conversions are done in the `calculate` method. In future it may be extended to support more complex expressions. diff --git a/1-js/05-data-types/04-array/6-calculator-extendable/task.md b/1-js/05-data-types/04-array/6-calculator-extendable/task.md deleted file mode 100644 index cc5453cebf..0000000000 --- a/1-js/05-data-types/04-array/6-calculator-extendable/task.md +++ /dev/null @@ -1,36 +0,0 @@ -importance: 5 - ---- - -# Create an extendable calculator - -Create a constructor function `Calculator` that creates "extendable" calculator objects. - -The task consists of two parts. - -1. First, implement the method `calculate(str)` that takes a string like `"1 + 2"` in the format "NUMBER operator NUMBER" (space-delimited) and returns the result. Should understand plus `+` and minus `-`. - - Usage example: - - ```js - let calc = new Calculator; - - alert( calc.calculate("3 + 7") ); // 10 - ``` -2. Then add the method `addMethod(name, func)` that teaches the calculator a new operation. It takes the operator `name` and the two-argument function `func(a,b)` that implements it. - - For instance, let's add the multiplication `*`, division `/` and power `**`: - - ```js - let powerCalc = new Calculator; - powerCalc.addMethod("*", (a, b) => a * b); - powerCalc.addMethod("/", (a, b) => a / b); - powerCalc.addMethod("**", (a, b) => a ** b); - - let result = powerCalc.calculate("2 ** 3"); - alert( result ); // 8 - ``` - -- No brackets or complex expressions in this task. -- The numbers and the operator are delimited with exactly one space. -- There may be error handling if you'd like to add it. diff --git a/1-js/05-data-types/04-array/array-pop.png b/1-js/05-data-types/04-array/array-pop.png deleted file mode 100644 index 023642fa7c..0000000000 Binary files a/1-js/05-data-types/04-array/array-pop.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-pop.svg b/1-js/05-data-types/04-array/array-pop.svg new file mode 100644 index 0000000000..82b112b4a8 --- /dev/null +++ b/1-js/05-data-types/04-array/array-pop.svg @@ -0,0 +1 @@ +0123"Apple""Orange""Pear""Lemon"length = 4clear012"Apple""Orange""Pear"length = 3 \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-pop@2x.png b/1-js/05-data-types/04-array/array-pop@2x.png deleted file mode 100644 index 301fd8f66b..0000000000 Binary files a/1-js/05-data-types/04-array/array-pop@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-shift.png b/1-js/05-data-types/04-array/array-shift.png deleted file mode 100644 index 5f2cef5dd0..0000000000 Binary files a/1-js/05-data-types/04-array/array-shift.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-shift.svg b/1-js/05-data-types/04-array/array-shift.svg new file mode 100644 index 0000000000..9485a3c969 --- /dev/null +++ b/1-js/05-data-types/04-array/array-shift.svg @@ -0,0 +1 @@ +123"Orange""Pear""Lemon"length = 423"Orange""Pear""Lemon"length = 3clearmove elements to the left0"Apple"012"Orange""Pear""Lemon"11 \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-shift@2x.png b/1-js/05-data-types/04-array/array-shift@2x.png deleted file mode 100644 index 1bd68b3154..0000000000 Binary files a/1-js/05-data-types/04-array/array-shift@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-speed.png b/1-js/05-data-types/04-array/array-speed.png deleted file mode 100644 index 3737e8248f..0000000000 Binary files a/1-js/05-data-types/04-array/array-speed.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-speed.svg b/1-js/05-data-types/04-array/array-speed.svg new file mode 100644 index 0000000000..41f7d998b4 --- /dev/null +++ b/1-js/05-data-types/04-array/array-speed.svg @@ -0,0 +1 @@ +0123popunshiftpushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-speed@2x.png b/1-js/05-data-types/04-array/array-speed@2x.png deleted file mode 100644 index e45624b502..0000000000 Binary files a/1-js/05-data-types/04-array/array-speed@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/article.md b/1-js/05-data-types/04-array/article.md index ca06c65c22..e71e86a5b9 100644 --- a/1-js/05-data-types/04-array/article.md +++ b/1-js/05-data-types/04-array/article.md @@ -1,12 +1,12 @@ -# Arrays +# Arrays -Objects allow to store keyed collections of values. That's fine. +Objects allow you to store keyed collections of values. That's fine. -But quite often we find that we need an *ordered collection*, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc. +But quite often we find that we need an *ordered collection*, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc. It is not convenient to use an object here, because it provides no methods to manage the order of elements. We can’t insert a new property “between” the existing ones. Objects are just not meant for such use. -There exists a special data structure named `Array`, to store ordered collections. +There exists a special data structure named `Array`, to store ordered collections. ## Declaration @@ -81,10 +81,10 @@ arr[3](); // hello ````smart header="Trailing comma" An array, just like an object, may end with a comma: -```js +```js let fruits = [ - "Apple", - "Orange", + "Apple", + "Orange", "Plum"*!*,*/!* ]; ``` @@ -92,21 +92,53 @@ let fruits = [ The "trailing comma" style makes it easier to insert/remove items, because all lines become alike. ```` +## Get last elements with "at" + +[recent browser="new"] + +Let's say we want the last element of the array. + +Some programming languages allow the use of negative indexes for the same purpose, like `fruits[-1]`. + +Although, in JavaScript it won't work. The result will be `undefined`, because the index in square brackets is treated literally. + +We can explicitly calculate the last element index and then access it: `fruits[fruits.length - 1]`. + +```js run +let fruits = ["Apple", "Orange", "Plum"]; + +alert( fruits[fruits.length-1] ); // Plum +``` + +A bit cumbersome, isn't it? We need to write the variable name twice. + +Luckily, there's a shorter syntax: `fruits.at(-1)`: + +```js run +let fruits = ["Apple", "Orange", "Plum"]; + +// same as fruits[fruits.length-1] +alert( fruits.at(-1) ); // Plum +``` + +In other words, `arr.at(i)`: +- is exactly the same as `arr[i]`, if `i >= 0`. +- for negative values of `i`, it steps back from the end of the array. ## Methods pop/push, shift/unshift -A [queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) is one of most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations: +A [queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) is one of the most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations: - `push` appends an element to the end. - `shift` get an element from the beginning, advancing the queue, so that the 2nd element becomes the 1st. -![](queue.png) +![](queue.svg) Arrays support both operations. -In practice we meet it very often. For example, a queue of messages that need to be shown on-screen. +In practice we need it very often. For example, a queue of messages that need to be shown on-screen. -There's another use case for arrays -- the data structure named [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)). +There's another use case for arrays -- the data structure named [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)). It supports two operations: @@ -117,13 +149,13 @@ So new elements are added or taken always from the "end". A stack is usually illustrated as a pack of cards: new cards are added to the top or taken from the top: -![](stack.png) +![](stack.svg) For stacks, the latest pushed item is received first, that's also called LIFO (Last-In-First-Out) principle. For queues, we have FIFO (First-In-First-Out). -Arrays in JavaScript can work both as a queue and as a stack. They allow to add/remove elements both to/from the beginning or the end. +Arrays in JavaScript can work both as a queue and as a stack. They allow you to add/remove elements, both to/from the beginning or the end. -In computer science the data structure that allows it is called [deque](https://en.wikipedia.org/wiki/Double-ended_queue). +In computer science, the data structure that allows this, is called [deque](https://en.wikipedia.org/wiki/Double-ended_queue). **Methods that work with the end of the array:** @@ -138,6 +170,8 @@ In computer science the data structure that allows it is called [deque](https:// alert( fruits ); // Apple, Orange ``` + Both `fruits.pop()` and `fruits.at(-1)` return the last element of the array, but `fruits.pop()` also modifies the array by removing it. + `push` : Append the element to the end of the array: @@ -156,7 +190,7 @@ In computer science the data structure that allows it is called [deque](https:// `shift` : Extracts the first element of the array and returns it: - ```js + ```js run let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // remove Apple and alert it @@ -167,7 +201,7 @@ In computer science the data structure that allows it is called [deque](https:// `unshift` : Add the element to the beginning of the array: - ```js + ```js run let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); @@ -189,11 +223,11 @@ alert( fruits ); ## Internals -An array is a special kind of object. The square brackets used to access a property `arr[0]` actually come from the object syntax. Numbers are used as keys. +An array is a special kind of object. The square brackets used to access a property `arr[0]` actually come from the object syntax. That's essentially the same as `obj[key]`, where `arr` is the object, while numbers are used as keys. They extend objects providing special methods to work with ordered collections of data and also the `length` property. But at the core it's still an object. -Remember, there are only 7 basic types in JavaScript. Array is an object and thus behaves like an object. +Remember, there are only eight basic data types in JavaScript (see the [Data types](info:types) chapter for more info). Array is an object and thus behaves like an object. For instance, it is copied by reference: @@ -203,13 +237,13 @@ let fruits = ["Banana"] let arr = fruits; // copy by reference (two variables reference the same array) alert( arr === fruits ); // true - + arr.push("Pear"); // modify the array by reference alert( fruits ); // Banana, Pear - 2 items now ``` -...But what makes arrays really special is their internal representation. The engine tries to store its elements in the contiguous memory area, one after another, just as depicted on the illustrations in this chapter, and there are other optimizations as well, to make arrays work really fast. +...But what makes arrays really special is their internal representation. The engine tries to store its elements in the contiguous memory area, one after another, just as depicted on the illustrations in this chapter, and there are other optimizations as well, to make arrays work really fast. But they all break if we quit working with an array as with an "ordered collection" and start working with it as if it were a regular object. @@ -229,7 +263,7 @@ But the engine will see that we're working with the array as with a regular obje The ways to misuse an array: -- Add a non-numeric property like `arr.test = 5`. +- Add a non-numeric property like `arr.test = 5`. - Make holes, like: add `arr[0]` and then `arr[1000]` (and nothing between them). - Fill the array in the reverse order, like `arr[1000]`, `arr[999]` and so on. @@ -239,7 +273,7 @@ Please think of arrays as special structures to work with the *ordered data*. Th Methods `push/pop` run fast, while `shift/unshift` are slow. -![](array-speed.png) +![](array-speed.svg) Why is it faster to work with the end of an array than with its beginning? Let's see what happens during the execution: @@ -247,7 +281,7 @@ Why is it faster to work with the end of an array than with its beginning? Let's fruits.shift(); // take 1 element from the start ``` -It's not enough to take and remove the element with the number `0`. Other elements need to be renumbered as well. +It's not enough to take and remove the element with the index `0`. Other elements need to be renumbered as well. The `shift` operation must do 3 things: @@ -255,7 +289,7 @@ The `shift` operation must do 3 things: 2. Move all elements to the left, renumber them from the index `1` to `0`, from `2` to `1` and so on. 3. Update the `length` property. -![](array-shift.png) +![](array-shift.svg) **The more elements in the array, the more time to move them, more in-memory operations.** @@ -269,7 +303,7 @@ The actions for the `pop` operation: fruits.pop(); // take 1 element from the end ``` -![](array-pop.png) +![](array-pop.svg) **The `pop` method does not need to move anything, because other elements keep their indexes. That's why it's blazingly fast.** @@ -296,7 +330,7 @@ let fruits = ["Apple", "Orange", "Plum"]; // iterates over array elements for (let fruit of fruits) { - alert( fruit ); + alert( fruit ); } ``` @@ -320,7 +354,7 @@ But that's actually a bad idea. There are potential problems with it: There are so-called "array-like" objects in the browser and in other environments, that *look like arrays*. That is, they have `length` and indexes properties, but they may also have other non-numeric properties and methods, which we usually don't need. The `for..in` loop will list them though. So if we need to work with array-like objects, then these "extra" properties can become a problem. -2. The `for..in` loop is optimized for generic objects, not arrays, and thus is 10-100 times slower. Of course, it's still very fast. The speedup may matter only in bottlenecks or just irrelevant. But still we should be aware of the difference. +2. The `for..in` loop is optimized for generic objects, not arrays, and thus is 10-100 times slower. Of course, it's still very fast. The speedup may only matter in bottlenecks. But still we should be aware of the difference. Generally, we shouldn't use `for..in` for arrays. @@ -338,7 +372,7 @@ fruits[123] = "Apple"; alert( fruits.length ); // 124 ``` -Note that we usually don't use arrays like that. +Note that we usually don't use arrays like that. Another interesting thing about the `length` property is that it's writable. @@ -365,11 +399,11 @@ There is one more syntax to create an array: let arr = *!*new Array*/!*("Apple", "Pear", "etc"); ``` -It's rarely used, because square brackets `[]` are shorter. Also there's a tricky feature with it. +It's rarely used, because square brackets `[]` are shorter. Also, there's a tricky feature with it. If `new Array` is called with a single argument which is a number, then it creates an array *without items, but with the given length*. -Let's see how one can shoot themself in the foot: +Let's see how one can shoot themselves in the foot: ```js run let arr = new Array(2); // will it create an array of [2] ? @@ -379,13 +413,11 @@ alert( arr[0] ); // undefined! no elements. alert( arr.length ); // length 2 ``` -In the code above, `new Array(number)` has all elements `undefined`. - -To evade such surprises, we usually use square brackets, unless we really know what we're doing. +To avoid such surprises, we usually use square brackets, unless we really know what we're doing. ## Multidimensional arrays -Arrays can have items that are also arrays. We can use it for multidimensional arrays, to store matrices: +Arrays can have items that are also arrays. We can use it for multidimensional arrays, for example to store matrices: ```js run let matrix = [ @@ -394,7 +426,7 @@ let matrix = [ [7, 8, 9] ]; -alert( matrix[1][1] ); // the central element +alert( matrix[0][1] ); // 2, the second value of the first inner array ``` ## toString @@ -429,36 +461,91 @@ alert( "1" + 1 ); // "11" alert( "1,2" + 1 ); // "1,21" ``` +## Don't compare arrays with == + +Arrays in JavaScript, unlike some other programming languages, shouldn't be compared with operator `==`. + +This operator has no special treatment for arrays, it works with them as with any objects. + +Let's recall the rules: + +- Two objects are equal `==` only if they're references to the same object. +- If one of the arguments of `==` is an object, and the other one is a primitive, then the object gets converted to primitive, as explained in the chapter . +- ...With an exception of `null` and `undefined` that equal `==` each other and nothing else. + +The strict comparison `===` is even simpler, as it doesn't convert types. + +So, if we compare arrays with `==`, they are never the same, unless we compare two variables that reference exactly the same array. + +For example: +```js run +alert( [] == [] ); // false +alert( [0] == [0] ); // false +``` + +These arrays are technically different objects. So they aren't equal. The `==` operator doesn't do item-by-item comparison. + +Comparison with primitives may give seemingly strange results as well: + +```js run +alert( 0 == [] ); // true + +alert('0' == [] ); // false +``` + +Here, in both cases, we compare a primitive with an array object. So the array `[]` gets converted to primitive for the purpose of comparison and becomes an empty string `''`. + +Then the comparison process goes on with the primitives, as described in the chapter : + +```js run +// after [] was converted to '' +alert( 0 == '' ); // true, as '' becomes converted to number 0 + +alert('0' == '' ); // false, no type conversion, different strings +``` + +So, how to compare arrays? + +That's simple: don't use the `==` operator. Instead, compare them item-by-item in a loop or using iteration methods explained in the next chapter. + ## Summary -Array is a special kind of objects, suited to store and manage ordered data items. +Array is a special kind of object, suited to storing and managing ordered data items. -- The declaration: +The declaration: - ```js - // square brackets (usual) - let arr = [item1, item2...]; +```js +// square brackets (usual) +let arr = [item1, item2...]; - // new Array (exceptionally rare) - let arr = new Array(item1, item2...); - ``` +// new Array (exceptionally rare) +let arr = new Array(item1, item2...); +``` - The call to `new Array(number)` creates an array with the given length, but without elements. +The call to `new Array(number)` creates an array with the given length, but without elements. -- The `length` property is the array length or, to be precise, its last numeric index plus one. It is auto-adjusted by array methods. +- The `length` property is the array length or, to be precise, its last numeric index plus one. It is auto-adjusted by array methods. - If we shorten `length` manually, the array is truncated. +Getting the elements: + +- we can get element by its index, like `arr[0]` +- also we can use `at(i)` method that allows negative indexes. For negative values of `i`, it steps back from the end of the array. If `i >= 0`, it works same as `arr[i]`. + We can use an array as a deque with the following operations: - `push(...items)` adds `items` to the end. - `pop()` removes the element from the end and returns it. - `shift()` removes the element from the beginning and returns it. -- `unshift(...items)` adds items to the beginning. +- `unshift(...items)` adds `items` to the beginning. To loop over the elements of the array: - `for (let i=0; i. +To compare arrays, don't use the `==` operator (as well as `>`, `<` and others), as they have no special treatment for arrays. They handle them as any objects, and it's not what we usually want. + +Instead you can use `for..of` loop to compare arrays item-by-item. +We will continue with arrays and study more methods to add, remove, extract elements and sort arrays in the next chapter . diff --git a/1-js/05-data-types/04-array/queue.png b/1-js/05-data-types/04-array/queue.png deleted file mode 100644 index 5e1fb640ce..0000000000 Binary files a/1-js/05-data-types/04-array/queue.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/queue.svg b/1-js/05-data-types/04-array/queue.svg new file mode 100644 index 0000000000..e89a3dcd2d --- /dev/null +++ b/1-js/05-data-types/04-array/queue.svg @@ -0,0 +1 @@ +pushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/queue@2x.png b/1-js/05-data-types/04-array/queue@2x.png deleted file mode 100644 index 6acfc83d12..0000000000 Binary files a/1-js/05-data-types/04-array/queue@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/stack.png b/1-js/05-data-types/04-array/stack.png deleted file mode 100644 index d1c9cb9afd..0000000000 Binary files a/1-js/05-data-types/04-array/stack.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/stack.svg b/1-js/05-data-types/04-array/stack.svg new file mode 100644 index 0000000000..ae0c474d0f --- /dev/null +++ b/1-js/05-data-types/04-array/stack.svg @@ -0,0 +1 @@ +pushpop \ No newline at end of file diff --git a/1-js/05-data-types/04-array/stack@2x.png b/1-js/05-data-types/04-array/stack@2x.png deleted file mode 100644 index b3835fa442..0000000000 Binary files a/1-js/05-data-types/04-array/stack@2x.png and /dev/null differ diff --git a/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js b/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js index 024d6d6c2b..490f570ada 100644 --- a/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js +++ b/1-js/05-data-types/05-array-methods/1-camelcase/_js.view/solution.js @@ -1,8 +1,10 @@ function camelize(str) { return str - .split('-') // my-long-word -> ['my', 'long', 'word'] - .map( + .split('-') // splits 'my-long-word' into array ['my', 'long', 'word'] + .map( + // capitalizes first letters of all array items except the first one + // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word'] (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1) - ) // ['my', 'long', 'word'] -> ['my', 'Long', 'Word'] - .join(''); // ['my', 'Long', 'Word'] -> myLongWord + ) + .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord' } diff --git a/1-js/05-data-types/05-array-methods/10-average-age/task.md b/1-js/05-data-types/05-array-methods/10-average-age/task.md index a991c156b9..bf5f85df38 100644 --- a/1-js/05-data-types/05-array-methods/10-average-age/task.md +++ b/1-js/05-data-types/05-array-methods/10-average-age/task.md @@ -4,7 +4,7 @@ importance: 4 # Get average age -Write the function `getAverageAge(users)` that gets an array of objects with property `age` and gets the average. +Write the function `getAverageAge(users)` that gets an array of objects with property `age` and returns the average age. The formula for the average is `(age1 + age2 + ... + ageN) / N`. @@ -19,4 +19,3 @@ let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28 ``` - diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/solution.md b/1-js/05-data-types/05-array-methods/11-array-unique/solution.md index 8f7fd9af44..b9d627a0a3 100644 --- a/1-js/05-data-types/05-array-methods/11-array-unique/solution.md +++ b/1-js/05-data-types/05-array-methods/11-array-unique/solution.md @@ -2,7 +2,7 @@ Let's walk the array items: - For each item we'll check if the resulting array already has that item. - If it is so, then ignore, otherwise add to results. -```js run +```js run demo function unique(arr) { let result = []; @@ -36,4 +36,4 @@ So if `arr.length` is `10000` we'll have something like `10000*10000` = 100 mill So the solution is only good for small arrays. -Further in the chapter we'll see how to optimize it. +Further in the chapter we'll see how to optimize it. diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js new file mode 100644 index 0000000000..8dea23a06a --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js @@ -0,0 +1,6 @@ +function groupById(array) { + return array.reduce((obj, value) => { + obj[value.id] = value; + return obj; + }, {}) +} diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js new file mode 100644 index 0000000000..e48ba138db --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js @@ -0,0 +1,21 @@ +describe("groupById", function() { + + it("creates an object grouped by id", function() { + let users = [ + {id: 'john', name: "John Smith", age: 20}, + {id: 'ann', name: "Ann Smith", age: 24}, + {id: 'pete', name: "Pete Peterson", age: 31}, + ]; + + assert.deepEqual(groupById(users), { + john: {id: 'john', name: "John Smith", age: 20}, + ann: {id: 'ann', name: "Ann Smith", age: 24}, + pete: {id: 'pete', name: "Pete Peterson", age: 31}, + }); + }); + + it("works with an empty array", function() { + users = []; + assert.deepEqual(groupById(users), {}); + }); +}); diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/solution.md b/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/solution.md rename to 1-js/05-data-types/05-array-methods/12-reduce-object/solution.md 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..7f0082357a --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/task.md @@ -0,0 +1,37 @@ +importance: 4 + +--- + +# Create keyed object from array + +Let's say we received an array of users in the form `{id:..., name:..., age:... }`. + +Create a function `groupById(arr)` that creates an object from it, with `id` as the key, and array items as values. + +For example: + +```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); + +/* +// after the call we should have: + +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}, +} +*/ +``` + +Such function is really handy when working with server data. + +In this task we assume that `id` is unique. There may be no two array items with the same `id`. + +Please use array `.reduce` method in the solution. diff --git a/1-js/05-data-types/05-array-methods/2-filter-range/solution.md b/1-js/05-data-types/05-array-methods/2-filter-range/solution.md index e69de29bb2..73993a07a0 100644 --- a/1-js/05-data-types/05-array-methods/2-filter-range/solution.md +++ b/1-js/05-data-types/05-array-methods/2-filter-range/solution.md @@ -0,0 +1,14 @@ +```js run demo +function filterRange(arr, a, b) { + // added brackets around the expression for better readability + return arr.filter(item => (a <= item && item <= b)); +} + +let arr = [5, 3, 8, 1]; + +let filtered = filterRange(arr, 1, 4); + +alert( filtered ); // 3,1 (matching values) + +alert( arr ); // 5,3,8,1 (not modified) +``` 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 18b2c1d9b5..46e47c93dc 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 # Filter range -Write a function `filterRange(arr, a, b)` that gets an array `arr`, looks for elements between `a` and `b` in it and returns an array of them. +Write a function `filterRange(arr, a, b)` that gets an array `arr`, looks for elements with values higher or equal to `a` and lower or equal to `b` and return a result as an array. The function should not modify the array. It should return the new array. diff --git a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js index 61cda126b6..488db3755b 100644 --- a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/solution.js @@ -1,5 +1,4 @@ - function filterRangeInPlace(arr, a, b) { for (let i = 0; i < arr.length; i++) { @@ -12,4 +11,4 @@ function filterRangeInPlace(arr, a, b) { } } -} \ No newline at end of file +} 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/3-filter-range-in-place/solution.md b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/solution.md index e69de29bb2..36e3130ff0 100644 --- a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/solution.md +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/solution.md @@ -0,0 +1,21 @@ +```js run demo +function filterRangeInPlace(arr, a, b) { + + for (let i = 0; i < arr.length; i++) { + let val = arr[i]; + + // remove if outside of the interval + if (val < a || val > b) { + arr.splice(i, 1); + i--; + } + } + +} + +let arr = [5, 3, 8, 1]; + +filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4 + +alert( arr ); // [3, 1] +``` diff --git a/1-js/05-data-types/05-array-methods/4-sort-back/task.md b/1-js/05-data-types/05-array-methods/4-sort-back/task.md index 05a08aad0b..0e3eeab76e 100644 --- a/1-js/05-data-types/05-array-methods/4-sort-back/task.md +++ b/1-js/05-data-types/05-array-methods/4-sort-back/task.md @@ -2,12 +2,12 @@ importance: 4 --- -# Sort in the reverse order +# Sort in decreasing order ```js let arr = [5, 2, 1, -10, 8]; -// ... your code to sort it in the reverse order +// ... your code to sort it in decreasing order alert( arr ); // 8, 5, 2, 1, -10 ``` diff --git a/1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/solution.js b/1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/solution.js new file mode 100644 index 0000000000..f62452a5f7 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/solution.js @@ -0,0 +1,25 @@ +function Calculator() { + + this.methods = { + "-": (a, b) => a - b, + "+": (a, b) => a + b + }; + + this.calculate = function(str) { + + let split = str.split(' '), + a = +split[0], + op = split[1], + b = +split[2]; + + if (!this.methods[op] || isNaN(a) || isNaN(b)) { + return NaN; + } + + return this.methods[op](a, b); + }; + + this.addMethod = function(name, func) { + this.methods[name] = func; + }; +} diff --git a/1-js/05-data-types/04-array/6-calculator-extendable/_js.view/test.js b/1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/04-array/6-calculator-extendable/_js.view/test.js rename to 1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/test.js 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 new file mode 100644 index 0000000000..ebe0714cfb --- /dev/null +++ b/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md @@ -0,0 +1,3 @@ + +- Please note how methods are stored. They are simply added to `this.methods` property. +- All tests and numeric conversions are done in the `calculate` method. In future it may be extended to support more complex expressions. diff --git a/1-js/05-data-types/05-array-methods/6-calculator-extendable/task.md b/1-js/05-data-types/05-array-methods/6-calculator-extendable/task.md new file mode 100644 index 0000000000..e0d302f4ce --- /dev/null +++ b/1-js/05-data-types/05-array-methods/6-calculator-extendable/task.md @@ -0,0 +1,36 @@ +importance: 5 + +--- + +# Create an extendable calculator + +Create a constructor function `Calculator` that creates "extendable" calculator objects. + +The task consists of two parts. + +1. First, implement the method `calculate(str)` that takes a string like `"1 + 2"` in the format "NUMBER operator NUMBER" (space-delimited) and returns the result. Should understand plus `+` and minus `-`. + + Usage example: + + ```js + let calc = new Calculator; + + alert( calc.calculate("3 + 7") ); // 10 + ``` +2. Then add the method `addMethod(name, func)` that teaches the calculator a new operation. It takes the operator `name` and the two-argument function `func(a,b)` that implements it. + + For instance, let's add the multiplication `*`, division `/` and power `**`: + + ```js + let powerCalc = new Calculator; + powerCalc.addMethod("*", (a, b) => a * b); + powerCalc.addMethod("/", (a, b) => a / b); + powerCalc.addMethod("**", (a, b) => a ** b); + + let result = powerCalc.calculate("2 ** 3"); + alert( result ); // 8 + ``` + +- No parentheses or complex expressions in this task. +- The numbers and the operator are delimited with exactly one space. +- There may be error handling if you'd like to add it. diff --git a/1-js/05-data-types/05-array-methods/7-map-objects/solution.md b/1-js/05-data-types/05-array-methods/7-map-objects/solution.md index 5d8bf4a13d..2d8d4fb0ee 100644 --- a/1-js/05-data-types/05-array-methods/7-map-objects/solution.md +++ b/1-js/05-data-types/05-array-methods/7-map-objects/solution.md @@ -25,7 +25,7 @@ alert( usersMapped[0].id ); // 1 alert( usersMapped[0].fullName ); // John Smith ``` -Please note that in for the arrow functions we need to use additional brackets. +Please note that in the arrow functions we need to use additional brackets. We can't write like this: ```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 8d56db9d66..cfaf9761a3 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,17 +1,18 @@ ```js run no-beautify -function sortByName(arr) { - arr.sort((a, b) => a.name > b.name); +function sortByAge(arr) { + arr.sort((a, b) => a.age - b.age); } let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; -let arr = [ john, pete, mary ]; +let arr = [ pete, john, mary ]; -sortByName(arr); +sortByAge(arr); // now sorted is: [john, mary, pete] +alert(arr[0].name); // John alert(arr[1].name); // Mary +alert(arr[2].name); // Pete ``` - diff --git a/1-js/05-data-types/05-array-methods/8-sort-objects/task.md b/1-js/05-data-types/05-array-methods/8-sort-objects/task.md index 8a3f5a97a3..9a215c9f4d 100644 --- a/1-js/05-data-types/05-array-methods/8-sort-objects/task.md +++ b/1-js/05-data-types/05-array-methods/8-sort-objects/task.md @@ -2,9 +2,9 @@ importance: 5 --- -# Sort objects +# Sort users by age -Write the function `sortByName(users)` that gets an array of objects with property `name` and sorts it. +Write the function `sortByAge(users)` that gets an array of objects with the `age` property and sorts them by `age`. For instance: @@ -13,11 +13,12 @@ let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; -let arr = [ john, pete, mary ]; +let arr = [ pete, john, mary ]; -sortByName(arr); +sortByAge(arr); // now: [john, mary, pete] +alert(arr[0].name); // John alert(arr[1].name); // Mary +alert(arr[2].name); // Pete ``` - diff --git a/1-js/05-data-types/05-array-methods/9-shuffle/solution.md b/1-js/05-data-types/05-array-methods/9-shuffle/solution.md index a43715db87..6674c444f8 100644 --- a/1-js/05-data-types/05-array-methods/9-shuffle/solution.md +++ b/1-js/05-data-types/05-array-methods/9-shuffle/solution.md @@ -45,7 +45,7 @@ for (let key in count) { } ``` -An example result (for V8, July 2017): +An example result (depends on JS engine): ```js 123: 250706 @@ -68,7 +68,13 @@ There are other good ways to do the task. For instance, there's a great algorith function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i - [array[i], array[j]] = [array[j], array[i]]; // swap elements + + // swap elements array[i] and array[j] + // we use "destructuring assignment" syntax to achieve that + // you'll find more details about that syntax in later chapters + // same can be written as: + // let t = array[i]; array[i] = array[j]; array[j] = t + [array[i], array[j]] = [array[j], array[i]]; } } ``` 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 83487cee5a..8536459582 100644 --- a/1-js/05-data-types/05-array-methods/article.md +++ b/1-js/05-data-types/05-array-methods/article.md @@ -1,6 +1,6 @@ # Array methods -Arrays provide a lot of methods. To make things easier, in this chapter they are split into groups. +Arrays provide a lot of methods. To make things easier, in this chapter, they are split into groups. ## Add/remove items @@ -11,7 +11,7 @@ We already know methods that add and remove items from the beginning or the end: - `arr.shift()` -- extracts an item from the beginning, - `arr.unshift(...items)` -- adds items to the beginning. -Here are few others. +Here are a few others. ### splice @@ -32,19 +32,19 @@ alert( arr.length ); // 3 The element was removed, but the array still has 3 elements, we can see that `arr.length == 3`. -That's natural, because `delete obj.key` removes a value by the `key`. It's all it does. Fine for objects. But for arrays we usually want the rest of elements to shift and occupy the freed place. We expect to have a shorter array now. +That's natural, because `delete obj.key` removes a value by the `key`. It's all it does. Fine for objects. But for arrays we usually want the rest of the elements to shift and occupy the freed place. We expect to have a shorter array now. So, special methods should be used. -The [arr.splice(str)](mdn:js/Array/splice) method is a swiss army knife for arrays. It can do everything: add, remove and insert elements. +The [arr.splice](mdn:js/Array/splice) method is a Swiss army knife for arrays. It can do everything: insert, remove and replace elements. The syntax is: ```js -arr.splice(index[, deleteCount, elem1, ..., elemN]) +arr.splice(start[, deleteCount, elem1, ..., elemN]) ``` -It starts from the position `index`: removes `deleteCount` elements and then inserts `elem1, ..., elemN` at their place. Returns the array of removed elements. +It modifies `arr` starting from the index `start`: removes `deleteCount` elements and then inserts `elem1, ..., elemN` at their place. Returns the array of removed elements. This method is easy to grasp by examples. @@ -62,7 +62,7 @@ alert( arr ); // ["I", "JavaScript"] Easy, right? Starting from the index `1` it removed `1` element. -In the next example we remove 3 elements and replace them with the other two: +In the next example, we remove 3 elements and replace them with the other two: ```js run let arr = [*!*"I", "study", "JavaScript",*/!* "right", "now"]; @@ -84,7 +84,7 @@ let removed = arr.splice(0, 2); alert( removed ); // "I", "study" <-- array of removed elements ``` -The `splice` method is also able to insert the elements without any removals. For that we need to set `deleteCount` to `0`: +The `splice` method is also able to insert the elements without any removals. For that, we need to set `deleteCount` to `0`: ```js run let arr = ["I", "study", "JavaScript"]; @@ -114,34 +114,33 @@ alert( arr ); // 1,2,3,4,5 ### slice -The method [arr.slice](mdn:js/Array/slice) is much simpler than similar-looking `arr.splice`. +The method [arr.slice](mdn:js/Array/slice) is much simpler than the similar-looking `arr.splice`. The syntax is: ```js -arr.slice(start, end) +arr.slice([start], [end]) ``` -It returns a new array where it copies all items start index `"start"` to `"end"` (not including `"end"`). Both `start` and `end` can be negative, in that case position from array end is assumed. +It returns a new array copying to it all items from index `start` to `end` (not including `end`). Both `start` and `end` can be negative, in that case position from array end is assumed. -It works like `str.slice`, but makes subarrays instead of substrings. +It's similar to a string method `str.slice`, but instead of substrings, it makes subarrays. For instance: ```js run -let str = "test"; let arr = ["t", "e", "s", "t"]; -alert( str.slice(1, 3) ); // es -alert( arr.slice(1, 3) ); // e,s +alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3) -alert( str.slice(-2) ); // st -alert( arr.slice(-2) ); // s,t +alert( arr.slice(-2) ); // s,t (copy from -2 till the end) ``` +We can also call it without arguments: `arr.slice()` creates a copy of `arr`. That's often used to obtain a copy for further transformations that should not affect the original array. + ### concat -The method [arr.concat](mdn:js/Array/concat) joins the array with other arrays and/or items. +The method [arr.concat](mdn:js/Array/concat) creates a new array that includes values from other arrays and additional items. The syntax is: @@ -153,24 +152,24 @@ It accepts any number of arguments -- either arrays or values. The result is a new array containing items from `arr`, then `arg1`, `arg2` etc. -If an argument is an array or has `Symbol.isConcatSpreadable` property, then all its elements are copied. Otherwise, the argument itself is copied. +If an argument `argN` is an array, then all its elements are copied. Otherwise, the argument itself is copied. For instance: ```js run let arr = [1, 2]; -// merge arr with [3,4] -alert( arr.concat([3, 4])); // 1,2,3,4 +// create an array from: arr and [3,4] +alert( arr.concat([3, 4]) ); // 1,2,3,4 -// merge arr with [3,4] and [5,6] -alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6 +// create an array from: arr and [3,4] and [5,6] +alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 -// merge arr with [3,4], then add values 5 and 6 -alert( arr.concat([3, 4], 5, 6)); // 1,2,3,4,5,6 +// create an array from: arr and [3,4], then add values 5 and 6 +alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6 ``` -Normally, it only copies elements from arrays ("spreads" them). Other objects, even if they look like arrays, added as a whole: +Normally, it only copies elements from arrays. Other objects, even if they look like arrays, are added as a whole: ```js run let arr = [1, 2]; @@ -181,10 +180,9 @@ let arrayLike = { }; alert( arr.concat(arrayLike) ); // 1,2,[object Object] -//[1, 2, arrayLike] ``` -...But if an array-like object has `Symbol.isConcatSpreadable` property, then its elements are added instead: +...But if an array-like object has a special `Symbol.isConcatSpreadable` property, then it's treated as an array by `concat`: its elements are added instead: ```js run let arr = [1, 2]; @@ -201,18 +199,48 @@ let arrayLike = { alert( arr.concat(arrayLike) ); // 1,2,something,else ``` +## Iterate: forEach + +The [arr.forEach](mdn:js/Array/forEach) method allows to run a function for every element of the array. + +The syntax: +```js +arr.forEach(function(item, index, array) { + // ... do something with an item +}); +``` + +For instance, this shows each element of the array: + +```js run +// for each element call alert +["Bilbo", "Gandalf", "Nazgul"].forEach(alert); +``` + +And this code is more elaborate about their positions in the target array: + +```js run +["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { + alert(`${item} is at index ${index} in ${array}`); +}); +``` + +The result of the function (if it returns any) is thrown away and ignored. + + ## Searching in array -These are methods to search for something in an array. +Now let's cover methods that search in an array. ### indexOf/lastIndexOf and includes -The methods [arr.indexOf](mdn:js/Array/indexOf), [arr.lastIndexOf](mdn:js/Array/lastIndexOf) and [arr.includes](mdn:js/Array/includes) have the same syntax and do essentially the same as their string counterparts, but operate on items instead of characters: +The methods [arr.indexOf](mdn:js/Array/indexOf) and [arr.includes](mdn:js/Array/includes) have the similar syntax and do essentially the same as their string counterparts, but operate on items instead of characters: -- `arr.indexOf(item, from)` looks for `item` starting from index `from`, and returns the index where it was found, otherwise `-1`. -- `arr.lastIndexOf(item, from)` -- same, but looks from right to left. +- `arr.indexOf(item, from)` -- looks for `item` starting from index `from`, and returns the index where it was found, otherwise `-1`. - `arr.includes(item, from)` -- looks for `item` starting from index `from`, returns `true` if found. +Usually, these methods are used with only one argument: the `item` to search. By default, the search is from the beginning. + For instance: ```js run @@ -225,38 +253,51 @@ alert( arr.indexOf(null) ); // -1 alert( arr.includes(1) ); // true ``` -Note that the methods use `===` comparison. So, if we look for `false`, it finds exactly `false` and not the zero. +Please note that `indexOf` uses the strict equality `===` for comparison. So, if we look for `false`, it finds exactly `false` and not the zero. + +If we want to check if `item` exists in the array and don't need the index, then `arr.includes` is preferred. + +The method [arr.lastIndexOf](mdn:js/Array/lastIndexOf) is the same as `indexOf`, but looks for from right to left. -If we want to check for inclusion, and don't want to know the exact index, then `arr.includes` is preferred. +```js run +let fruits = ['Apple', 'Orange', 'Apple'] + +alert( fruits.indexOf('Apple') ); // 0 (first Apple) +alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple) +``` -Also, a very minor difference of `includes` is that it correctly handles `NaN`, unlike `indexOf/lastIndexOf`: +````smart header="The `includes` method handles `NaN` correctly" +A minor, but noteworthy feature of `includes` is that it correctly handles `NaN`, unlike `indexOf`: ```js run const arr = [NaN]; -alert( arr.indexOf(NaN) ); // -1 (should be 0, but === equality doesn't work for NaN) +alert( arr.indexOf(NaN) ); // -1 (wrong, should be 0) alert( arr.includes(NaN) );// true (correct) ``` +That's because `includes` was added to JavaScript much later and uses the more up-to-date comparison algorithm internally. +```` -### find and findIndex +### find and findIndex/findLastIndex -Imagine we have an array of objects. How do we find an object with the specific condition? +Imagine we have an array of objects. How do we find an object with a specific condition? -Here the [arr.find](mdn:js/Array/find) method comes in handy. +Here the [arr.find(fn)](mdn:js/Array/find) method comes in handy. The syntax is: ```js let result = arr.find(function(item, index, array) { - // should return true if the item is what we are looking for + // if true is returned, item is returned and iteration is stopped + // for falsy scenario returns undefined }); ``` -The function is called repetitively for each element of the array: +The function is called for elements of the array, one after another: - `item` is the element. - `index` is its index. - `array` is the array itself. -If it returns `true`, the search is stopped, the `item` is returned. If nothing found, `undefined` is returned. +If it returns `true`, the search is stopped, the `item` is returned. If nothing is found, `undefined` is returned. For example, we have an array of users, each with the fields `id` and `name`. Let's find the one with `id == 1`: @@ -272,11 +313,30 @@ let user = users.find(item => item.id == 1); alert(user.name); // John ``` -In real life arrays of objects is a common thing, so the `find` method is very useful. +In real life, arrays of objects are a common thing, so the `find` method is very useful. -Note that in the example we provide to `find` a single-argument function `item => item.id == 1`. Other parameters of `find` are rarely used. +Note that in the example we provide to `find` the function `item => item.id == 1` with one argument. That's typical, other arguments of this function are rarely used. -The [arr.findIndex](mdn:js/Array/findIndex) method is essentially the same, but it returns the index where the element was found instead of the element itself. +The [arr.findIndex](mdn:js/Array/findIndex) method has the same syntax but returns the index where the element was found instead of the element itself. The value of `-1` is returned if nothing is found. + +The [arr.findLastIndex](mdn:js/Array/findLastIndex) method is like `findIndex`, but searches from right to left, similar to `lastIndexOf`. + +Here's an example: + +```js run +let users = [ + {id: 1, name: "John"}, + {id: 2, name: "Pete"}, + {id: 3, name: "Mary"}, + {id: 4, name: "John"} +]; + +// Find the index of the first John +alert(users.findIndex(user => user.name == 'John')); // 0 + +// Find the index of the last John +alert(users.findLastIndex(user => user.name == 'John')); // 3 +``` ### filter @@ -284,11 +344,12 @@ The `find` method looks for a single (first) element that makes the function ret If there may be many, we can use [arr.filter(fn)](mdn:js/Array/filter). -The syntax is roughly the same as `find`, but it returns an array of matching elements: +The syntax is similar to `find`, but `filter` returns an array of all matching elements: ```js let results = arr.filter(function(item, index, array) { - // should return true if the item passes the filter + // if true item is pushed to results and the iteration continues + // returns empty array if nothing found }); ``` @@ -309,40 +370,41 @@ alert(someUsers.length); // 2 ## Transform an array -This section is about the methods transforming or reordering the array. - +Let's move on to methods that transform and reorder an array. ### map The [arr.map](mdn:js/Array/map) method is one of the most useful and often used. +It calls the function for each element of the array and returns the array of results. + The syntax is: ```js let result = arr.map(function(item, index, array) { // returns the new value instead of item -}) +}); ``` -It calls the function for each element of the array and returns the array of results. - For instance, here we transform each element into its length: ```js run -let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length) +let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6 ``` ### sort(fn) -The method [arr.sort](mdn:js/Array/sort) sorts the array *in place*. +The call to [arr.sort()](mdn:js/Array/sort) sorts the array *in place*, changing its element order. + +It also returns the sorted array, but the returned value is usually ignored, as `arr` itself is modified. For instance: ```js run let arr = [ 1, 2, 15 ]; -// the method reorders the content of arr (and returns it) +// the method reorders the content of arr arr.sort(); alert( arr ); // *!*1, 15, 2*/!* @@ -354,20 +416,21 @@ The order became `1, 15, 2`. Incorrect. But why? **The items are sorted as strings by default.** -Literally, all elements are converted to strings and then compared. So, the lexicographic ordering is applied and indeed `"2" > "15"`. +Literally, all elements are converted to strings for comparisons. For strings, lexicographic ordering is applied and indeed `"2" > "15"`. + +To use our own sorting order, we need to supply a function as the argument of `arr.sort()`. -To use our own sorting order, we need to supply a function of two arguments as the argument of `arr.sort()`. +The function should compare two arbitrary values and return: -The function should work like this: ```js function compare(a, b) { - if (a > b) return 1; - if (a == b) return 0; - if (a < b) return -1; + if (a > b) return 1; // if the first value is greater than the second + if (a == b) return 0; // if values are equal + if (a < b) return -1; // if the first value is less than the second } ``` -For instance: +For instance, to sort as numbers: ```js run function compareNumeric(a, b) { @@ -387,20 +450,20 @@ alert(arr); // *!*1, 2, 15*/!* Now it works as intended. -Let's step aside and think what's happening. The `arr` can be array of anything, right? It may contain numbers or strings or html elements or whatever. We have a set of *something*. To sort it, we need an *ordering function* that knows how to compare its elements. The default is a string order. +Let's step aside and think about what's happening. The `arr` can be an array of anything, right? It may contain numbers or strings or objects or whatever. We have a set of *some items*. To sort it, we need an *ordering function* that knows how to compare its elements. The default is a string order. -The `arr.sort(fn)` method has a built-in implementation of sorting algorithm. We don't need to care how it exactly works (an optimized [quicksort](https://en.wikipedia.org/wiki/Quicksort) most of the time). It will walk the array, compare its elements using the provided function and reorder them, all we need is to provide the `fn` which does the comparison. +The `arr.sort(fn)` method implements a generic sorting algorithm. We don't need to care how it internally works (an optimized [quicksort](https://en.wikipedia.org/wiki/Quicksort) or [Timsort](https://en.wikipedia.org/wiki/Timsort) most of the time). It will walk the array, compare its elements using the provided function and reorder them, all we need is to provide the `fn` which does the comparison. -By the way, if we ever want to know which elements are compared -- nothing prevents from alerting them: +By the way, if we ever want to know which elements are compared -- nothing prevents us from alerting them: ```js run [1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); + return a - b; }); ``` -The algorithm may compare an element multiple times in the process, but it tries to make as few comparisons as possible. - +The algorithm may compare an element with multiple others in the process, but it tries to make as few comparisons as possible. ````smart header="A comparison function may return any number" Actually, a comparison function is only required to return a positive number to say "greater" and a negative number to say "less". @@ -417,13 +480,29 @@ alert(arr); // *!*1, 2, 15*/!* ```` ````smart header="Arrow functions for the best" -Remember [arrow functions](info:function-expression#arrow-functions)? We can use them here for neater sorting: +Remember [arrow functions](info:arrow-functions-basics)? We can use them here for neater sorting: ```js arr.sort( (a, b) => a - b ); ``` -This works exactly the same as the other, longer, version above. +This works exactly the same as the longer version above. +```` + +````smart header="Use `localeCompare` for strings" +Remember [strings](info:string#correct-comparisons) comparison algorithm? It compares letters by their codes by default. + +For many alphabets, it's better to use `str.localeCompare` method to correctly sort letters, such as `Ö`. + +For example, let's sort a few countries in German: + +```js run +let countries = ['Österreich', 'Andorra', 'Vietnam']; + +alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong) + +alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!) +``` ```` ### reverse @@ -443,11 +522,11 @@ It also returns the array `arr` after the reversal. ### split and join -Here's the situation from the real life. We are writing a messaging app, and the person enters the comma-delimited list of receivers: `John, Pete, Mary`. But for us an array of names would be much more comfortable than a single string. How to get it? +Here's the situation from real life. We are writing a messaging app, and the person enters the comma-delimited list of receivers: `John, Pete, Mary`. But for us an array of names would be much more comfortable than a single string. How to get it? The [str.split(delim)](mdn:js/String/split) method does exactly that. It splits the string into an array by the given delimiter `delim`. -In the example below, we split by a comma followed by space: +In the example below, we split by a comma followed by a space: ```js run let names = 'Bilbo, Gandalf, Nazgul'; @@ -477,21 +556,21 @@ alert( str.split('') ); // t,e,s,t ``` ```` -The call [arr.join(str)](mdn:js/Array/join) does the reverse to `split`. It creates a string of `arr` items glued by `str` between them. +The call [arr.join(glue)](mdn:js/Array/join) does the reverse to `split`. It creates a string of `arr` items joined by `glue` between them. For instance: ```js run let arr = ['Bilbo', 'Gandalf', 'Nazgul']; -let str = arr.join(';'); +let str = arr.join(';'); // glue the array into a string using ; alert( str ); // Bilbo;Gandalf;Nazgul ``` ### reduce/reduceRight -When we need to iterate over an array -- we can use `forEach`. +When we need to iterate over an array -- we can use `forEach`, `for` or `for..of`. When we need to iterate and return the data for each element -- we can use `map`. @@ -500,24 +579,29 @@ The methods [arr.reduce](mdn:js/Array/reduce) and [arr.reduceRight](mdn:js/Array The syntax is: ```js -let value = arr.reduce(function(previousValue, item, index, arr) { +let value = arr.reduce(function(accumulator, item, index, array) { // ... -}, initial); +}, [initial]); ``` -The function is applied to the elements. You may notice the familiar arguments, starting from the 2nd: +The function is applied to all array elements one after another and "carries on" its result to the next call. + +Arguments: +- `accumulator` -- is the result of the previous function call, equals `initial` the first time (if `initial` is provided). - `item` -- is the current array item. - `index` -- is its position. -- `arr` -- is the array. +- `array` -- is the array. + +As the function is applied, the result of the previous function call is passed to the next one as the first argument. -So far, like `forEach/map`. But there's one more argument: +So, the first argument is essentially the accumulator that stores the combined result of all previous executions. And at the end, it becomes the result of `reduce`. -- `previousValue` -- is the result of the previous function call, `initial` for the first call. +Sounds complicated? The easiest way to grasp that is by example. -Here we get a sum of array in one line: +Here we get a sum of an array in one line: ```js run let arr = [1, 2, 3, 4, 5]; @@ -527,21 +611,21 @@ let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15 ``` -Here we used the most common variant of `reduce` which uses only 2 arguments. +The function passed to `reduce` uses only 2 arguments, that's typically enough. Let's see the details of what's going on. -1. On the first run, `sum` is the initial value (the last argument of `reduce`), equals `0`, and `current` is the first array element, equals `1`. So the result is `1`. +1. On the first run, `sum` is the `initial` value (the last argument of `reduce`), equals `0`, and `current` is the first array element, equals `1`. So the function result is `1`. 2. On the second run, `sum = 1`, we add the second array element (`2`) to it and return. 3. On the 3rd run, `sum = 3` and we add one more element to it, and so on... The calculation flow: -![](reduce.png) +![](reduce.svg) -Or in the form of a table, where each row represents is a function call on the next array element: +Or in the form of a table, where each row represents a function call on the next array element: -| |`sum`|`current`|`result`| +| |`sum`|`current`|result| |---|-----|---------|---------| |the first call|`0`|`1`|`1`| |the second call|`1`|`2`|`3`| @@ -549,8 +633,7 @@ Or in the form of a table, where each row represents is a function call on the n |the fourth call|`6`|`4`|`10`| |the fifth call|`10`|`5`|`15`| - -As we can see, the result of the previous call becomes the first argument of the next one. +Here we can clearly see how the result of the previous call becomes the first argument of the next one. We also can omit the initial value: @@ -579,39 +662,9 @@ let arr = []; arr.reduce((sum, current) => sum + current); ``` - So it's advised to always specify the initial value. -The method [arr.reduceRight](mdn:js/Array/reduceRight) does the same, but goes from right to left. - - -## Iterate: forEach - -The [arr.forEach](mdn:js/Array/forEach) method allows to run a function for every element of the array. - -The syntax: -```js -arr.forEach(function(item, index, array) { - // ... do something with item -}); -``` - -For instance, this shows each element of the array: - -```js run -// for each element call alert -["Bilbo", "Gandalf", "Nazgul"].forEach(alert); -``` - -And this code is more elaborate about their positions in the target array: - -```js run -["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { - alert(`${item} is at index ${index} in ${array}`); -}); -``` - -The result of the function (if it returns any) is thrown away and ignored. +The method [arr.reduceRight](mdn:js/Array/reduceRight) does the same but goes from right to left. ## Array.isArray @@ -621,7 +674,7 @@ So `typeof` does not help to distinguish a plain object from an array: ```js run alert(typeof {}); // object -alert(typeof []); // same +alert(typeof []); // object (same) ``` ...But arrays are used so often that there's a special method for that: [Array.isArray(value)](mdn:js/Array/isArray). It returns `true` if the `value` is an array, and `false` otherwise. @@ -636,7 +689,7 @@ alert(Array.isArray([])); // true Almost all array methods that call functions -- like `find`, `filter`, `map`, with a notable exception of `sort`, accept an optional additional parameter `thisArg`. -That parameter is not explained in the sections above, because it's rarely used. But for completeness we have to cover it. +That parameter is not explained in the sections above, because it's rarely used. But for completeness, we have to cover it. Here's the full syntax of these methods: @@ -650,80 +703,100 @@ arr.map(func, thisArg); The value of `thisArg` parameter becomes `this` for `func`. -For instance, here we use an object method as a filter and `thisArg` comes in handy: +For example, here we use a method of `army` object as a filter, and `thisArg` passes the context: ```js run -let user = { - age: 18, - younger(otherUser) { - return otherUser.age < this.age; +let army = { + minAge: 18, + maxAge: 27, + canJoin(user) { + return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [ - {age: 12}, {age: 16}, - {age: 32} + {age: 20}, + {age: 23}, + {age: 30} ]; *!* -// find all users younger than user -let youngerUsers = users.filter(user.younger, user); +// find users, for who army.canJoin returns true +let soldiers = users.filter(army.canJoin, army); */!* -alert(youngerUsers.length); // 2 +alert(soldiers.length); // 2 +alert(soldiers[0].age); // 20 +alert(soldiers[1].age); // 23 ``` -In the call above, we use `user.younger` as a filter and also provide `user` as the context for it. If we didn't provide the context, `users.filter(user.younger)` would call `user.younger` as a standalone function, with `this=undefined`. That would mean an instant error. +If in the example above we used `users.filter(army.canJoin)`, then `army.canJoin` would be called as a standalone function, with `this=undefined`, thus leading to an instant error. + +A call to `users.filter(army.canJoin, army)` can be replaced with `users.filter(user => army.canJoin(user))`, that does the same. The latter is used more often, as it's a bit easier to understand for most people. ## Summary -A cheatsheet of array methods: +A cheat sheet of array methods: - To add/remove elements: - `push(...items)` -- adds items to the end, - `pop()` -- extracts an item from the end, - `shift()` -- extracts an item from the beginning, - `unshift(...items)` -- adds items to the beginning. - - `splice(pos, deleteCount, ...items)` -- at index `pos` delete `deleteCount` elements and insert `items`. - - `slice(start, end)` -- creates a new array, copies elements from position `start` till `end` (not inclusive) into it. + - `splice(pos, deleteCount, ...items)` -- at index `pos` deletes `deleteCount` elements and inserts `items`. + - `slice(start, end)` -- creates a new array, copies elements from index `start` till `end` (not inclusive) into it. - `concat(...items)` -- returns a new array: copies all members of the current one and adds `items` to it. If any of `items` is an array, then its elements are taken. - To search among elements: - - `indexOf/lastIndexOf(item, pos)` -- look for `item` starting from position `pos`, return the index or `-1` if not found. + - `indexOf/lastIndexOf(item, pos)` -- look for `item` starting from position `pos`, and return the index or `-1` if not found. - `includes(value)` -- returns `true` if the array has `value`, otherwise `false`. - `find/filter(func)` -- filter elements through the function, return first/all values that make it return `true`. - `findIndex` is like `find`, but returns the index instead of a value. +- To iterate over elements: + - `forEach(func)` -- calls `func` for every element, does not return anything. + - To transform the array: - `map(func)` -- creates a new array from results of calling `func` for every element. - `sort(func)` -- sorts the array in-place, then returns it. - `reverse()` -- reverses the array in-place, then returns it. - `split/join` -- convert a string to array and back. - - `reduce(func, initial)` -- calculate a single value over the array by calling `func` for each element and passing an intermediate result between the calls. - -- To iterate over elements: - - `forEach(func)` -- calls `func` for every element, does not return anything. + - `reduce/reduceRight(func, initial)` -- calculate a single value over the array by calling `func` for each element and passing an intermediate result between the calls. - Additionally: - - `Array.isArray(arr)` checks `arr` for being an array. + - `Array.isArray(value)` checks `value` for being an array, if so returns `true`, otherwise `false`. Please note that methods `sort`, `reverse` and `splice` modify the array itself. These methods are the most used ones, they cover 99% of use cases. But there are few others: -- [arr.some(fn)](mdn:js/Array/some)/[arr.every(fn)](mdn:js/Array/every) checks the array. +- [arr.some(fn)](mdn:js/Array/some)/[arr.every(fn)](mdn:js/Array/every) check the array. The function `fn` is called on each element of the array similar to `map`. If any/all results are `true`, returns `true`, otherwise `false`. + These methods behave sort of like `||` and `&&` operators: if `fn` returns a truthy value, `arr.some()` immediately returns `true` and stops iterating over the rest of items; if `fn` returns a falsy value, `arr.every()` immediately returns `false` and stops iterating over the rest of items as well. + + We can use `every` to compare arrays: + + ```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) -- fills the array with repeating `value` from index `start` to `end`. - [arr.copyWithin(target, start, end)](mdn:js/Array/copyWithin) -- copies its elements from position `start` till position `end` into *itself*, at position `target` (overwrites existing). +- [arr.flat(depth)](mdn:js/Array/flat)/[arr.flatMap(fn)](mdn:js/Array/flatMap) create a new flat array from a multidimensional array. + For the full list, see the [manual](mdn:js/Array). -From the first sight it may seem that there are so many methods, quite difficult to remember. But actually that's much easier than it seems. +At first sight, it may seem that there are so many methods, quite difficult to remember. But actually, that's much easier. -Look through the cheatsheet just to be aware of them. Then solve the tasks of this chapter to practice, so that you have experience with array methods. +Look through the cheat sheet just to be aware of them. Then solve the tasks of this chapter to practice, so that you have experience with array methods. -Afterwards whenever you need to do something with an array, and you don't know how -- come here, look at the cheatsheet and find the right method. Examples will help you to write it correctly. Soon you'll automatically remember the methods, without specific efforts from your side. +Afterwards whenever you need to do something with an array, and you don't know how -- come here, look at the cheat sheet and find the right method. Examples will help you to write it correctly. Soon you'll automatically remember the methods, without specific efforts from your side. diff --git a/1-js/05-data-types/05-array-methods/reduce.png b/1-js/05-data-types/05-array-methods/reduce.png deleted file mode 100644 index 41476d2aea..0000000000 Binary files a/1-js/05-data-types/05-array-methods/reduce.png and /dev/null differ diff --git a/1-js/05-data-types/05-array-methods/reduce.svg b/1-js/05-data-types/05-array-methods/reduce.svg new file mode 100644 index 0000000000..180941dc29 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/reduce.svg @@ -0,0 +1 @@ +1sum 0 current 12sum 0+1 current 23sum 0+1+2 current 34sum 0+1+2+3 current 45sum 0+1+2+3+4 current 50+1+2+3+4+5 = 15 \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/reduce@2x.png b/1-js/05-data-types/05-array-methods/reduce@2x.png deleted file mode 100644 index f31647d17b..0000000000 Binary files a/1-js/05-data-types/05-array-methods/reduce@2x.png and /dev/null differ diff --git a/1-js/05-data-types/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md index 300d8f265c..e2c0d4f976 100644 --- a/1-js/05-data-types/06-iterable/article.md +++ b/1-js/05-data-types/06-iterable/article.md @@ -1,17 +1,18 @@ # Iterables -*Iterable* objects is a generalization of arrays. That's a concept that allows to make any object useable in a `for..of` loop. +*Iterable* objects are a generalization of arrays. That's a concept that allows us to make any object useable in a `for..of` loop. -Arrays by themselves are iterable. But not only arrays. Strings are iterable too, and many other built-in objects as well. +Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well. For instance, strings are also iterable. + +If an object isn't technically an array, but represents a collection (list, set) of something, then `for..of` is a great syntax to loop over it, so let's see how to make it work. -Iterables are widely used by the core JavaScript. As we'll see many built-in operators and methods rely on them. ## Symbol.iterator We can easily grasp the concept of iterables by making one of our own. -For instance, we have an object, that is not an array, but looks suitable for `for..of`. +For instance, we have an object that is not an array, but looks suitable for `for..of`. Like a `range` object that represents an interval of numbers: @@ -25,14 +26,14 @@ let range = { // for(let num of range) ... num=1,2,3,4,5 ``` -To make the `range` iterable (and thus let `for..of` work) we need to add a method to the object named `Symbol.iterator` (a special built-in symbol just for that). +To make the `range` object iterable (and thus let `for..of` work) we need to add a method to the object named `Symbol.iterator` (a special built-in symbol just for that). -- When `for..of` starts, it calls that method (or errors if not found). -- The method must return an *iterator* -- an object with the method `next`. -- When `for..of` wants the next value, it calls `next()` on that object. -- The result of `next()` must have the form `{done: Boolean, value: any}`, where `done=true` means that the iteration is finished, otherwise `value` must be the new value. +1. When `for..of` starts, it calls that method once (or errors if not found). The method must return an *iterator* -- an object with the method `next`. +2. Onward, `for..of` works *only with that returned object*. +3. When `for..of` wants the next value, it calls `next()` on that object. +4. The result of `next()` must have the form `{done: Boolean, value: any}`, where `done=true` means that the loop is finished, otherwise `value` is the next value. -Here's the full implementation for `range`: +Here's the full implementation for `range` with remarks: ```js run let range = { @@ -43,10 +44,11 @@ let range = { // 1. call to for..of initially calls this range[Symbol.iterator] = function() { - // 2. ...it returns the iterator: + // ...it returns the iterator object: + // 2. Onward, for..of works only with the iterator object below, asking it for next values return { current: this.from, - last: this.to, + last: this.to, // 3. next() is called on each iteration by the for..of loop next() { @@ -66,10 +68,10 @@ for (let num of range) { } ``` -There is an important separation of concerns in this code: +Please note the core feature of iterables: separation of concerns. - The `range` itself does not have the `next()` method. -- Instead, another object, a so-called "iterator" is created by the call to `range[Symbol.iterator]()`, and it handles the iteration. +- Instead, another object, a so-called "iterator" is created by the call to `range[Symbol.iterator]()`, and its `next()` generates values for the iteration. So, the iterator object is separate from the object it iterates over. @@ -101,10 +103,12 @@ for (let num of range) { } ``` -Now `range[Symbol.iterator]()` returns the `range` object itself: it has the necessary `next()` method and remembers the current iteration progress in `this.current`. Sometimes that's fine too. The downside is that now it's impossible to have two `for..of` loops running over the object simultaneously: they'll share the iteration state, because there's only one iterator -- the object itself. +Now `range[Symbol.iterator]()` returns the `range` object itself: it has the necessary `next()` method and remembers the current iteration progress in `this.current`. Shorter? Yes. And sometimes that's fine too. + +The downside is that now it's impossible to have two `for..of` loops running over the object simultaneously: they'll share the iteration state, because there's only one iterator -- the object itself. But two parallel for-ofs is a rare thing, even in async scenarios. ```smart header="Infinite iterators" -Infinite iterators are also doable. For instance, the `range` becomes infinite for `range.to = Infinity`. Or we can make an iterable object that generates an infinite sequence of pseudorandom numbers. Also can be useful. +Infinite iterators are also possible. For instance, the `range` becomes infinite for `range.to = Infinity`. Or we can make an iterable object that generates an infinite sequence of pseudorandom numbers. Also can be useful. There are no limitations on `next`, it can return more and more values, that's normal. @@ -120,11 +124,12 @@ For a string, `for..of` loops over its characters: ```js run for (let char of "test") { + // triggers 4 times: once for each character alert( char ); // t, then e, then s, then t } ``` -And it works right with surrogate pairs! +And it works correctly with surrogate pairs! ```js run let str = '𝒳😂'; @@ -135,11 +140,9 @@ for (let char of str) { ## Calling an iterator explicitly -Normally, internals of iterables are hidden from the external code. There's a `for..of` loop, that works, that's all it needs to know. - -But to understand things a little bit deeper let's see how to create an iterator explicitly. +For deeper understanding, let's see how to use an iterator explicitly. -We'll iterate over a string the same way as `for..of`, but with direct calls. This code gets a string iterator and calls it "manually": +We'll iterate over a string in exactly the same way as `for..of`, but with direct calls. This code creates a string iterator and gets values from it "manually": ```js run let str = "Hello"; @@ -147,7 +150,9 @@ let str = "Hello"; // does the same as // for (let char of str) alert(char); +*!* let iterator = str[Symbol.iterator](); +*/!* while (true) { let result = iterator.next(); @@ -160,14 +165,16 @@ That is rarely needed, but gives us more control over the process than `for..of` ## Iterables and array-likes [#array-like] -There are two official terms that look similar, but are very different. Please make sure you understand them well to avoid the confusion. +Two official terms look similar, but are very different. Please make sure you understand them well to avoid the confusion. - *Iterables* are objects that implement the `Symbol.iterator` method, as described above. - *Array-likes* are objects that have indexes and `length`, so they look like arrays. -Naturally, these properties can combine. For instance, strings are both iterable (`for..of` works on them) and array-like (they have numeric indexes and `length`). +When we use JavaScript for practical tasks in a browser or any other environment, we may meet objects that are iterables or array-likes, or both. -But an iterable may be not array-like. And vice versa an array-like may be not iterable. +For instance, strings are both iterable (`for..of` works on them) and array-like (they have numeric indexes and `length`). + +But an iterable may not be array-like. And vice versa an array-like may not be iterable. For example, the `range` in the example above is iterable, but not array-like, because it does not have indexed properties and `length`. @@ -186,11 +193,11 @@ for (let item of arrayLike) {} */!* ``` -What do they have in common? Both iterables and array-likes are usually *not arrays*, they don't have `push`, `pop` etc. That's rather inconvenient if we have such an object and want to work with it as with an array. +Both iterables and array-likes are usually *not arrays*, they don't have `push`, `pop` etc. That's rather inconvenient if we have such an object and want to work with it as with an array. E.g. we would like to work with `range` using array methods. How to achieve that? ## Array.from -There's a universal method [Array.from](mdn:js/Array/from) that brings them together. It takes an iterable or array-like value and makes a "real" `Array` from it. Then we can call array methods on it. +There's a universal method [Array.from](mdn:js/Array/from) that takes an iterable or array-like value and makes a "real" `Array` from it. Then we can call array methods on it. For instance: @@ -207,26 +214,26 @@ let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // World (method works) ``` -`Array.from` at the line `(*)` takes the object, examines it for being an iterable or array-like, then makes a new array and copies there all items. +`Array.from` at the line `(*)` takes the object, examines it for being an iterable or array-like, then makes a new array and copies all items to it. The same happens for an iterable: -```js +```js run // assuming that range is taken from the example above let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (array toString conversion works) ``` -The full syntax for `Array.from` allows to provide an optional "mapping" function: +The full syntax for `Array.from` also allows us to provide an optional "mapping" function: ```js Array.from(obj[, mapFn, thisArg]) ``` -The second argument `mapFn` should be the function to apply to each element before adding to the array, and `thisArg` allows to set `this` for it. +The optional second argument `mapFn` can be a function that will be applied to each element before adding it to the array, and `thisArg` allows us to set `this` for it. For instance: -```js +```js run // assuming that range is taken from the example above // square each number @@ -263,7 +270,7 @@ for (let char of str) { alert(chars); ``` -...But is shorter. +...But it is shorter. We can even build surrogate-aware `slice` on it: @@ -276,7 +283,7 @@ let str = '𝒳😂𩷶'; alert( slice(str, 1, 3) ); // 😂𩷶 -// native method does not support surrogate pairs +// the native method does not support surrogate pairs alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs) ``` @@ -286,8 +293,8 @@ alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs) Objects that can be used in `for..of` are called *iterable*. - Technically, iterables must implement the method named `Symbol.iterator`. - - The result of `obj[Symbol.iterator]` is called an *iterator*. It handles the further iteration process. - - An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the iteration end, otherwise the `value` is the next value. + - The result of `obj[Symbol.iterator]()` is called an *iterator*. It handles further iteration process. + - An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the end of the iteration process, otherwise the `value` is the next value. - The `Symbol.iterator` method is called automatically by `for..of`, but we also can do it directly. - Built-in iterables like strings or arrays, also implement `Symbol.iterator`. - String iterator knows about surrogate pairs. @@ -297,4 +304,4 @@ Objects that have indexed properties and `length` are called *array-like*. Such If we look inside the specification -- we'll see that most built-in methods assume that they work with iterables or array-likes instead of "real" arrays, because that's more abstract. -`Array.from(obj[, mapFn, thisArg])` makes a real `Array` of an iterable or array-like `obj`, and we can then use array methods on it. The optional arguments `mapFn` and `thisArg` allow us to apply a function to each item. +`Array.from(obj[, mapFn, thisArg])` makes a real `Array` from an iterable or array-like `obj`, and we can then use array methods on it. The optional arguments `mapFn` and `thisArg` allow us to apply a function to each item. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md deleted file mode 100644 index dacb42856c..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md +++ /dev/null @@ -1,77 +0,0 @@ -To find all anagrams, let's split every word to letters and sort them. When letter-sorted, all anagrams are same. - -For instance: - -``` -nap, pan -> anp -ear, era, are -> aer -cheaters, hectares, teachers -> aceehrst -... -``` - -We'll use the letter-sorted variants as map keys to store only one value per each key: - -```js run -function aclean(arr) { - let map = new Map(); - - for (let word of arr) { - // split the word by letters, sort them and join back -*!* - let sorted = word.toLowerCase().split('').sort().join(''); // (*) -*/!* - map.set(sorted, word); - } - - return Array.from(map.values()); -} - -let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; - -alert( aclean(arr) ); -``` - -Letter-sorting is done by the chain of calls in the line `(*)`. - -For convenience let's split it into multiple lines: - -```js -let sorted = arr[i] // PAN - .toLowerCase() // pan - .split('') // ['p','a','n'] - .sort() // ['a','n','p'] - .join(''); // anp -``` - -Two different words `'PAN'` and `'nap'` receive the same letter-sorted form `'anp'`. - -The next line put the word into the map: - -```js -map.set(sorted, word); -``` - -If we ever meet a word the same letter-sorted form again, then it would overwrite the previous value with the same key in the map. So we'll always have at maximum one word per letter-form. - -At the end `Array.from(map.values())` takes an iterable over map values (we don't need keys in the result) and returns an array of them. - -Here we could also use a plain object instead of the `Map`, because keys are strings. - -That's how the solution can look: - -```js run -function aclean(arr) { - let obj = {}; - - for (let i = 0; i < arr.length; i++) { - let sorted = arr[i].toLowerCase().split("").sort().join(""); - obj[sorted] = arr[i]; - } - - return Array.from(Object.values(obj)); -} - -let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; - -alert( aclean(arr) ); -``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md deleted file mode 100644 index 9a3e5d250f..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md +++ /dev/null @@ -1,24 +0,0 @@ -importance: 5 - ---- - -# Iterable keys - -We want to get an array of `map.keys()` and go on working with it (apart from the map itself). - -But there's a problem: - -```js run -let map = new Map(); - -map.set("name", "John"); - -let keys = map.keys(); - -*!* -// Error: numbers.push is not a function -keys.push("more"); -*/!* -``` - -Why? How can we fix the code to make `keys.push` work? diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md deleted file mode 100644 index ce56f593a2..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md +++ /dev/null @@ -1,41 +0,0 @@ -The sane choice here is a `WeakSet`: - -```js -let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} -]; - -let readMessages = new WeakSet(); - -// two messages have been read -readMessages.add(messages[0]); -readMessages.add(messages[1]); -// readMessages has 2 elements - -// ...let's read the first message again! -readMessages.add(messages[0]); -// readMessages still has 2 unique elements - -// answer: was the message[0] read? -alert("Read message 0: " + readMessages.has(messages[0])); // true - -messages.shift(); -// now readMessages has 1 element (technically memory may be cleaned later) -``` - -The `WeakSet` allows to store a set of messages and easily check for the existance of a message in it. - -It cleans up itself automatically. The tradeoff is that we can't iterate over it. We can't get "all read messages" directly. But we can do it by iterating over all messages and filtering those that are in the set. - -P.S. Adding a property of our own to each message may be dangerous if messages are managed by someone else's code, but we can make it a symbol to evade conflicts. - -Like this: -```js -// the symbolic property is only known to our code -let isRead = Symbol("isRead"); -messages[0][isRead] = true; -``` - -Now even if someone else's code uses `for..in` loop for message properties, our secret flag won't appear. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md deleted file mode 100644 index 7ec1faf16c..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md +++ /dev/null @@ -1,23 +0,0 @@ -importance: 5 - ---- - -# Store "unread" flags - -There's an array of messages: - -```js -let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} -]; -``` - -Your code can access it, but the messages are managed by someone else's code. New messages are added, old ones are removed regularly by that code, and you don't know the exact moments when it happens. - -Now, which data structure you could use to store information whether the message "have been read"? The structure must be well-suited to give the answer "was it read?" for the given message object. - -P.S. When a message is removed from `messages`, it should disappear from your structure as well. - -P.P.S. We shouldn't modify message objects directly. If they are managed by someone else's code, then adding extra properties to them may have bad consequences. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md b/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md deleted file mode 100644 index 7f387b4da0..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md +++ /dev/null @@ -1,15 +0,0 @@ - -To store a date, we can use `WeakMap`: - -```js -let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} -]; - -let readMap = new WeakMap(); - -readMap.set(messages[0], new Date(2017, 1, 1)); -// Date object we'll study later -``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md b/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md deleted file mode 100644 index 22b51a382e..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md +++ /dev/null @@ -1,19 +0,0 @@ -importance: 5 - ---- - -# Store read dates - -There's an array of messages as in the [previous task](info:task/recipients-read). The situation is similar. - -```js -let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} -]; -``` - -The question now is: which data structure you'd suggest to store the information: "when the message was read?". - -In the previous task we only needed to store the "yes/no" fact. Now we need to store the date and it, once again, should disappear if the message is gone. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/article.md b/1-js/05-data-types/07-map-set-weakmap-weakset/article.md deleted file mode 100644 index 8af8b6cd11..0000000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/article.md +++ /dev/null @@ -1,432 +0,0 @@ - -# Map, Set, WeakMap and WeakSet - -Now we've learned about the following complex data structures: - -- Objects for storing keyed collections. -- Arrays for storing ordered collections. - -But that's not enough for real life. That's why `Map` and `Set` also exist. - -## Map - -[Map](mdn:js/Map) is a collection of keyed data items, just like an `Object`. But the main difference is that `Map` allows keys of any type. - -The main methods are: - -- `new Map()` -- creates the map. -- `map.set(key, value)` -- stores the value by the key. -- `map.get(key)` -- returns the value by the key, `undefined` if `key` doesn't exist in map. -- `map.has(key)` -- returns `true` if the `key` exists, `false` otherwise. -- `map.delete(key)` -- removes the value by the key. -- `map.clear()` -- clears the map -- `map.size` -- returns the current element count. - -For instance: - -```js run -let map = new Map(); - -map.set('1', 'str1'); // a string key -map.set(1, 'num1'); // a numeric key -map.set(true, 'bool1'); // a boolean key - -// remember the regular Object? it would convert keys to string -// Map keeps the type, so these two are different: -alert( map.get(1) ); // 'num1' -alert( map.get('1') ); // 'str1' - -alert( map.size ); // 3 -``` - -As we can see, unlike objects, keys are not converted to strings. Any type of key is possible. - -**Map can also use objects as keys.** - -For instance: -```js run -let john = { name: "John" }; - -// for every user, let's store their visits count -let visitsCountMap = new Map(); - -// john is the key for the map -visitsCountMap.set(john, 123); - -alert( visitsCountMap.get(john) ); // 123 -``` - -Using objects as keys is one of most notable and important `Map` features. For string keys, `Object` can be fine, but it would be difficult to replace the `Map` with a regular `Object` in the example above. - -In the old times, before `Map` existed, people added unique identifiers to objects for that: - -```js run -// we add the id field -let john = { name: "John", *!*id: 1*/!* }; - -let visitsCounts = {}; - -// now store the value by id -visitsCounts[john.id] = 123; - -alert( visitsCounts[john.id] ); // 123 -``` - -...But `Map` is much more elegant. - - -```smart header="How `Map` compares keys" -To test values for equivalence, `Map` uses the algorithm [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). It is roughly the same as strict equality `===`, but the difference is that `NaN` is considered equal to `NaN`. So `NaN` can be used as the key as well. - -This algorithm can't be changed or customized. -``` - - -````smart header="Chaining" - -Every `map.set` call returns the map itself, so we can "chain" the calls: - -```js -map.set('1', 'str1') - .set(1, 'num1') - .set(true, 'bool1'); -``` -```` - -## Map from Object - -When a `Map` is created, we can pass an array (or another iterable) with key-value pairs, like this: - -```js -// array of [key, value] pairs -let map = new Map([ - ['1', 'str1'], - [1, 'num1'], - [true, 'bool1'] -]); -``` - -There is a built-in method [Object.entries(obj)](mdn:js/Object/entries) that returns an array of key/value pairs for an object exactly in that format. - -So we can initialize a map from an object like this: - -```js -let map = new Map(Object.entries({ - name: "John", - age: 30 -})); -``` - -Here, `Object.entries` returns the array of key/value pairs: `[ ["name","John"], ["age", 30] ]`. That's what `Map` needs. - -## Iteration over Map - -For looping over a `map`, there are 3 methods: - -- `map.keys()` -- returns an iterable for keys, -- `map.values()` -- returns an iterable for values, -- `map.entries()` -- returns an iterable for entries `[key, value]`, it's used by default in `for..of`. - -For instance: - -```js run -let recipeMap = new Map([ - ['cucumber', 500], - ['tomatoes', 350], - ['onion', 50] -]); - -// iterate over keys (vegetables) -for (let vegetable of recipeMap.keys()) { - alert(vegetable); // cucumber, tomatoes, onion -} - -// iterate over values (amounts) -for (let amount of recipeMap.values()) { - alert(amount); // 500, 350, 50 -} - -// iterate over [key, value] entries -for (let entry of recipeMap) { // the same as of recipeMap.entries() - alert(entry); // cucumber,500 (and so on) -} -``` - -```smart header="The insertion order is used" -The iteration goes in the same order as the values were inserted. `Map` preserves this order, unlike a regular `Object`. -``` - -Besides that, `Map` has a built-in `forEach` method, similar to `Array`: - -```js -recipeMap.forEach( (value, key, map) => { - alert(`${key}: ${value}`); // cucumber: 500 etc -}); -``` - - -## Set - -A `Set` is a collection of values, where each value may occur only once. - -Its main methods are: - -- `new Set(iterable)` -- creates the set, optionally from an array of values (any iterable will do). -- `set.add(value)` -- adds a value, returns the set itself. -- `set.delete(value)` -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. -- `set.has(value)` -- returns `true` if the value exists in the set, otherwise `false`. -- `set.clear()` -- removes everything from the set. -- `set.size` -- is the elements count. - -For example, we have visitors coming, and we'd like to remember everyone. But repeated visits should not lead to duplicates. A visitor must be "counted" only once. - -`Set` is just the right thing for that: - -```js run -let set = new Set(); - -let john = { name: "John" }; -let pete = { name: "Pete" }; -let mary = { name: "Mary" }; - -// visits, some users come multiple times -set.add(john); -set.add(pete); -set.add(mary); -set.add(john); -set.add(mary); - -// set keeps only unique values -alert( set.size ); // 3 - -for (let user of set) { - alert(user.name); // John (then Pete and Mary) -} -``` - -The alternative to `Set` could be an array of users, and the code to check for duplicates on every insertion using [arr.find](mdn:js/Array/find). But the performance would be much worse, because this method walks through the whole array checking every element. `Set` is much better optimized internally for uniqueness checks. - -## Iteration over Set - -We can loop over a set either with `for..of` or using `forEach`: - -```js run -let set = new Set(["oranges", "apples", "bananas"]); - -for (let value of set) alert(value); - -// the same with forEach: -set.forEach((value, valueAgain, set) => { - alert(value); -}); -``` - -Note the funny thing. The `forEach` function in the `Set` has 3 arguments: a value, then *again a value*, and then the target object. Indeed, the same value appears in the arguments twice. - -That's for compatibility with `Map` where `forEach` has three arguments. - -The same methods `Map` has for iterators are also supported: - -- `set.keys()` -- returns an iterable object for values, -- `set.values()` -- same as `set.keys`, for compatibility with `Map`, -- `set.entries()` -- returns an iterable object for entries `[value, value]`, exists for compatibility with `Map`. - -## WeakMap and WeakSet - -`WeakSet` is a special kind of `Set` that does not prevent JavaScript from removing its items from memory. `WeakMap` is the same thing for `Map`. - -As we know from the chapter , JavaScript engine stores a value in memory while it is reachable (and can potentially be used). - -For instance: -```js -let john = { name: "John" }; - -// the object can be accessed, john is the reference to it - -// overwrite the reference -john = null; - -*!* -// the object will be removed from memory -*/!* -``` - -Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory. - -In a regular `Map`, it does not matter if we store an object as a key or as a value. It's kept in memory even if there are no more references to it. - -For instance: -```js -let john = { name: "John" }; - -let map = new Map(); -map.set(john, "..."); - -john = null; // overwrite the reference - -*!* -// john is stored inside the map -// we can get it by using map.keys() -*/!* -``` - - -With the exception of `WeakMap/WeakSet`. - -**`WeakMap/WeakSet` does not prevent the object removal from the memory.** - -Let's start with `WeakMap`. - -The first difference from `Map` is that its keys must be objects, not primitive values: - -```js run -let weakMap = new WeakMap(); - -let obj = {}; - -weakMap.set(obj, "ok"); // works fine (object key) - -*!* -weakMap.set("test", "Whoops"); // Error, because "test" is a primitive -*/!* -``` - -Now, if we use an object as the key in it, and there are no other references to that object -- it will be removed from memory (and from the map) automatically. - -```js -let john = { name: "John" }; - -let weakMap = new WeakMap(); -weakMap.set(john, "..."); - -john = null; // overwrite the reference - -// john is removed from memory! -``` - -Compare it with the regular `Map` example above. Now if `john` only exists as the key of `WeakMap` -- it is to be automatically deleted. - -...And `WeakMap` does not support methods `keys()`, `values()`, `entries()`, we can not iterate over it. So there's really no way to receive all keys or values from it. - -`WeakMap` has only the following methods: - -- `weakMap.get(key)` -- `weakMap.set(key, value)` -- `weakMap.delete(key, value)` -- `weakMap.has(key)` - -Why such a limitation? That's for technical reasons. If the object has lost all other references (like `john` in the code above), then it is to be deleted automatically. But technically it's not exactly specified *when the cleanup happens*. - -The JavaScript engine decides that. It may choose to perform the memory cleanup immediately or to wait and do the cleaning later when more deletions happen. So, technically the current element count of the `WeakMap` is not known. The engine may have cleaned it up or not, or did it partially. For that reason, methods that access `WeakMap` as a whole are not supported. - -Now where do we need such thing? - -The idea of `WeakMap` is that we can store something for an object that exists only while the object exists. But we do not force the object to live by the mere fact that we store something for it. - -```js -weakMap.put(john, "secret documents"); -// if john dies, secret documents will be destroyed -``` - -That's useful for situations when we have a main storage for the objects somewhere and need to keep additional information that is only relevant while the object lives. - -Let's look at an example. - -For instance, we have code that keeps a visit count for each user. The information is stored in a map: a user is the key and the visit count is the value. When a user leaves, we don't want to store their visit count anymore. - -One way would be to keep track of leaving users and clean up the storage manually: - -```js run -let john = { name: "John" }; - -// map: user => visits count -let visitsCountMap = new Map(); - -// john is the key for the map -visitsCountMap.set(john, 123); - -// now john leaves us, we don't need him anymore -john = null; - -*!* -// but it's still in the map, we need to clean it! -*/!* -alert( visitsCountMap.size ); // 1 -// it's also in the memory, because Map uses it as the key -``` - -Another way would be to use `WeakMap`: - -```js -let john = { name: "John" }; - -let visitsCountMap = new WeakMap(); - -visitsCountMap.set(john, 123); - -// now john leaves us, we don't need him anymore -john = null; - -// there are no references except WeakMap, -// so the object is removed both from the memory and from visitsCountMap automatically -``` - -With a regular `Map`, cleaning up after a user has left becomes a tedious task: we not only need to remove the user from its main storage (be it a variable or an array), but also need to clean up the additional stores like `visitsCountMap`. And it can become cumbersome in more complex cases when users are managed in one place of the code and the additional structure is at another place and is getting no information about removals. - -`WeakMap` can make things simpler, because it is cleaned up automatically. The information in it like visits count in the example above lives only while the key object exists. - -`WeakSet` behaves similarly: - -- It is analogous to `Set`, but we may only add objects to `WeakSet` (not primitives). -- An object exists in the set while it has reachable from somewhere else. -- Like `Set`, it supports `add`, `has` and `delete`, but not `size`, `keys()` and no iterations. - -For instance, we can use it to keep track of whether an item is checked: - -```js -let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} -]; - -// fill it with array elements (3 items) -let unreadSet = new WeakSet(messages); - -// we can use unreadSet to see whether a message is unread -alert(unreadSet.has(messages[1])); // true -// remove it from the set after reading -unreadSet.delete(messages[1]); // true - -// and when we shift our messages history, the set is cleaned up automatically -messages.shift(); -// no need to clean unreadSet, it now has 2 items -// unfortunately, there's no method to get the exact count of items, so can't show it -``` - -The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterations, and inability to get all current content. That may appear inconvenient, but actually does not prevent `WeakMap/WeakSet` from doing their main job -- be an "additional" storage of data for objects which are stored/managed at another place. - -## Summary - -- `Map` -- is a collection of keyed values. - - The differences from a regular `Object`: - - - Any keys, objects can be keys. - - Iterates in the insertion order. - - Additional convenient methods, the `size` property. - -- `Set` -- is a collection of unique values. - - - Unlike an array, does not allow to reorder elements. - - Keeps the insertion order. - -- `WeakMap` -- a variant of `Map` that allows only objects as keys and removes them once they become inaccessible by other means. - - - It does not support operations on the structure as a whole: no `size`, no `clear()`, no iterations. - -- `WeakSet` -- is a variant of `Set` that only stores objects and removes them once they become inaccessible by other means. - - - Also does not support `size/clear()` and iterations. - -`WeakMap` and `WeakSet` are used as "secondary" data structures in addition to the "main" object storage. Once the object is removed from the main storage, so it only stays in `WeakMap/WeakSet`, they clean up automatically. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js b/1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js rename to 1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/solution.js diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js b/1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js rename to 1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/test.js diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/solution.md b/1-js/05-data-types/07-map-set/01-array-unique-map/solution.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/solution.md rename to 1-js/05-data-types/07-map-set/01-array-unique-map/solution.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md b/1-js/05-data-types/07-map-set/01-array-unique-map/task.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md rename to 1-js/05-data-types/07-map-set/01-array-unique-map/task.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js b/1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/solution.js diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js b/1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/test.js diff --git a/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md new file mode 100644 index 0000000000..1606751852 --- /dev/null +++ b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md @@ -0,0 +1,77 @@ +To find all anagrams, let's split every word to letters and sort them. When letter-sorted, all anagrams are same. + +For instance: + +``` +nap, pan -> anp +ear, era, are -> aer +cheaters, hectares, teachers -> aceehrst +... +``` + +We'll use the letter-sorted variants as map keys to store only one value per each key: + +```js run +function aclean(arr) { + let map = new Map(); + + for (let word of arr) { + // split the word by letters, sort them and join back +*!* + let sorted = word.toLowerCase().split('').sort().join(''); // (*) +*/!* + map.set(sorted, word); + } + + return Array.from(map.values()); +} + +let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; + +alert( aclean(arr) ); +``` + +Letter-sorting is done by the chain of calls in the line `(*)`. + +For convenience let's split it into multiple lines: + +```js +let sorted = word // PAN + .toLowerCase() // pan + .split('') // ['p','a','n'] + .sort() // ['a','n','p'] + .join(''); // anp +``` + +Two different words `'PAN'` and `'nap'` receive the same letter-sorted form `'anp'`. + +The next line put the word into the map: + +```js +map.set(sorted, word); +``` + +If we ever meet a word the same letter-sorted form again, then it would overwrite the previous value with the same key in the map. So we'll always have at maximum one word per letter-form. + +At the end `Array.from(map.values())` takes an iterable over map values (we don't need keys in the result) and returns an array of them. + +Here we could also use a plain object instead of the `Map`, because keys are strings. + +That's how the solution can look: + +```js run demo +function aclean(arr) { + let obj = {}; + + for (let i = 0; i < arr.length; i++) { + let sorted = arr[i].toLowerCase().split("").sort().join(""); + obj[sorted] = arr[i]; + } + + return Object.values(obj); +} + +let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"]; + +alert( aclean(arr) ); +``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/task.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/task.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md b/1-js/05-data-types/07-map-set/03-iterable-keys/solution.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md rename to 1-js/05-data-types/07-map-set/03-iterable-keys/solution.md diff --git a/1-js/05-data-types/07-map-set/03-iterable-keys/task.md b/1-js/05-data-types/07-map-set/03-iterable-keys/task.md new file mode 100644 index 0000000000..81507647f4 --- /dev/null +++ b/1-js/05-data-types/07-map-set/03-iterable-keys/task.md @@ -0,0 +1,24 @@ +importance: 5 + +--- + +# Iterable keys + +We'd like to get an array of `map.keys()` in a variable and then apply array-specific methods to it, e.g. `.push`. + +But that doesn't work: + +```js run +let map = new Map(); + +map.set("name", "John"); + +let keys = map.keys(); + +*!* +// Error: keys.push is not a function +keys.push("more"); +*/!* +``` + +Why? How can we fix the code to make `keys.push` work? diff --git a/1-js/05-data-types/07-map-set/article.md b/1-js/05-data-types/07-map-set/article.md new file mode 100644 index 0000000000..37f5e48c2d --- /dev/null +++ b/1-js/05-data-types/07-map-set/article.md @@ -0,0 +1,331 @@ + +# Map and Set + +Till now, we've learned about the following complex data structures: + +- Objects are used for storing keyed collections. +- Arrays are used for storing ordered collections. + +But that's not enough for real life. That's why `Map` and `Set` also exist. + +## Map + +[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) is a collection of keyed data items, just like an `Object`. But the main difference is that `Map` allows keys of any type. + +Methods and properties are: + +- [`new Map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) -- creates the map. +- [`map.set(key, value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set) -- stores the value by the key. +- [`map.get(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) -- returns the value by the key, `undefined` if `key` doesn't exist in map. +- [`map.has(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) -- returns `true` if the `key` exists, `false` otherwise. +- [`map.delete(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete) -- removes the element (the key/value pair) by the key. +- [`map.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) -- removes everything from the map. +- [`map.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size) -- returns the current element count. + +For instance: + +```js run +let map = new Map(); + +map.set('1', 'str1'); // a string key +map.set(1, 'num1'); // a numeric key +map.set(true, 'bool1'); // a boolean key + +// remember the regular Object? it would convert keys to string +// Map keeps the type, so these two are different: +alert( map.get(1) ); // 'num1' +alert( map.get('1') ); // 'str1' + +alert( map.size ); // 3 +``` + +As we can see, unlike objects, keys are not converted to strings. Any type of key is possible. + +```smart header="`map[key]` isn't the right way to use a `Map`" +Although `map[key]` also works, e.g. we can set `map[key] = 2`, this is treating `map` as a plain JavaScript object, so it implies all corresponding limitations (only string/symbol keys and so on). + +So we should use `map` methods: `set`, `get` and so on. +``` + +**Map can also use objects as keys.** + +For instance: + +```js run +let john = { name: "John" }; + +// for every user, let's store their visits count +let visitsCountMap = new Map(); + +// john is the key for the map +visitsCountMap.set(john, 123); + +alert( visitsCountMap.get(john) ); // 123 +``` + +Using objects as keys is one of the most notable and important `Map` features. The same does not count for `Object`. String as a key in `Object` is fine, but we can't use another `Object` as a key in `Object`. + +Let's try: + +```js run +let john = { name: "John" }; +let ben = { name: "Ben" }; + +let visitsCountObj = {}; // try to use an object + +visitsCountObj[ben] = 234; // try to use ben object as the key +visitsCountObj[john] = 123; // try to use john object as the key, ben object will get replaced + +*!* +// That's what got written! +alert( visitsCountObj["[object Object]"] ); // 123 +*/!* +``` + +As `visitsCountObj` is an object, it converts all `Object` keys, such as `john` and `ben` above, to same string `"[object Object]"`. Definitely not what we want. + +```smart header="How `Map` compares keys" +To test keys for equivalence, `Map` uses the algorithm [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). It is roughly the same as strict equality `===`, but the difference is that `NaN` is considered equal to `NaN`. So `NaN` can be used as the key as well. + +This algorithm can't be changed or customized. +``` + +````smart header="Chaining" +Every `map.set` call returns the map itself, so we can "chain" the calls: + +```js +map.set('1', 'str1') + .set(1, 'num1') + .set(true, 'bool1'); +``` +```` + +## Iteration over Map + +For looping over a `map`, there are 3 methods: + +- [`map.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys) -- returns an iterable for keys, +- [`map.values()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values) -- returns an iterable for values, +- [`map.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) -- returns an iterable for entries `[key, value]`, it's used by default in `for..of`. + +For instance: + +```js run +let recipeMap = new Map([ + ['cucumber', 500], + ['tomatoes', 350], + ['onion', 50] +]); + +// iterate over keys (vegetables) +for (let vegetable of recipeMap.keys()) { + alert(vegetable); // cucumber, tomatoes, onion +} + +// iterate over values (amounts) +for (let amount of recipeMap.values()) { + alert(amount); // 500, 350, 50 +} + +// iterate over [key, value] entries +for (let entry of recipeMap) { // the same as of recipeMap.entries() + alert(entry); // cucumber,500 (and so on) +} +``` + +```smart header="The insertion order is used" +The iteration goes in the same order as the values were inserted. `Map` preserves this order, unlike a regular `Object`. +``` + +Besides that, `Map` has a built-in `forEach` method, similar to `Array`: + +```js +// runs the function for each (key, value) pair +recipeMap.forEach( (value, key, map) => { + alert(`${key}: ${value}`); // cucumber: 500 etc +}); +``` + +## Object.entries: Map from Object + +When a `Map` is created, we can pass an array (or another iterable) with key/value pairs for initialization, like this: + +```js run +// array of [key, value] pairs +let map = new Map([ + ['1', 'str1'], + [1, 'num1'], + [true, 'bool1'] +]); + +alert( map.get('1') ); // str1 +``` + +If we have a plain object, and we'd like to create a `Map` from it, then we can use built-in method [Object.entries(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries) that returns an array of key/value pairs for an object exactly in that format. + +So we can create a map from an object like this: + +```js run +let obj = { + name: "John", + age: 30 +}; + +*!* +let map = new Map(Object.entries(obj)); +*/!* + +alert( map.get('name') ); // John +``` + +Here, `Object.entries` returns the array of key/value pairs: `[ ["name","John"], ["age", 30] ]`. That's what `Map` needs. + + +## Object.fromEntries: Object from Map + +We've just seen how to create `Map` from a plain object with `Object.entries(obj)`. + +There's `Object.fromEntries` method that does the reverse: given an array of `[key, value]` pairs, it creates an object from them: + +```js run +let prices = Object.fromEntries([ + ['banana', 1], + ['orange', 2], + ['meat', 4] +]); + +// now prices = { banana: 1, orange: 2, meat: 4 } + +alert(prices.orange); // 2 +``` + +We can use `Object.fromEntries` to get a plain object from `Map`. + +E.g. we store the data in a `Map`, but we need to pass it to a 3rd-party code that expects a plain object. + +Here we go: + +```js run +let map = new Map(); +map.set('banana', 1); +map.set('orange', 2); +map.set('meat', 4); + +*!* +let obj = Object.fromEntries(map.entries()); // make a plain object (*) +*/!* + +// done! +// obj = { banana: 1, orange: 2, meat: 4 } + +alert(obj.orange); // 2 +``` + +A call to `map.entries()` returns an iterable of key/value pairs, exactly in the right format for `Object.fromEntries`. + +We could also make line `(*)` shorter: +```js +let obj = Object.fromEntries(map); // omit .entries() +``` + +That's the same, because `Object.fromEntries` expects an iterable object as the argument. Not necessarily an array. And the standard iteration for `map` returns same key/value pairs as `map.entries()`. So we get a plain object with same key/values as the `map`. + +## Set + +A [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) is a special type collection - "set of values" (without keys), where each value may occur only once. + +Its main methods are: + +- [`new Set([iterable])`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) -- creates the set, and if an `iterable` object is provided (usually an array), copies values from it into the set. +- [`set.add(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add) -- adds a value, returns the set itself. +- [`set.delete(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete) -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. +- [`set.has(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) -- returns `true` if the value exists in the set, otherwise `false`. +- [`set.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear) -- removes everything from the set. +- [`set.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/size) -- is the elements count. + +The main feature is that repeated calls of `set.add(value)` with the same value don't do anything. That's the reason why each value appears in a `Set` only once. + +For example, we have visitors coming, and we'd like to remember everyone. But repeated visits should not lead to duplicates. A visitor must be "counted" only once. + +`Set` is just the right thing for that: + +```js run +let set = new Set(); + +let john = { name: "John" }; +let pete = { name: "Pete" }; +let mary = { name: "Mary" }; + +// visits, some users come multiple times +set.add(john); +set.add(pete); +set.add(mary); +set.add(john); +set.add(mary); + +// set keeps only unique values +alert( set.size ); // 3 + +for (let user of set) { + alert(user.name); // John (then Pete and Mary) +} +``` + +The alternative to `Set` could be an array of users, and the code to check for duplicates on every insertion using [arr.find](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). But the performance would be much worse, because this method walks through the whole array checking every element. `Set` is much better optimized internally for uniqueness checks. + +## Iteration over Set + +We can loop over a set either with `for..of` or using `forEach`: + +```js run +let set = new Set(["oranges", "apples", "bananas"]); + +for (let value of set) alert(value); + +// the same with forEach: +set.forEach((value, valueAgain, set) => { + alert(value); +}); +``` + +Note the funny thing. The callback function passed in `forEach` has 3 arguments: a `value`, then *the same value* `valueAgain`, and then the target object. Indeed, the same value appears in the arguments twice. + +That's for compatibility with `Map` where the callback passed `forEach` has three arguments. Looks a bit strange, for sure. But this may help to replace `Map` with `Set` in certain cases with ease, and vice versa. + +The same methods `Map` has for iterators are also supported: + +- [`set.keys()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/keys) -- returns an iterable object for values, +- [`set.values()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values) -- same as `set.keys()`, for compatibility with `Map`, +- [`set.entries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries) -- returns an iterable object for entries `[value, value]`, exists for compatibility with `Map`. + +## Summary + +[`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) -- is a collection of keyed values. + +Methods and properties: + +- [`new Map([iterable])`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map) -- creates the map, with optional `iterable` (e.g. array) of `[key,value]` pairs for initialization. +- [`map.set(key, value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set) -- stores the value by the key, returns the map itself. +- [`map.get(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) -- returns the value by the key, `undefined` if `key` doesn't exist in map. +- [`map.has(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) -- returns `true` if the `key` exists, `false` otherwise. +- [`map.delete(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete) -- removes the element by the key, returns `true` if `key` existed at the moment of the call, otherwise `false`. +- [`map.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear) -- removes everything from the map. +- [`map.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size) -- returns the current element count. + +The differences from a regular `Object`: + +- Any keys, objects can be keys. +- Additional convenient methods, the `size` property. + +[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) -- is a collection of unique values. + +Methods and properties: + +- [`new Set([iterable])`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/Set) -- creates the set, with optional `iterable` (e.g. array) of values for initialization. +- [`set.add(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add) -- adds a value (does nothing if `value` exists), returns the set itself. +- [`set.delete(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete) -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. +- [`set.has(value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) -- returns `true` if the value exists in the set, otherwise `false`. +- [`set.clear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear) -- removes everything from the set. +- [`set.size`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/size) -- is the elements count. + +Iteration over `Map` and `Set` is always in the insertion order, so we can't say that these collections are unordered, but we can't reorder elements or directly get an element by its number. diff --git a/1-js/05-data-types/08-keys-values-entries/article.md b/1-js/05-data-types/08-keys-values-entries/article.md deleted file mode 100644 index 50ba91cef5..0000000000 --- a/1-js/05-data-types/08-keys-values-entries/article.md +++ /dev/null @@ -1,70 +0,0 @@ - -# Object.keys, values, entries - -Let's step away from the individual data structures and talk about the iterations over them. - -In the previous chapter we saw methods `map.keys()`, `map.values()`, `map.entries()`. - -These methods are generic, there is a common agreement to use them for data structures. If we ever create a data structure of our own, we should implement them too. - -They are supported for: - -- `Map` -- `Set` -- `Array` (except `arr.values()`) - -Plain objects also support similar methods, but the syntax is a bit different. - -## Object.keys, values, entries - -For plain objects, the following methods are available: - -- [Object.keys(obj)](mdn:js/Object/keys) -- returns an array of keys. -- [Object.values(obj)](mdn:js/Object/values) -- returns an array of values. -- [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of `[key, value]` pairs. - -...But please note the distinctions (compared to map for example): - -| | Map | Object | -|-------------|------------------|--------------| -| Call syntax | `map.keys()` | `Object.keys(obj)`, but not `obj.keys()` | -| Returns | iterable | "real" Array | - -The first difference is that we have to call `Object.keys(obj)`, and not `obj.keys()`. - -Why so? The main reason is flexibility. Remember, objects are a base of all complex structures in JavaScript. So we may have an object of our own like `order` that implements its own `order.values()` method. And we still can call `Object.values(order)` on it. - -The second difference is that `Object.*` methods return "real" array objects, not just an iterable. That's mainly for historical reasons. - -For instance: - -```js -let user = { - name: "John", - age: 30 -}; -``` - -- `Object.keys(user) = [name, age]` -- `Object.values(user) = ["John", 30]` -- `Object.entries(user) = [ ["name","John"], ["age",30] ]` - -Here's an example of using `Object.values` to loop over property values: - -```js run -let user = { - name: "John", - age: 30 -}; - -// loop over values -for (let value of Object.values(user)) { - alert(value); // John, then 30 -} -``` - -## Object.keys/values/entries ignore symbolic properties - -Just like a `for..in` loop, these methods ignore properties that use `Symbol(...)` as keys. - -Usually that's convenient. But if we want symbolic keys too, then there's a separate method [Object.getOwnPropertySymbols](mdn:js/Object/getOwnPropertySymbols) that returns an array of only symbolic keys. Also, the method [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) returns *all* keys. diff --git a/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md new file mode 100644 index 0000000000..e2147ccfac --- /dev/null +++ b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md @@ -0,0 +1,43 @@ +Let's store read messages in `WeakSet`: + +```js run +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; + +let readMessages = new WeakSet(); + +// two messages have been read +readMessages.add(messages[0]); +readMessages.add(messages[1]); +// readMessages has 2 elements + +// ...let's read the first message again! +readMessages.add(messages[0]); +// readMessages still has 2 unique elements + +// answer: was the message[0] read? +alert("Read message 0: " + readMessages.has(messages[0])); // true + +messages.shift(); +// now readMessages has 1 element (technically memory may be cleaned later) +``` + +The `WeakSet` allows to store a set of messages and easily check for the existence of a message in it. + +It cleans up itself automatically. The tradeoff is that we can't iterate over it, can't get "all read messages" from it directly. But we can do it by iterating over all messages and filtering those that are in the set. + +Another, different solution could be to add a property like `message.isRead=true` to a message after it's read. As messages objects are managed by another code, that's generally discouraged, but we can use a symbolic property to avoid conflicts. + +Like this: +```js +// the symbolic property is only known to our code +let isRead = Symbol("isRead"); +messages[0][isRead] = true; +``` + +Now third-party code probably won't see our extra property. + +Although symbols allow to lower the probability of problems, using `WeakSet` is better from the architectural point of view. diff --git a/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md new file mode 100644 index 0000000000..fd31a891b7 --- /dev/null +++ b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Store "unread" flags + +There's an array of messages: + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; +``` + +Your code can access it, but the messages are managed by someone else's code. New messages are added, old ones are removed regularly by that code, and you don't know the exact moments when it happens. + +Now, which data structure could you use to store information about whether the message "has been read"? The structure must be well-suited to give the answer "was it read?" for the given message object. + +P.S. When a message is removed from `messages`, it should disappear from your structure as well. + +P.P.S. We shouldn't modify message objects, add our properties to them. As they are managed by someone else's code, that may lead to bad consequences. diff --git a/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/solution.md b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/solution.md new file mode 100644 index 0000000000..2af0547c1f --- /dev/null +++ b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/solution.md @@ -0,0 +1,15 @@ + +To store a date, we can use `WeakMap`: + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; + +let readMap = new WeakMap(); + +readMap.set(messages[0], new Date(2017, 1, 1)); +// Date object we'll study later +``` diff --git a/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/task.md b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/task.md new file mode 100644 index 0000000000..8e341c1841 --- /dev/null +++ b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/task.md @@ -0,0 +1,21 @@ +importance: 5 + +--- + +# Store read dates + +There's an array of messages as in the [previous task](info:task/recipients-read). The situation is similar. + +```js +let messages = [ + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} +]; +``` + +The question now is: which data structure you'd suggest to store the information: "when the message was read?". + +In the previous task we only needed to store the "yes/no" fact. Now we need to store the date, and it should only remain in memory until the message is garbage collected. + +P.S. Dates can be stored as objects of built-in `Date` class, that we'll cover later. diff --git a/1-js/05-data-types/08-weakmap-weakset/article.md b/1-js/05-data-types/08-weakmap-weakset/article.md new file mode 100644 index 0000000000..9795017d41 --- /dev/null +++ b/1-js/05-data-types/08-weakmap-weakset/article.md @@ -0,0 +1,295 @@ + +# WeakMap and WeakSet + +As we know from the chapter , JavaScript engine keeps a value in memory while it is "reachable" and can potentially be used. + +For instance: + +```js +let john = { name: "John" }; + +// the object can be accessed, john is the reference to it + +// overwrite the reference +john = null; + +*!* +// the object will be removed from memory +*/!* +``` + +Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory. + +For instance, if we put an object into an array, then while the array is alive, the object will be alive as well, even if there are no other references to it. + +Like this: + +```js +let john = { name: "John" }; + +let array = [ john ]; + +john = null; // overwrite the reference + +*!* +// the object previously referenced by john is stored inside the array +// therefore it won't be garbage-collected +// we can get it as array[0] +*/!* +``` + +Similar to that, if we use an object as the key in a regular `Map`, then while the `Map` exists, that object exists as well. It occupies memory and may not be garbage collected. + +For instance: + +```js +let john = { name: "John" }; + +let map = new Map(); +map.set(john, "..."); + +john = null; // overwrite the reference + +*!* +// john is stored inside the map, +// we can get it by using map.keys() +*/!* +``` + +[`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) is fundamentally different in this aspect. It doesn't prevent garbage-collection of key objects. + +Let's see what it means on examples. + +## WeakMap + +The first difference between [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) is that keys must be objects, not primitive values: + +```js run +let weakMap = new WeakMap(); + +let obj = {}; + +weakMap.set(obj, "ok"); // works fine (object key) + +*!* +// can't use a string as the key +weakMap.set("test", "Whoops"); // Error, because "test" is not an object +*/!* +``` + +Now, if we use an object as the key in it, and there are no other references to that object -- it will be removed from memory (and from the map) automatically. + +```js +let john = { name: "John" }; + +let weakMap = new WeakMap(); +weakMap.set(john, "..."); + +john = null; // overwrite the reference + +// john is removed from memory! +``` + +Compare it with the regular `Map` example above. Now if `john` only exists as the key of `WeakMap` -- it will be automatically deleted from the map (and memory). + +`WeakMap` does not support iteration and methods `keys()`, `values()`, `entries()`, so there's no way to get all keys or values from it. + +`WeakMap` has only the following methods: + +- [`weakMap.set(key, value)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/set) +- [`weakMap.get(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/get) +- [`weakMap.delete(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/delete) +- [`weakMap.has(key)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap/has) + +Why such a limitation? That's for technical reasons. If an object has lost all other references (like `john` in the code above), then it is to be garbage-collected automatically. But technically it's not exactly specified *when the cleanup happens*. + +The JavaScript engine decides that. It may choose to perform the memory cleanup immediately or to wait and do the cleaning later when more deletions happen. So, technically, the current element count of a `WeakMap` is not known. The engine may have cleaned it up or not, or did it partially. For that reason, methods that access all keys/values are not supported. + +Now, where do we need such a data structure? + +## Use case: additional data + +The main area of application for `WeakMap` is an *additional data storage*. + +If we're working with an object that "belongs" to another code, maybe even a third-party library, and would like to store some data associated with it, that should only exist while the object is alive - then `WeakMap` is exactly what's needed. + +We put the data to a `WeakMap`, using the object as the key, and when the object is garbage collected, that data will automatically disappear as well. + +```js +weakMap.set(john, "secret documents"); +// if john dies, secret documents will be destroyed automatically +``` + +Let's look at an example. + +For instance, we have code that keeps a visit count for users. The information is stored in a map: a user object is the key and the visit count is the value. When a user leaves (its object gets garbage collected), we don't want to store their visit count anymore. + +Here's an example of a counting function with `Map`: + +```js +// 📁 visitsCount.js +let visitsCountMap = new Map(); // map: user => visits count + +// increase the visits count +function countUser(user) { + let count = visitsCountMap.get(user) || 0; + visitsCountMap.set(user, count + 1); +} +``` + +And here's another part of the code, maybe another file using it: + +```js +// 📁 main.js +let john = { name: "John" }; + +countUser(john); // count his visits + +// later john leaves us +john = null; +``` + +Now, `john` object should be garbage collected, but remains in memory, as it's a key in `visitsCountMap`. + +We need to clean `visitsCountMap` when we remove users, otherwise it will grow in memory indefinitely. Such cleaning can become a tedious task in complex architectures. + +We can avoid it by switching to `WeakMap` instead: + +```js +// 📁 visitsCount.js +let visitsCountMap = new WeakMap(); // weakmap: user => visits count + +// increase the visits count +function countUser(user) { + let count = visitsCountMap.get(user) || 0; + visitsCountMap.set(user, count + 1); +} +``` + +Now we don't have to clean `visitsCountMap`. After `john` object becomes unreachable, by all means except as a key of `WeakMap`, it gets removed from memory, along with the information by that key from `WeakMap`. + +## Use case: caching + +Another common example is caching. We can store ("cache") results from a function, so that future calls on the same object can reuse it. + +To achieve that, we can use `Map` (not optimal scenario): + +```js run +// 📁 cache.js +let cache = new Map(); + +// calculate and remember the result +function process(obj) { + if (!cache.has(obj)) { + let result = /* calculations of the result for */ obj; + + cache.set(obj, result); + return result; + } + + return cache.get(obj); +} + +*!* +// Now we use process() in another file: +*/!* + +// 📁 main.js +let obj = {/* let's say we have an object */}; + +let result1 = process(obj); // calculated + +// ...later, from another place of the code... +let result2 = process(obj); // remembered result taken from cache + +// ...later, when the object is not needed any more: +obj = null; + +alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!) +``` + +For multiple calls of `process(obj)` with the same object, it only calculates the result the first time, and then just takes it from `cache`. The downside is that we need to clean `cache` when the object is not needed any more. + +If we replace `Map` with `WeakMap`, then this problem disappears. The cached result will be removed from memory automatically after the object gets garbage collected. + +```js run +// 📁 cache.js +*!* +let cache = new WeakMap(); +*/!* + +// calculate and remember the result +function process(obj) { + if (!cache.has(obj)) { + let result = /* calculate the result for */ obj; + + cache.set(obj, result); + return result; + } + + return cache.get(obj); +} + +// 📁 main.js +let obj = {/* some object */}; + +let result1 = process(obj); +let result2 = process(obj); + +// ...later, when the object is not needed any more: +obj = null; + +// Can't get cache.size, as it's a WeakMap, +// but it's 0 or soon be 0 +// When obj gets garbage collected, cached data will be removed as well +``` + +## WeakSet + +[`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) behaves similarly: + +- It is analogous to `Set`, but we may only add objects to `WeakSet` (not primitives). +- An object exists in the set while it is reachable from somewhere else. +- Like `Set`, it supports [`add`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Weakset/add), [`has`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Weakset/has) and [`delete`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Weakset/delete), but not `size`, `keys()` and no iterations. + +Being "weak", it also serves as additional storage. But not for arbitrary data, rather for "yes/no" facts. A membership in `WeakSet` may mean something about the object. + +For instance, we can add users to `WeakSet` to keep track of those who visited our site: + +```js run +let visitedSet = new WeakSet(); + +let john = { name: "John" }; +let pete = { name: "Pete" }; +let mary = { name: "Mary" }; + +visitedSet.add(john); // John visited us +visitedSet.add(pete); // Then Pete +visitedSet.add(john); // John again + +// visitedSet has 2 users now + +// check if John visited? +alert(visitedSet.has(john)); // true + +// check if Mary visited? +alert(visitedSet.has(mary)); // false + +john = null; + +// visitedSet will be cleaned automatically +``` + +The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterations, and the inability to get all current content. That may appear inconvenient, but does not prevent `WeakMap/WeakSet` from doing their main job -- be an "additional" storage of data for objects which are stored/managed at another place. + +## Summary + +[`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) is `Map`-like collection that allows only objects as keys and removes them together with associated value once they become inaccessible by other means. + +[`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) is `Set`-like collection that stores only objects and removes them once they become inaccessible by other means. + +Their main advantages are that they have weak reference to objects, so they can easily be removed by garbage collector. + +That comes at the cost of not having support for `clear`, `size`, `keys`, `values`... + +`WeakMap` and `WeakSet` are used as "secondary" data structures in addition to the "primary" object storage. Once the object is removed from the primary storage, if it is only found as the key of `WeakMap` or in a `WeakSet`, it will be cleaned up automatically. diff --git a/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/task.md b/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/task.md deleted file mode 100644 index b2213323ac..0000000000 --- a/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/task.md +++ /dev/null @@ -1,33 +0,0 @@ -importance: 5 - ---- - -# Destructuring assignment - -We have an object: - -```js -let user = { - name: "John", - years: 30 -}; -``` - -Write the destructuring assignment that reads: - -- `name` property into the variable `name`. -- `years` property into the variable `age`. -- `isAdmin` property into the variable `isAdmin` (false if absent) - -The values after the assignment should be: - -```js -let user = { name: "John", years: 30 }; - -// your code to the left side: -// ... = user - -alert( name ); // John -alert( age ); // 30 -alert( isAdmin ); // false -``` diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js b/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js deleted file mode 100644 index d95cf1b1e5..0000000000 --- a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js +++ /dev/null @@ -1,16 +0,0 @@ -function topSalary(salaries) { - - let max = 0; - let maxName = null; - - for(let [name, salary] of Object.entries(salaries)) { - if (max < salary) { - max = salary; - maxName = name; - } - } - - return maxName; -} - - diff --git a/1-js/05-data-types/09-destructuring-assignment/article.md b/1-js/05-data-types/09-destructuring-assignment/article.md deleted file mode 100644 index 6dfa754f77..0000000000 --- a/1-js/05-data-types/09-destructuring-assignment/article.md +++ /dev/null @@ -1,515 +0,0 @@ -# Destructuring assignment - -The two most used data structures in JavaScript are `Object` and `Array`. - -Objects allow us to pack many pieces of information into a single entity and arrays allow us to store ordered collections. So we can make an object or an array and handle it as a single entity, or maybe pass it to a function call. - -*Destructuring assignment* is a special syntax that allows us to "unpack" arrays or objects into a bunch of variables, as sometimes they are more convenient. Destructuring also works great with complex functions that have a lot of parameters, default values, and soon we'll see how these are handled too. - -## Array destructuring - -An example of how the array is destructured into variables: - -```js -// we have an array with the name and surname -let arr = ["Ilya", "Kantor"] - -*!* -// destructuring assignment -let [firstName, surname] = arr; -*/!* - -alert(firstName); // Ilya -alert(surname); // Kantor -``` - -Now we can work with variables instead of array members. - -It looks great when combined with `split` or other array-returning methods: - -```js -let [firstName, surname] = "Ilya Kantor".split(' '); -``` - -````smart header="\"Destructuring\" does not mean \"destructive\"" -It's called "destructuring assignment", because it "destructurizes" by copying items into variables. But the array itself is not modified. - -It's just a shorter way to write: -```js -// let [firstName, surname] = arr; -let firstName = arr[0]; -let surname = arr[1]; -``` -```` - -````smart header="Ignore first elements" -Unwanted elements of the array can also be thrown away via an extra comma: - -```js run -*!* -// first and second elements are not needed -let [, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; -*/!* - -alert( title ); // Consul -``` - -In the code above, although the first and second elements of the array are skipped, the third one is assigned to `title`, and the rest are also skipped. -```` - -````smart header="Works with any iterable on the right-side" - -...Actually, we can use it with any iterable, not only arrays: - -```js -let [a, b, c] = "abc"; // ["a", "b", "c"] -let [one, two, three] = new Set([1, 2, 3]); -``` - -```` - - -````smart header="Assign to anything at the left-side" - -We can use any "assignables" at the left side. - -For instance, an object property: -```js run -let user = {}; -[user.name, user.surname] = "Ilya Kantor".split(' '); - -alert(user.name); // Ilya -``` - -```` - -````smart header="Looping with .entries()" - -In the previous chapter we saw the [Object.entries(obj)](mdn:js/Object/entries) method. - -We can use it with destructuring to loop over keys-and-values of an object: - -```js run -let user = { - name: "John", - age: 30 -}; - -// loop over keys-and-values -*!* -for (let [key, value] of Object.entries(user)) { -*/!* - alert(`${key}:${value}`); // name:John, then age:30 -} -``` - -...And the same for a map: - -```js run -let user = new Map(); -user.set("name", "John"); -user.set("age", "30"); - -*!* -for (let [key, value] of user.entries()) { -*/!* - alert(`${key}:${value}`); // name:John, then age:30 -} -``` -```` -### The rest '...' - -If we want not just to get first values, but also to gather all that follows -- we can add one more parameter that gets "the rest" using three dots `"..."`: - -```js run -let [name1, name2, *!*...rest*/!*] = ["Julius", "Caesar", *!*"Consul", "of the Roman Republic"*/!*]; - -alert(name1); // Julius -alert(name2); // Caesar - -*!* -alert(rest[0]); // Consul -alert(rest[1]); // of the Roman Republic -alert(rest.length); // 2 -*/!* -``` - -The value of `rest` is the array of the remaining array elements. We can use any other variable name in place of `rest`, just make sure it has three dots before it and goes last in the destructuring assignment. - -### Default values - -If there are fewer values in the array than variables in the assignment, there will be no error. Absent values are considered undefined: - -```js run -*!* -let [firstName, surname] = []; -*/!* - -alert(firstName); // undefined -``` - -If we want a "default" value to replace the missing one, we can provide it using `=`: - -```js run -*!* -// default values -let [name = "Guest", surname = "Anonymous"] = ["Julius"]; -*/!* - -alert(name); // Julius (from array) -alert(surname); // Anonymous (default used) -``` - -Default values can be more complex expressions or even function calls. They are evaluated only if the value is not provided. - -For instance, here we use the `prompt` function for two defaults. But it will run only for the missing one: - -```js run -// runs only prompt for surname -let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"]; - -alert(name); // Julius (from array) -alert(surname); // whatever prompt gets -``` - - - -## Object destructuring - -The destructuring assignment also works with objects. - -The basic syntax is: - -```js -let {var1, var2} = {var1:…, var2…} -``` - -We have an existing object at the right side, that we want to split into variables. The left side contains a "pattern" for corresponding properties. In the simple case, that's a list of variable names in `{...}`. - -For instance: - -```js run -let options = { - title: "Menu", - width: 100, - height: 200 -}; - -*!* -let {title, width, height} = options; -*/!* - -alert(title); // Menu -alert(width); // 100 -alert(height); // 200 -``` - -Properties `options.title`, `options.width` and `options.height` are assigned to the corresponding variables. The order does not matter. This works too: - -```js -// changed the order of properties in let {...} -let {height, width, title} = { title: "Menu", height: 200, width: 100 } -``` - -The pattern on the left side may be more complex and specify the mapping between properties and variables. - -If we want to assign a property to a variable with another name, for instance, `options.width` to go into the variable named `w`, then we can set it using a colon: - -```js run -let options = { - title: "Menu", - width: 100, - height: 200 -}; - -*!* -// { sourceProperty: targetVariable } -let {width: w, height: h, title} = options; -*/!* - -// width -> w -// height -> h -// title -> title - -alert(title); // Menu -alert(w); // 100 -alert(h); // 200 -``` - -The colon shows "what : goes where". In the example above the property `width` goes to `w`, property `height` goes to `h`, and `title` is assigned to the same name. - -For potentially missing properties we can set default values using `"="`, like this: - -```js run -let options = { - title: "Menu" -}; - -*!* -let {width = 100, height = 200, title} = options; -*/!* - -alert(title); // Menu -alert(width); // 100 -alert(height); // 200 -``` - -Just like with arrays or function parameters, default values can be any expressions or even function calls. They will be evaluated if the value is not provided. - -The code below asks for width, but not the title. - -```js run -let options = { - title: "Menu" -}; - -*!* -let {width = prompt("width?"), title = prompt("title?")} = options; -*/!* - -alert(title); // Menu -alert(width); // (whatever you the result of prompt is) -``` - -We also can combine both the colon and equality: - -```js run -let options = { - title: "Menu" -}; - -*!* -let {width: w = 100, height: h = 200, title} = options; -*/!* - -alert(title); // Menu -alert(w); // 100 -alert(h); // 200 -``` - -### The rest operator - -What if the object has more properties than we have variables? Can we take some and then assign the "rest" somewhere? - -The specification for using the rest operator (three dots) here is almost in the standard, but most browsers do not support it yet. - -It looks like this: - -```js run -let options = { - title: "Menu", - height: 200, - width: 100 -}; - -*!* -let {title, ...rest} = options; -*/!* - -// now title="Menu", rest={height: 200, width: 100} -alert(rest.height); // 200 -alert(rest.width); // 100 -``` - - - -````smart header="Gotcha without `let`" -In the examples above variables were declared right before the assignment: `let {…} = {…}`. Of course, we could use existing variables too. But there's a catch. - -This won't work: -```js run -let title, width, height; - -// error in this line -{title, width, height} = {title: "Menu", width: 200, height: 100}; -``` - -The problem is that JavaScript treats `{...}` in the main code flow (not inside another expression) as a code block. Such code blocks can be used to group statements, like this: - -```js run -{ - // a code block - let message = "Hello"; - // ... - alert( message ); -} -``` - -To show JavaScript that it's not a code block, we can wrap the whole assignment in brackets `(...)`: - -```js run -let title, width, height; - -// okay now -*!*(*/!*{title, width, height} = {title: "Menu", width: 200, height: 100}*!*)*/!*; - -alert( title ); // Menu -``` - -```` - -## Nested destructuring - -If an object or an array contain other objects and arrays, we can use more complex left-side patterns to extract deeper portions. - -In the code below `options` has another object in the property `size` and an array in the property `items`. The pattern at the left side of the assignment has the same structure: - -```js run -let options = { - size: { - width: 100, - height: 200 - }, - items: ["Cake", "Donut"], - extra: true // something extra that we will not destruct -}; - -// destructuring assignment on multiple lines for clarity -let { - size: { // put size here - width, - height - }, - items: [item1, item2], // assign items here - title = "Menu" // not present in the object (default value is used) -} = options; - -alert(title); // Menu -alert(width); // 100 -alert(height); // 200 -alert(item1); // Cake -alert(item2); // Donut -``` - -The whole `options` object except `extra` that was not mentioned, is assigned to corresponding variables. - -![](destructuring-complex.png) - -Finally, we have `width`, `height`, `item1`, `item2` and `title` from the default value. - -That often happens with destructuring assignments. We have a complex object with many properties and want to extract only what we need. - -Even here it happens: -```js -// take size as a whole into a variable, ignore the rest -let { size } = options; -``` - -## Smart function parameters - -There are times when a function may have many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, items list and so on. - -Here's a bad way to write such function: - -```js -function showMenu(title = "Untitled", width = 200, height = 100, items = []) { - // ... -} -``` - -In real-life the problem is how to remember the order of arguments. Usually IDEs try to help us, especially if the code is well-documented, but still... Another problem is how to call a function when most parameters are ok by default. - -Like this? - -```js -showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]) -``` - -That's ugly. And becomes unreadable when we deal with more parameters. - -Destructuring comes to the rescue! - -We can pass parameters as an object, and the function immediately destructurizes them into variables: - -```js run -// we pass object to function -let options = { - title: "My menu", - items: ["Item1", "Item2"] -}; - -// ...and it immediately expands it to variables -function showMenu(*!*{title = "Untitled", width = 200, height = 100, items = []}*/!*) { - // title, items – taken from options, - // width, height – defaults used - alert( `${title} ${width} ${height}` ); // My Menu 200 100 - alert( items ); // Item1, Item2 -} - -showMenu(options); -``` - -We can also use more complex destructuring with nested objects and colon mappings: - -```js run -let options = { - title: "My menu", - items: ["Item1", "Item2"] -}; - -*!* -function showMenu({ - title = "Untitled", - width: w = 100, // width goes to w - height: h = 200, // height goes to h - items: [item1, item2] // items first element goes to item1, second to item2 -}) { -*/!* - alert( `${title} ${w} ${h}` ); // My Menu 100 200 - alert( item1 ); // Item1 - alert( item2 ); // Item2 -} - -showMenu(options); -``` - -The syntax is the same as for a destructuring assignment: -```js -function({ - incomingProperty: parameterName = defaultValue - ... -}) -``` - -Please note that such destructuring assumes that `showMenu()` does have an argument. If we want all values by default, then we should specify an empty object: - -```js -showMenu({}); - - -showMenu(); // this would give an error -``` - -We can fix this by making `{}` the default value for the whole destructuring thing: - - -```js run -// simplified parameters a bit for clarity -function showMenu(*!*{ title = "Menu", width = 100, height = 200 } = {}*/!*) { - alert( `${title} ${width} ${height}` ); -} - -showMenu(); // Menu 100 200 -``` - -In the code above, the whole arguments object is `{}` by default, so there's always something to destructurize. - -## Summary - -- Destructuring assignment allows for instantly mapping an object or array onto many variables. -- The object syntax: - ```js - let {prop : varName = default, ...} = object - ``` - - This means that property `prop` should go into the variable `varName` and, if no such property exists, then `default` value should be used. - -- The array syntax: - - ```js - let [item1 = default, item2, ...rest] = array - ``` - - The first item goes to `item1`, the second goes into `item2`, all the rest makes the array `rest`. - -- For more complex cases, the left side must have the same structure as the right one. diff --git a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png deleted file mode 100644 index 50c4ffc93a..0000000000 Binary files a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png and /dev/null differ diff --git a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png deleted file mode 100644 index bb908281df..0000000000 Binary files a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png and /dev/null differ diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/solution.js diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/test.js diff --git a/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/solution.md b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/solution.md new file mode 100644 index 0000000000..27a7b418ab --- /dev/null +++ b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/solution.md @@ -0,0 +1,29 @@ +```js run demo +function sumSalaries(salaries) { + + let sum = 0; + for (let salary of Object.values(salaries)) { + sum += salary; + } + + return sum; // 650 +} + +let salaries = { + "John": 100, + "Pete": 300, + "Mary": 250 +}; + +alert( sumSalaries(salaries) ); // 650 +``` +Or, optionally, we could also get the sum using `Object.values` and `reduce`: + +```js +// reduce loops over array of salaries, +// adding them up +// and returns the result +function sumSalaries(salaries) { + return Object.values(salaries).reduce((a, b) => a + b, 0) // 650 +} +``` diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/task.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/task.md diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js b/1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/solution.js diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js b/1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/test.js diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/solution.md b/1-js/05-data-types/09-keys-values-entries/02-count-properties/solution.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/solution.md rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/solution.md diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md b/1-js/05-data-types/09-keys-values-entries/02-count-properties/task.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/task.md 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 new file mode 100644 index 0000000000..bef678f53a --- /dev/null +++ b/1-js/05-data-types/09-keys-values-entries/article.md @@ -0,0 +1,103 @@ + +# Object.keys, values, entries + +Let's step away from the individual data structures and talk about the iterations over them. + +In the previous chapter we saw methods `map.keys()`, `map.values()`, `map.entries()`. + +These methods are generic, there is a common agreement to use them for data structures. If we ever create a data structure of our own, we should implement them too. + +They are supported for: + +- `Map` +- `Set` +- `Array` + +Plain objects also support similar methods, but the syntax is a bit different. + +## Object.keys, values, entries + +For plain objects, the following methods are available: + +- [Object.keys(obj)](mdn:js/Object/keys) -- returns an array of keys. +- [Object.values(obj)](mdn:js/Object/values) -- returns an array of values. +- [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of `[key, value]` pairs. + +Please note the distinctions (compared to map for example): + +| | Map | Object | +|-------------|------------------|--------------| +| Call syntax | `map.keys()` | `Object.keys(obj)`, but not `obj.keys()` | +| Returns | iterable | "real" Array | + +The first difference is that we have to call `Object.keys(obj)`, and not `obj.keys()`. + +Why so? The main reason is flexibility. Remember, objects are a base of all complex structures in JavaScript. So we may have an object of our own like `data` that implements its own `data.values()` method. And we still can call `Object.values(data)` on it. + +The second difference is that `Object.*` methods return "real" array objects, not just an iterable. That's mainly for historical reasons. + +For instance: + +```js +let user = { + name: "John", + age: 30 +}; +``` + +- `Object.keys(user) = ["name", "age"]` +- `Object.values(user) = ["John", 30]` +- `Object.entries(user) = [ ["name","John"], ["age",30] ]` + +Here's an example of using `Object.values` to loop over property values: + +```js run +let user = { + name: "John", + age: 30 +}; + +// loop over values +for (let value of Object.values(user)) { + alert(value); // John, then 30 +} +``` + +```warn header="Object.keys/values/entries ignore symbolic properties" +Just like a `for..in` loop, these methods ignore properties that use `Symbol(...)` as keys. + +Usually that's convenient. But if we want symbolic keys too, then there's a separate method [Object.getOwnPropertySymbols](mdn:js/Object/getOwnPropertySymbols) that returns an array of only symbolic keys. Also, there exist a method [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns *all* keys. +``` + + +## Transforming objects + +Objects lack many methods that exist for arrays, e.g. `map`, `filter` and others. + +If we'd like to apply them, then we can use `Object.entries` followed by `Object.fromEntries`: + +1. Use `Object.entries(obj)` to get an array of key/value pairs from `obj`. +2. Use array methods on that array, e.g. `map`, to transform these key/value pairs. +3. Use `Object.fromEntries(array)` on the resulting array to turn it back into an object. + +For example, we have an object with prices, and would like to double them: + +```js run +let prices = { + banana: 1, + orange: 2, + meat: 4, +}; + +*!* +let doublePrices = Object.fromEntries( + // convert prices to array, map each key/value pair into another pair + // and then fromEntries gives back the object + Object.entries(prices).map(entry => [entry[0], entry[1] * 2]) +); +*/!* + +alert(doublePrices.meat); // 8 +``` + +It may look difficult at first sight, but becomes easy to understand after you use it once or twice. We can make powerful chains of transforms this way. diff --git a/1-js/05-data-types/10-date/1-new-date/solution.md b/1-js/05-data-types/10-date/1-new-date/solution.md deleted file mode 100644 index eb271a91a8..0000000000 --- a/1-js/05-data-types/10-date/1-new-date/solution.md +++ /dev/null @@ -1,8 +0,0 @@ -The `new Date` constructor uses the local time zone by default. So the only important thing to remember is that months start from zero. - -So February has number 1. - -```js run -let d = new Date(2012, 1, 20, 3, 12); -alert( d ); -``` diff --git a/1-js/05-data-types/10-date/2-get-week-day/solution.md b/1-js/05-data-types/10-date/2-get-week-day/solution.md deleted file mode 100644 index 2acaabf9c7..0000000000 --- a/1-js/05-data-types/10-date/2-get-week-day/solution.md +++ /dev/null @@ -1,14 +0,0 @@ -The method `date.getDay()` returns the number of the weekday, starting from sunday. - -Let's make an array of weekdays, so that we can get the proper day name by its number: - -```js run -function getWeekDay(date) { - let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; - - return days[date.getDay()]; -} - -let date = new Date(2014, 0, 3); // 3 Jan 2014 -alert( getWeekDay(date) ); // FR -``` diff --git a/1-js/05-data-types/10-date/3-weekday/solution.md b/1-js/05-data-types/10-date/3-weekday/solution.md deleted file mode 100644 index bfe3f4cac6..0000000000 --- a/1-js/05-data-types/10-date/3-weekday/solution.md +++ /dev/null @@ -1,14 +0,0 @@ -```js run -function getLocalDay(date) { - - let day = date.getDay(); - - if (day == 0) { // 0 becomes 7 - day = 7; - } - - return day; -} - -alert( getLocalDay(new Date(2012, 0, 3)) ); // 2 -``` diff --git a/1-js/05-data-types/10-date/4-get-date-ago/solution.md b/1-js/05-data-types/10-date/4-get-date-ago/solution.md deleted file mode 100644 index 7abc7ab9cd..0000000000 --- a/1-js/05-data-types/10-date/4-get-date-ago/solution.md +++ /dev/null @@ -1,27 +0,0 @@ -The idea is simple: to substract given number of days from `date`: - -```js -function getDateAgo(date, days) { - date.setDate(date.getDate() - days); - return date.getDate(); -} -``` - -...But the function should not change `date`. That's an important thing, because the outer code which gives us the date does not expect it to change. - -To implement it let's clone the date, like this: - -```js run -function getDateAgo(date, days) { - let dateCopy = new Date(date); - - dateCopy.setDate(date.getDate() - days); - return dateCopy.getDate(); -} - -let date = new Date(2015, 0, 2); - -alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015) -alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014) -alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014) -``` diff --git a/1-js/05-data-types/10-date/4-get-date-ago/task.md b/1-js/05-data-types/10-date/4-get-date-ago/task.md deleted file mode 100644 index 40dcd926dc..0000000000 --- a/1-js/05-data-types/10-date/4-get-date-ago/task.md +++ /dev/null @@ -1,21 +0,0 @@ -importance: 4 - ---- - -# Which day of month was many days ago? - -Create a function `getDateAgo(date, days)` to return the day of month `days` ago from the `date`. - -For instance, if today is 20th, then `getDateAgo(new Date(), 1)` should be 19th and `getDateAgo(new Date(), 2)` should be 18th. - -Should also work over months/years reliably: - -```js -let date = new Date(2015, 0, 2); - -alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015) -alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014) -alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014) -``` - -P.S. The function should not modify the given `date`. diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/solution.md b/1-js/05-data-types/10-date/5-last-day-of-month/solution.md deleted file mode 100644 index 65f61c5bae..0000000000 --- a/1-js/05-data-types/10-date/5-last-day-of-month/solution.md +++ /dev/null @@ -1,13 +0,0 @@ -Let's create a date using the next month, but pass zero as the day: -```js run -function getLastDayOfMonth(year, month) { - let date = new Date(year, month + 1, 0); - return date.getDate(); -} - -alert( getLastDayOfMonth(2012, 0) ); // 31 -alert( getLastDayOfMonth(2012, 1) ); // 29 -alert( getLastDayOfMonth(2013, 1) ); // 28 -``` - -Normally, dates start from 1, but technically we can pass any number, the date will autoadjust itself. So when we pass 0, then it means "one day before 1st day of the month", in other words: "the last day of the previous month". diff --git a/1-js/05-data-types/10-date/6-get-seconds-today/solution.md b/1-js/05-data-types/10-date/6-get-seconds-today/solution.md deleted file mode 100644 index 91903d9058..0000000000 --- a/1-js/05-data-types/10-date/6-get-seconds-today/solution.md +++ /dev/null @@ -1,26 +0,0 @@ -To get the number of seconds, we can generate a date using the current day and time 00:00:00, then substract it from "now". - -The difference is the number of milliseconds from the beginning of the day, that we should divide by 1000 to get seconds: - -```js run -function getSecondsToday() { - let now = new Date(); - - // create an object using the current day/month/year - let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - - let diff = now - today; // ms difference - return Math.round(diff / 1000); // make seconds -} - -alert( getSecondsToday() ); -``` - -An alternative solution would be to get hours/minutes/seconds and convert them to seconds: - -```js run -function getSecondsToday() { - let d = new Date(); - return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); -}; -``` diff --git a/1-js/05-data-types/10-date/6-get-seconds-today/task.md b/1-js/05-data-types/10-date/6-get-seconds-today/task.md deleted file mode 100644 index 3fbe132868..0000000000 --- a/1-js/05-data-types/10-date/6-get-seconds-today/task.md +++ /dev/null @@ -1,15 +0,0 @@ -importance: 5 - ---- - -# How many seconds has passed today? - -Write a function `getSecondsToday()` that returns the number of seconds from the beginning of today. - -For instance, if now `10:00 am`, and there was no daylight savings shift, then: - -```js -getSecondsToday() == 36000 // (3600 * 10) -``` - -The function should work in any day. That is, it should not have a hard-coded value of "today". diff --git a/1-js/05-data-types/10-date/8-format-date-relative/solution.md b/1-js/05-data-types/10-date/8-format-date-relative/solution.md deleted file mode 100644 index 24fec997d5..0000000000 --- a/1-js/05-data-types/10-date/8-format-date-relative/solution.md +++ /dev/null @@ -1,76 +0,0 @@ -To get the time from `date` till now -- let's substract the dates. - -```js run -function formatDate(date) { - let diff = new Date() - date; // the difference in milliseconds - - if (diff < 1000) { // less than 1 second - return 'right now'; - } - - let sec = Math.floor(diff / 1000); // convert diff to seconds - - if (sec < 60) { - return sec + ' sec. ago'; - } - - let min = Math.floor(diff / 60000); // convert diff to minutes - if (min < 60) { - return min + ' min. ago'; - } - - // format the date - // add leading zeroes to single-digit day/month/hours/minutes - let d = date; - d = [ - '0' + d.getDate(), - '0' + (d.getMonth() + 1), - '' + d.getFullYear(), - '0' + d.getHours(), - '0' + d.getMinutes() - ].map(component => component.slice(-2)); // take last 2 digits of every component - - // join the components into date - return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); -} - -alert( formatDate(new Date(new Date - 1)) ); // "right now" - -alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" - -alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" - -// yesterday's date like 31.12.2016, 20:00 -alert( formatDate(new Date(new Date - 86400 * 1000)) ); -``` - -Alternative solution: - -```js run -function formatDate(date) { - let dayOfMonth = date.getDate(); - let month = date.getMonth() + 1; - let year = date.getFullYear(); - let hour = date.getHours(); - let minutes = date.getMinutes(); - let diffMs = new Date() - date; - let diffSec = Math.round(diffMs / 1000); - let diffMin = diffSec / 60; - let diffHour = diffMin / 60; - - // formatting - year = year.toString().slice(-2); - month = month < 10 ? '0' + month : month; - dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth; - - if (diffSec < 1) { - return 'right now'; - } else if (diffMin < 1) { - return `${diffSec} sec. ago` - } else if (diffHour < 1) { - return `${diffMin} min. ago` - } else { - return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}` - } -} -``` diff --git a/1-js/05-data-types/10-date/8-format-date-relative/task.md b/1-js/05-data-types/10-date/8-format-date-relative/task.md deleted file mode 100644 index 7b341ca2e6..0000000000 --- a/1-js/05-data-types/10-date/8-format-date-relative/task.md +++ /dev/null @@ -1,25 +0,0 @@ -importance: 4 - ---- - -# Format the relative date - -Write a function `formatDate(date)` that should format `date` as follows: - -- If since `date` passed less than 1 second, then `"right now"`. -- Otherwise, if since `date` passed less than 1 minute, then `"n sec. ago"`. -- Otherwise, if less than an hour, then `"m min. ago"`. -- Otherwise, the full date in the format `"DD.MM.YY HH:mm"`. That is: `"day.month.year hours:minutes"`, all in 2-digit format, e.g. `31.12.16 10:00`. - -For instance: - -```js -alert( formatDate(new Date(new Date - 1)) ); // "right now" - -alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" - -alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" - -// yesterday's date like 31.12.2016, 20:00 -alert( formatDate(new Date(new Date - 86400 * 1000)) ); -``` diff --git a/1-js/05-data-types/10-date/article.md b/1-js/05-data-types/10-date/article.md deleted file mode 100644 index 00390812f3..0000000000 --- a/1-js/05-data-types/10-date/article.md +++ /dev/null @@ -1,421 +0,0 @@ -# Date and time - -Let's meet a new built-in object: [Date](mdn:js/Date). It stores the date, time and provides methods for date/time management. - -For instance, we can use it to store creation/modification times, or to measure time, or just to print out the current date. - -## Creation - -To create a new `Date` object call `new Date()` with one of the following arguments: - -`new Date()` -: Without arguments -- create a `Date` object for the current date and time: - - ```js run - let now = new Date(); - alert( now ); // shows current date/time - ``` - -`new Date(milliseconds)` -: Create a `Date` object with the time equal to number of milliseconds (1/1000 of a second) passed after the Jan 1st of 1970 UTC+0. - - ```js run - // 0 means 01.01.1970 UTC+0 - let Jan01_1970 = new Date(0); - alert( Jan01_1970 ); - - // now add 24 hours, get 02.01.1970 UTC+0 - let Jan02_1970 = new Date(24 * 3600 * 1000); - alert( Jan02_1970 ); - ``` - - The number of milliseconds that has passed since the beginning of 1970 is called a *timestamp*. - - It's a lightweight numeric representation of a date. We can always create a date from a timestamp using `new Date(timestamp)` and convert the existing `Date` object to a timestamp using the `date.getTime()` method (see below). - -`new Date(datestring)` -: If there is a single argument, and it's a string, then it is parsed with the `Date.parse` algorithm (see below). - - - ```js run - let date = new Date("2017-01-26"); - alert(date); // Thu Jan 26 2017 ... - ``` - -`new Date(year, month, date, hours, minutes, seconds, ms)` -: Create the date with the given components in the local time zone. Only two first arguments are obligatory. - - Note: - - - The `year` must have 4 digits: `2013` is okay, `98` is not. - - The `month` count starts with `0` (Jan), up to `11` (Dec). - - The `date` parameter is actually the day of month, if absent then `1` is assumed. - - If `hours/minutes/seconds/ms` is absent, they are assumed to be equal `0`. - - For instance: - - ```js - new Date(2011, 0, 1, 0, 0, 0, 0); // // 1 Jan 2011, 00:00:00 - new Date(2011, 0, 1); // the same, hours etc are 0 by default - ``` - - The minimal precision is 1 ms (1/1000 sec): - - ```js run - let date = new Date(2011, 0, 1, 2, 3, 4, 567); - alert( date ); // 1.01.2011, 02:03:04.567 - ``` - -## Access date components - -There are many methods to access the year, month and so on from the `Date` object. But they can be easily remembered when categorized. - -[getFullYear()](mdn:js/Date/getFullYear) -: Get the year (4 digits) - -[getMonth()](mdn:js/Date/getMonth) -: Get the month, **from 0 to 11**. - -[getDate()](mdn:js/Date/getDate) -: Get the day of month, from 1 to 31, the name of the method does look a little bit strange. - -[getHours()](mdn:js/Date/getHours), [getMinutes()](mdn:js/Date/getMinutes), [getSeconds()](mdn:js/Date/getSeconds), [getMilliseconds()](mdn:js/Date/getMilliseconds) -: Get the corresponding time components. - -```warn header="Not `getYear()`, but `getFullYear()`" -Many JavaScript engines implement a non-standard method `getYear()`. This method is deprecated. It returns 2-digit year sometimes. Please never use it. There is `getFullYear()` for the year. -``` - -Additionally, we can get a day of week: - -[getDay()](mdn:js/Date/getDay) -: Get the day of week, from `0` (Sunday) to `6` (Saturday). The first day is always Sunday, in some countries that's not so, but can't be changed. - -**All the methods above return the components relative to the local time zone.** - -There are also their UTC-counterparts, that return day, month, year and so on for the time zone UTC+0: [getUTCFullYear()](mdn:js/Date/getUTCFullYear), [getUTCMonth()](mdn:js/Date/getUTCMonth), [getUTCDay()](mdn:js/Date/getUTCDay). Just insert the `"UTC"` right after `"get"`. - -If your local time zone is shifted relative to UTC, then the code below shows different hours: - -```js run -// current date -let date = new Date(); - -// the hour in your current time zone -alert( date.getHours() ); - -// the hour in UTC+0 time zone (London time without daylight savings) -alert( date.getUTCHours() ); -``` - -Besides the given methods, there are two special ones, that do not have a UTC-variant: - -[getTime()](mdn:js/Date/getTime) -: Returns the timestamp for the date -- a number of milliseconds passed from the January 1st of 1970 UTC+0. - -[getTimezoneOffset()](mdn:js/Date/getTimezoneOffset) -: Returns the difference between the local time zone and UTC, in minutes: - - ```js run - // if you are in timezone UTC-1, outputs 60 - // if you are in timezone UTC+3, outputs -180 - alert( new Date().getTimezoneOffset() ); - - ``` - -## Setting date components - -The following methods allow to set date/time components: - -- [`setFullYear(year [, month, date])`](mdn:js/Date/setFullYear) -- [`setMonth(month [, date])`](mdn:js/Date/setMonth) -- [`setDate(date)`](mdn:js/Date/setDate) -- [`setHours(hour [, min, sec, ms])`](mdn:js/Date/setHours) -- [`setMinutes(min [, sec, ms])`](mdn:js/Date/setMinutes) -- [`setSeconds(sec [, ms])`](mdn:js/Date/setSeconds) -- [`setMilliseconds(ms)`](mdn:js/Date/setMilliseconds) -- [`setTime(milliseconds)`](mdn:js/Date/setTime) (sets the whole date by milliseconds since 01.01.1970 UTC) - -Every one of them except `setTime()` has a UTC-variant, for instance: `setUTCHours()`. - -As we can see, some methods can set multiple components at once, for example `setHours`. The components that are not mentioned are not modified. - -For instance: - -```js run -let today = new Date(); - -today.setHours(0); -alert(today); // still today, but the hour is changed to 0 - -today.setHours(0, 0, 0, 0); -alert(today); // still today, now 00:00:00 sharp. -``` - -## Autocorrection - -The *autocorrection* is a very handy feature of `Date` objects. We can set out-of-range values, and it will auto-adjust itself. - -For instance: - -```js run -let date = new Date(2013, 0, *!*32*/!*); // 32 Jan 2013 ?!? -alert(date); // ...is 1st Feb 2013! -``` - -Out-of-range date components are distributed automatically. - -Let's say we need to increase the date "28 Feb 2016" by 2 days. It may be "2 Mar" or "1 Mar" in case of a leap-year. We don't need to think about it. Just add 2 days. The `Date` object will do the rest: - -```js run -let date = new Date(2016, 1, 28); -*!* -date.setDate(date.getDate() + 2); -*/!* - -alert( date ); // 1 Mar 2016 -``` - -That feature is often used to get the date after the given period of time. For instance, let's get the date for "70 seconds after now": - -```js run -let date = new Date(); -date.setSeconds(date.getSeconds() + 70); - -alert( date ); // shows the correct date -``` - -We can also set zero or even negative values. For example: - -```js run -let date = new Date(2016, 0, 2); // 2 Jan 2016 - -date.setDate(1); // set day 1 of month -alert( date ); - -date.setDate(0); // min day is 1, so the last day of the previous month is assumed -alert( date ); // 31 Dec 2015 -``` - -## Date to number, date diff - -When a `Date` object is converted to number, it becomes the timestamp same as `date.getTime()`: - -```js run -let date = new Date(); -alert(+date); // the number of milliseconds, same as date.getTime() -``` - -The important side effect: dates can be subtracted, the result is their difference in ms. - -That can be used for time measurements: - -```js run -let start = new Date(); // start counting - -// do the job -for (let i = 0; i < 100000; i++) { - let doSomething = i * i * i; -} - -let end = new Date(); // done - -alert( `The loop took ${end - start} ms` ); -``` - -## Date.now() - -If we only want to measure the difference, we don't need the `Date` object. - -There's a special method `Date.now()` that returns the current timestamp. - -It is semantically equivalent to `new Date().getTime()`, but it doesn't create an intermediate `Date` object. So it's faster and doesn't put pressure on garbage collection. - -It is used mostly for convenience or when performance matters, like in games in JavaScript or other specialized applications. - -So this is probably better: - -```js run -*!* -let start = Date.now(); // milliseconds count from 1 Jan 1970 -*/!* - -// do the job -for (let i = 0; i < 100000; i++) { - let doSomething = i * i * i; -} - -*!* -let end = Date.now(); // done -*/!* - -alert( `The loop took ${end - start} ms` ); // subtract numbers, not dates -``` - -## Benchmarking - -If we want a reliable benchmark of CPU-hungry function, we should be careful. - -For instance, let's measure two functions that calculate the difference between two dates: which one is faster? - -```js -// we have date1 and date2, which function faster returns their difference in ms? -function diffSubtract(date1, date2) { - return date2 - date1; -} - -// or -function diffGetTime(date1, date2) { - return date2.getTime() - date1.getTime(); -} -``` - -These two do exactly the same thing, but one of them uses an explicit `date.getTime()` to get the date in ms, and the other one relies on a date-to-number transform. Their result is always the same. - -So, which one is faster? - -The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it around 100000 times. - -Let's measure: - -```js run -function diffSubtract(date1, date2) { - return date2 - date1; -} - -function diffGetTime(date1, date2) { - return date2.getTime() - date1.getTime(); -} - -function bench(f) { - let date1 = new Date(0); - let date2 = new Date(); - - let start = Date.now(); - for (let i = 0; i < 100000; i++) f(date1, date2); - return Date.now() - start; -} - -alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' ); -alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' ); -``` - -Wow! Using `getTime()` is so much faster! That's because there's no type conversion, it is much easier for engines to optimize. - -Okay, we have something. But that's not a good benchmark yet. - -Imagine that at the time of running `bench(diffSubtract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` the work has finished. - -A pretty real scenario for a modern multi-process OS. - -As a result, the first benchmark will have less CPU resources than the second. That may lead to wrong results. - -**For more reliable benchmarking, the whole pack of benchmarks should be rerun multiple times.** - -Here's the code example: - -```js run -function diffSubtract(date1, date2) { - return date2 - date1; -} - -function diffGetTime(date1, date2) { - return date2.getTime() - date1.getTime(); -} - -function bench(f) { - let date1 = new Date(0); - let date2 = new Date(); - - let start = Date.now(); - for (let i = 0; i < 100000; i++) f(date1, date2); - return Date.now() - start; -} - -let time1 = 0; -let time2 = 0; - -*!* -// run bench(upperSlice) and bench(upperLoop) each 10 times alternating -for (let i = 0; i < 10; i++) { - time1 += bench(diffSubtract); - time2 += bench(diffGetTime); -} -*/!* - -alert( 'Total time for diffSubtract: ' + time1 ); -alert( 'Total time for diffGetTime: ' + time2 ); -``` - -Modern JavaScript engines start applying advanced optimizations only to "hot code" that executes many times (no need to optimize rarely executed things). So, in the example above, first executions are not well-optimized. We may want to add a heat-up run: - -```js -// added for "heating up" prior to the main loop -bench(diffSubtract); -bench(diffGetTime); - -// now benchmark -for (let i = 0; i < 10; i++) { - time1 += bench(diffSubtract); - time2 += bench(diffGetTime); -} -``` - -```warn header="Be careful doing microbenchmarking" -Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all. - -The great pack of articles about V8 can be found at . -``` - -## Date.parse from a string - -The method [Date.parse(str)](mdn:js/Date/parse) can read a date from a string. - -The string format should be: `YYYY-MM-DDTHH:mm:ss.sssZ`, where: - -- `YYYY-MM-DD` -- is the date: year-month-day. -- The character `"T"` is used as the delimiter. -- `HH:mm:ss.sss` -- is the time: hours, minutes, seconds and milliseconds. -- The optional `'Z'` part denotes the time zone in the format `+-hh:mm`. A single letter `Z` that would mean UTC+0. - -Shorter variants are also possible, like `YYYY-MM-DD` or `YYYY-MM` or even `YYYY`. - -The call to `Date.parse(str)` parses the string in the given format and returns the timestamp (number of milliseconds from 1 Jan 1970 UTC+0). If the format is invalid, returns `NaN`. - -For instance: - -```js run -let ms = Date.parse('2012-01-26T13:51:50.417-07:00'); - -alert(ms); // 1327611110417 (timestamp) -``` - -We can instantly create a `new Date` object from the timestamp: - -```js run -let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') ); - -alert(date); -``` - -## Summary - -- Date and time in JavaScript are represented with the [Date](mdn:js/Date) object. We can't create "only date" or "only time": `Date` objects always carry both. -- Months are counted from zero (yes, January is a zero month). -- Days of week in `getDay()` are also counted from zero (that's Sunday). -- `Date` auto-corrects itself when out-of-range components are set. Good for adding/subtracting days/months/hours. -- Dates can be subtracted, giving their difference in milliseconds. That's because a `Date` becomes the timestamp when converted to a number. -- Use `Date.now()` to get the current timestamp fast. - -Note that unlike many other systems, timestamps in JavaScript are in milliseconds, not in seconds. - -Also, sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point): - -```js run -alert(`Loading started ${performance.now()}ms ago`); -// Something like: "Loading started 34731.26000000001ms ago" -// .26 is microseconds (260 microseconds) -// more than 3 digits after the decimal point are precision errors, but only the first 3 are correct -``` - -Node.JS has `microtime` module and other ways. Technically, any device and environment allows to get more precision, it's just not in `Date`. diff --git a/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/solution.md b/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/solution.md similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/1-destruct-user/solution.md rename to 1-js/05-data-types/10-destructuring-assignment/1-destruct-user/solution.md diff --git a/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/task.md b/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/task.md new file mode 100644 index 0000000000..b68db5c593 --- /dev/null +++ b/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/task.md @@ -0,0 +1,33 @@ +importance: 5 + +--- + +# Destructuring assignment + +We have an object: + +```js +let user = { + name: "John", + years: 30 +}; +``` + +Write the destructuring assignment that reads: + +- `name` property into the variable `name`. +- `years` property into the variable `age`. +- `isAdmin` property into the variable `isAdmin` (false, if no such property) + +Here's an example of the values after your assignment: + +```js +let user = { name: "John", years: 30 }; + +// your code to the left side: +// ... = user + +alert( name ); // John +alert( age ); // 30 +alert( isAdmin ); // false +``` diff --git a/1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/solution.js b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/solution.js new file mode 100644 index 0000000000..6538af42b9 --- /dev/null +++ b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/solution.js @@ -0,0 +1,14 @@ +function topSalary(salaries) { + + let maxSalary = 0; + let maxName = null; + + for(const [name, salary] of Object.entries(salaries)) { + if (maxSalary < salary) { + maxSalary = salary; + maxName = name; + } + } + + return maxName; +} \ No newline at end of file diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/test.js diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/solution.md b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/solution.md similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/solution.md rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/solution.md diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/task.md similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/task.md diff --git a/1-js/05-data-types/10-destructuring-assignment/article.md b/1-js/05-data-types/10-destructuring-assignment/article.md new file mode 100644 index 0000000000..0c52741d14 --- /dev/null +++ b/1-js/05-data-types/10-destructuring-assignment/article.md @@ -0,0 +1,579 @@ +# Destructuring assignment + +The two most used data structures in JavaScript are `Object` and `Array`. + +- Objects allow us to create a single entity that stores data items by key. +- Arrays allow us to gather data items into an ordered list. + +However, when we pass these to a function, we may not need all of it. The function might only require certain elements or properties. + +*Destructuring assignment* is a special syntax that allows us to "unpack" arrays or objects into a bunch of variables, as sometimes that's more convenient. + +Destructuring also works well with complex functions that have a lot of parameters, default values, and so on. Soon we'll see that. + +## Array destructuring + +Here's an example of how an array is destructured into variables: + +```js +// we have an array with a name and surname +let arr = ["John", "Smith"] + +*!* +// destructuring assignment +// sets firstName = arr[0] +// and surname = arr[1] +let [firstName, surname] = arr; +*/!* + +alert(firstName); // John +alert(surname); // Smith +``` + +Now we can work with variables instead of array members. + +It looks great when combined with `split` or other array-returning methods: + +```js run +let [firstName, surname] = "John Smith".split(' '); +alert(firstName); // John +alert(surname); // Smith +``` + +As you can see, the syntax is simple. There are several peculiar details though. Let's see more examples to understand it better. + +````smart header="\"Destructuring\" does not mean \"destructive\"." +It's called "destructuring assignment," because it "destructurizes" by copying items into variables. However, the array itself is not modified. + +It's just a shorter way to write: +```js +// let [firstName, surname] = arr; +let firstName = arr[0]; +let surname = arr[1]; +``` +```` + +````smart header="Ignore elements using commas" +Unwanted elements of the array can also be thrown away via an extra comma: + +```js run +*!* +// second element is not needed +let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; +*/!* + +alert( title ); // Consul +``` + +In the code above, the second element of the array is skipped, the third one is assigned to `title`, and the rest of the array items are also skipped (as there are no variables for them). +```` + +````smart header="Works with any iterable on the right-side" + +...Actually, we can use it with any iterable, not only arrays: + +```js +let [a, b, c] = "abc"; // ["a", "b", "c"] +let [one, two, three] = new Set([1, 2, 3]); +``` +That works, because internally a destructuring assignment works by iterating over the right value. It's a kind of syntax sugar for calling `for..of` over the value to the right of `=` and assigning the values. +```` + + +````smart header="Assign to anything at the left-side" +We can use any "assignables" on the left side. + +For instance, an object property: +```js run +let user = {}; +[user.name, user.surname] = "John Smith".split(' '); + +alert(user.name); // John +alert(user.surname); // Smith +``` + +```` + +````smart header="Looping with .entries()" +In the previous chapter, we saw the [Object.entries(obj)](mdn:js/Object/entries) method. + +We can use it with destructuring to loop over the keys-and-values of an object: + +```js run +let user = { + name: "John", + age: 30 +}; + +// loop over the keys-and-values +*!* +for (let [key, value] of Object.entries(user)) { +*/!* + alert(`${key}:${value}`); // name:John, then age:30 +} +``` + +The similar code for a `Map` is simpler, as it's iterable: + +```js run +let user = new Map(); +user.set("name", "John"); +user.set("age", "30"); + +*!* +// Map iterates as [key, value] pairs, very convenient for destructuring +for (let [key, value] of user) { +*/!* + alert(`${key}:${value}`); // name:John, then age:30 +} +``` +```` + +````smart header="Swap variables trick" +There's a well-known trick for swapping values of two variables using a destructuring assignment: + +```js run +let guest = "Jane"; +let admin = "Pete"; + +// Let's swap the values: make guest=Pete, admin=Jane +*!* +[guest, admin] = [admin, guest]; +*/!* + +alert(`${guest} ${admin}`); // Pete Jane (successfully swapped!) +``` + +Here we create a temporary array of two variables and immediately destructure it in swapped order. + +We can swap more than two variables this way. +```` + +### The rest '...' + +Usually, if the array is longer than the list at the left, the "extra" items are omitted. + +For example, here only two items are taken, and the rest is just ignored: + +```js run +let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; + +alert(name1); // Julius +alert(name2); // Caesar +// Further items aren't assigned anywhere +``` + +If we'd like also to gather all that follows -- we can add one more parameter that gets "the rest" using three dots `"..."`: + +```js run +let [name1, name2, *!*...rest*/!*] = ["Julius", "Caesar", *!*"Consul", "of the Roman Republic"*/!*]; + +*!* +// rest is an array of items, starting from the 3rd one +alert(rest[0]); // Consul +alert(rest[1]); // of the Roman Republic +alert(rest.length); // 2 +*/!* +``` + +The value of `rest` is the array of the remaining array elements. + +We can use any other variable name in place of `rest`, just make sure it has three dots before it and goes last in the destructuring assignment. + +```js run +let [name1, name2, *!*...titles*/!*] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; +// now titles = ["Consul", "of the Roman Republic"] +``` + +### Default values + +If the array is shorter than the list of variables on the left, there will be no errors. Absent values are considered undefined: + +```js run +*!* +let [firstName, surname] = []; +*/!* + +alert(firstName); // undefined +alert(surname); // undefined +``` + +If we want a "default" value to replace the missing one, we can provide it using `=`: + +```js run +*!* +// default values +let [name = "Guest", surname = "Anonymous"] = ["Julius"]; +*/!* + +alert(name); // Julius (from array) +alert(surname); // Anonymous (default used) +``` + +Default values can be more complex expressions or even function calls. They are evaluated only if the value is not provided. + +For instance, here we use the `prompt` function for two defaults: + +```js run +// runs only prompt for surname +let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"]; + +alert(name); // Julius (from array) +alert(surname); // whatever prompt gets +``` + +Please note: the `prompt` will run only for the missing value (`surname`). + +## Object destructuring + +The destructuring assignment also works with objects. + +The basic syntax is: + +```js +let {var1, var2} = {var1:…, var2:…} +``` + +We should have an existing object on the right side, that we want to split into variables. The left side contains an object-like "pattern" for corresponding properties. In the simplest case, that's a list of variable names in `{...}`. + +For instance: + +```js run +let options = { + title: "Menu", + width: 100, + height: 200 +}; + +*!* +let {title, width, height} = options; +*/!* + +alert(title); // Menu +alert(width); // 100 +alert(height); // 200 +``` + +Properties `options.title`, `options.width` and `options.height` are assigned to the corresponding variables. + +The order does not matter. This works too: + +```js +// changed the order in let {...} +let {height, width, title} = { title: "Menu", height: 200, width: 100 } +``` + +The pattern on the left side may be more complex and specify the mapping between properties and variables. + +If we want to assign a property to a variable with another name, for instance, make `options.width` go into the variable named `w`, then we can set the variable name using a colon: + +```js run +let options = { + title: "Menu", + width: 100, + height: 200 +}; + +*!* +// { sourceProperty: targetVariable } +let {width: w, height: h, title} = options; +*/!* + +// width -> w +// height -> h +// title -> title + +alert(title); // Menu +alert(w); // 100 +alert(h); // 200 +``` + +The colon shows "what : goes where". In the example above the property `width` goes to `w`, property `height` goes to `h`, and `title` is assigned to the same name. + +For potentially missing properties we can set default values using `"="`, like this: + +```js run +let options = { + title: "Menu" +}; + +*!* +let {width = 100, height = 200, title} = options; +*/!* + +alert(title); // Menu +alert(width); // 100 +alert(height); // 200 +``` + +Just like with arrays or function parameters, default values can be any expressions or even function calls. They will be evaluated if the value is not provided. + +In the code below `prompt` asks for `width`, but not for `title`: + +```js run +let options = { + title: "Menu" +}; + +*!* +let {width = prompt("width?"), title = prompt("title?")} = options; +*/!* + +alert(title); // Menu +alert(width); // (whatever the result of prompt is) +``` + +We also can combine both the colon and equality: + +```js run +let options = { + title: "Menu" +}; + +*!* +let {width: w = 100, height: h = 200, title} = options; +*/!* + +alert(title); // Menu +alert(w); // 100 +alert(h); // 200 +``` + +If we have a complex object with many properties, we can extract only what we need: + +```js run +let options = { + title: "Menu", + width: 100, + height: 200 +}; + +// only extract title as a variable +let { title } = options; + +alert(title); // Menu +``` + +### The rest pattern "..." + +What if the object has more properties than we have variables? Can we take some and then assign the "rest" somewhere? + +We can use the rest pattern, just like we did with arrays. It's not supported by some older browsers (IE, use Babel to polyfill it), but works in modern ones. + +It looks like this: + +```js run +let options = { + title: "Menu", + height: 200, + width: 100 +}; + +*!* +// title = property named title +// rest = object with the rest of properties +let {title, ...rest} = options; +*/!* + +// now title="Menu", rest={height: 200, width: 100} +alert(rest.height); // 200 +alert(rest.width); // 100 +``` + +````smart header="Gotcha if there's no `let`" +In the examples above variables were declared right in the assignment: `let {…} = {…}`. Of course, we could use existing variables too, without `let`. But there's a catch. + +This won't work: +```js run +let title, width, height; + +// error in this line +{title, width, height} = {title: "Menu", width: 200, height: 100}; +``` + +The problem is that JavaScript treats `{...}` in the main code flow (not inside another expression) as a code block. Such code blocks can be used to group statements, like this: + +```js run +{ + // a code block + let message = "Hello"; + // ... + alert( message ); +} +``` + +So here JavaScript assumes that we have a code block, that's why there's an error. We want destructuring instead. + +To show JavaScript that it's not a code block, we can wrap the expression in parentheses `(...)`: + +```js run +let title, width, height; + +// okay now +*!*(*/!*{title, width, height} = {title: "Menu", width: 200, height: 100}*!*)*/!*; + +alert( title ); // Menu +``` +```` + +## Nested destructuring + +If an object or an array contains other nested objects and arrays, we can use more complex left-side patterns to extract deeper portions. + +In the code below `options` has another object in the property `size` and an array in the property `items`. The pattern on the left side of the assignment has the same structure to extract values from them: + +```js run +let options = { + size: { + width: 100, + height: 200 + }, + items: ["Cake", "Donut"], + extra: true +}; + +// destructuring assignment split in multiple lines for clarity +let { + size: { // put size here + width, + height + }, + items: [item1, item2], // assign items here + title = "Menu" // not present in the object (default value is used) +} = options; + +alert(title); // Menu +alert(width); // 100 +alert(height); // 200 +alert(item1); // Cake +alert(item2); // Donut +``` + +All properties of `options` object except `extra` which is absent in the left part, are assigned to corresponding variables: + +![](destructuring-complex.svg) + +Finally, we have `width`, `height`, `item1`, `item2` and `title` from the default value. + +Note that there are no variables for `size` and `items`, as we take their content instead. + +## Smart function parameters + +There are times when a function has many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, an item list and so on. + +Here's a bad way to write such a function: + +```js +function showMenu(title = "Untitled", width = 200, height = 100, items = []) { + // ... +} +``` + +In real-life, the problem is how to remember the order of arguments. Usually, IDEs try to help us, especially if the code is well-documented, but still... Another problem is how to call a function when most parameters are ok by default. + +Like this? + +```js +// undefined where default values are fine +showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]) +``` + +That's ugly. And becomes unreadable when we deal with more parameters. + +Destructuring comes to the rescue! + +We can pass parameters as an object, and the function immediately destructurizes them into variables: + +```js run +// we pass object to function +let options = { + title: "My menu", + items: ["Item1", "Item2"] +}; + +// ...and it immediately expands it to variables +function showMenu(*!*{title = "Untitled", width = 200, height = 100, items = []}*/!*) { + // title, items – taken from options, + // width, height – defaults used + alert( `${title} ${width} ${height}` ); // My Menu 200 100 + alert( items ); // Item1, Item2 +} + +showMenu(options); +``` + +We can also use more complex destructuring with nested objects and colon mappings: + +```js run +let options = { + title: "My menu", + items: ["Item1", "Item2"] +}; + +*!* +function showMenu({ + title = "Untitled", + width: w = 100, // width goes to w + height: h = 200, // height goes to h + items: [item1, item2] // items first element goes to item1, second to item2 +}) { +*/!* + alert( `${title} ${w} ${h}` ); // My Menu 100 200 + alert( item1 ); // Item1 + alert( item2 ); // Item2 +} + +showMenu(options); +``` + +The full syntax is the same as for a destructuring assignment: +```js +function({ + incomingProperty: varName = defaultValue + ... +}) +``` + +Then, for an object of parameters, there will be a variable `varName` for the property `incomingProperty`, with `defaultValue` by default. + +Please note that such destructuring assumes that `showMenu()` does have an argument. If we want all values by default, then we should specify an empty object: + +```js +showMenu({}); // ok, all values are default + +showMenu(); // this would give an error +``` + +We can fix this by making `{}` the default value for the whole object of parameters: + +```js run +function showMenu({ title = "Menu", width = 100, height = 200 }*!* = {}*/!*) { + alert( `${title} ${width} ${height}` ); +} + +showMenu(); // Menu 100 200 +``` + +In the code above, the whole arguments object is `{}` by default, so there's always something to destructurize. + +## Summary + +- Destructuring assignment allows for instantly mapping an object or array onto many variables. +- The full object syntax: + ```js + let {prop : varName = defaultValue, ...rest} = object + ``` + + This means that property `prop` should go into the variable `varName` and, if no such property exists, then the `default` value should be used. + + Object properties that have no mapping are copied to the `rest` object. + +- The full array syntax: + + ```js + let [item1 = defaultValue, item2, ...rest] = array + ``` + + The first item goes to `item1`; the second goes into `item2`, and all the rest makes the array `rest`. + +- It's possible to extract data from nested arrays/objects, for that the left side must have the same structure as the right one. 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 new file mode 100644 index 0000000000..8a1ff1a934 --- /dev/null +++ b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/05-data-types/11-date/1-new-date/solution.md b/1-js/05-data-types/11-date/1-new-date/solution.md new file mode 100644 index 0000000000..18286c336a --- /dev/null +++ b/1-js/05-data-types/11-date/1-new-date/solution.md @@ -0,0 +1,18 @@ +The `new Date` constructor uses the local time zone. So the only important thing to remember is that months start from zero. + +So February has number 1. + +Here's an example with numbers as date components: + +```js run +//new Date(year, month, date, hour, minute, second, millisecond) +let d1 = new Date(2012, 1, 20, 3, 12); +alert( d1 ); +``` +We could also create a date from a string, like this: + +```js run +//new Date(datastring) +let d2 = new Date("2012-02-20T03:12"); +alert( d2 ); +``` diff --git a/1-js/05-data-types/10-date/1-new-date/task.md b/1-js/05-data-types/11-date/1-new-date/task.md similarity index 100% rename from 1-js/05-data-types/10-date/1-new-date/task.md rename to 1-js/05-data-types/11-date/1-new-date/task.md diff --git a/1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js b/1-js/05-data-types/11-date/2-get-week-day/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js rename to 1-js/05-data-types/11-date/2-get-week-day/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js b/1-js/05-data-types/11-date/2-get-week-day/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js rename to 1-js/05-data-types/11-date/2-get-week-day/_js.view/test.js diff --git a/1-js/05-data-types/11-date/2-get-week-day/solution.md b/1-js/05-data-types/11-date/2-get-week-day/solution.md new file mode 100644 index 0000000000..58d75c1c31 --- /dev/null +++ b/1-js/05-data-types/11-date/2-get-week-day/solution.md @@ -0,0 +1,14 @@ +The method `date.getDay()` returns the number of the weekday, starting from sunday. + +Let's make an array of weekdays, so that we can get the proper day name by its number: + +```js run demo +function getWeekDay(date) { + let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; + + return days[date.getDay()]; +} + +let date = new Date(2014, 0, 3); // 3 Jan 2014 +alert( getWeekDay(date) ); // FR +``` diff --git a/1-js/05-data-types/10-date/2-get-week-day/task.md b/1-js/05-data-types/11-date/2-get-week-day/task.md similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/task.md rename to 1-js/05-data-types/11-date/2-get-week-day/task.md diff --git a/1-js/05-data-types/10-date/3-weekday/_js.view/solution.js b/1-js/05-data-types/11-date/3-weekday/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/_js.view/solution.js rename to 1-js/05-data-types/11-date/3-weekday/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/3-weekday/_js.view/test.js b/1-js/05-data-types/11-date/3-weekday/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/_js.view/test.js rename to 1-js/05-data-types/11-date/3-weekday/_js.view/test.js diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.md b/1-js/05-data-types/11-date/3-weekday/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.md rename to 1-js/05-data-types/11-date/3-weekday/solution.md diff --git a/1-js/05-data-types/10-date/3-weekday/task.md b/1-js/05-data-types/11-date/3-weekday/task.md similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/task.md rename to 1-js/05-data-types/11-date/3-weekday/task.md diff --git a/1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js b/1-js/05-data-types/11-date/4-get-date-ago/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js rename to 1-js/05-data-types/11-date/4-get-date-ago/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js b/1-js/05-data-types/11-date/4-get-date-ago/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js rename to 1-js/05-data-types/11-date/4-get-date-ago/_js.view/test.js diff --git a/1-js/05-data-types/11-date/4-get-date-ago/solution.md b/1-js/05-data-types/11-date/4-get-date-ago/solution.md new file mode 100644 index 0000000000..5c394c100b --- /dev/null +++ b/1-js/05-data-types/11-date/4-get-date-ago/solution.md @@ -0,0 +1,27 @@ +The idea is simple: to substract given number of days from `date`: + +```js +function getDateAgo(date, days) { + date.setDate(date.getDate() - days); + return date.getDate(); +} +``` + +...But the function should not change `date`. That's an important thing, because the outer code which gives us the date does not expect it to change. + +To implement it let's clone the date, like this: + +```js run demo +function getDateAgo(date, days) { + let dateCopy = new Date(date); + + dateCopy.setDate(date.getDate() - days); + return dateCopy.getDate(); +} + +let date = new Date(2015, 0, 2); + +alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015) +alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014) +alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014) +``` diff --git a/1-js/05-data-types/11-date/4-get-date-ago/task.md b/1-js/05-data-types/11-date/4-get-date-ago/task.md new file mode 100644 index 0000000000..058d39c7ea --- /dev/null +++ b/1-js/05-data-types/11-date/4-get-date-ago/task.md @@ -0,0 +1,21 @@ +importance: 4 + +--- + +# Which day of month was many days ago? + +Create a function `getDateAgo(date, days)` to return the day of month `days` ago from the `date`. + +For instance, if today is 20th, then `getDateAgo(new Date(), 1)` should be 19th and `getDateAgo(new Date(), 2)` should be 18th. + +Should work reliably for `days=365` or more: + +```js +let date = new Date(2015, 0, 2); + +alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015) +alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014) +alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014) +``` + +P.S. The function should not modify the given `date`. diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js b/1-js/05-data-types/11-date/5-last-day-of-month/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js rename to 1-js/05-data-types/11-date/5-last-day-of-month/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js b/1-js/05-data-types/11-date/5-last-day-of-month/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js rename to 1-js/05-data-types/11-date/5-last-day-of-month/_js.view/test.js diff --git a/1-js/05-data-types/11-date/5-last-day-of-month/solution.md b/1-js/05-data-types/11-date/5-last-day-of-month/solution.md new file mode 100644 index 0000000000..4f642536e1 --- /dev/null +++ b/1-js/05-data-types/11-date/5-last-day-of-month/solution.md @@ -0,0 +1,13 @@ +Let's create a date using the next month, but pass zero as the day: +```js run demo +function getLastDayOfMonth(year, month) { + let date = new Date(year, month + 1, 0); + return date.getDate(); +} + +alert( getLastDayOfMonth(2012, 0) ); // 31 +alert( getLastDayOfMonth(2012, 1) ); // 29 +alert( getLastDayOfMonth(2013, 1) ); // 28 +``` + +Normally, dates start from 1, but technically we can pass any number, the date will autoadjust itself. So when we pass 0, then it means "one day before 1st day of the month", in other words: "the last day of the previous month". diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/task.md b/1-js/05-data-types/11-date/5-last-day-of-month/task.md similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/task.md rename to 1-js/05-data-types/11-date/5-last-day-of-month/task.md diff --git a/1-js/05-data-types/11-date/6-get-seconds-today/solution.md b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md new file mode 100644 index 0000000000..8f8e52b68d --- /dev/null +++ b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md @@ -0,0 +1,28 @@ +To get the number of seconds, we can generate a date using the current day and time 00:00:00, then substract it from "now". + +The difference is the number of milliseconds from the beginning of the day, that we should divide by 1000 to get seconds: + +```js run +function getSecondsToday() { + let now = new Date(); + + // create an object using the current day/month/year + let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + let diff = now - today; // ms difference + return Math.round(diff / 1000); // make seconds +} + +alert( getSecondsToday() ); +``` + +An alternative solution would be to get hours/minutes/seconds and convert them to seconds: + +```js run +function getSecondsToday() { + let d = new Date(); + return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); +} + +alert( getSecondsToday() ); +``` diff --git a/1-js/05-data-types/11-date/6-get-seconds-today/task.md b/1-js/05-data-types/11-date/6-get-seconds-today/task.md new file mode 100644 index 0000000000..4567909286 --- /dev/null +++ b/1-js/05-data-types/11-date/6-get-seconds-today/task.md @@ -0,0 +1,15 @@ +importance: 5 + +--- + +# How many seconds have passed today? + +Write a function `getSecondsToday()` that returns the number of seconds from the beginning of today. + +For instance, if now were `10:00 am`, and there was no daylight savings shift, then: + +```js +getSecondsToday() == 36000 // (3600 * 10) +``` + +The function should work in any day. That is, it should not have a hard-coded value of "today". diff --git a/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md b/1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md rename to 1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/solution.md diff --git a/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md b/1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/task.md similarity index 100% rename from 1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md rename to 1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/task.md diff --git a/1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js b/1-js/05-data-types/11-date/8-format-date-relative/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js rename to 1-js/05-data-types/11-date/8-format-date-relative/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js b/1-js/05-data-types/11-date/8-format-date-relative/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js rename to 1-js/05-data-types/11-date/8-format-date-relative/_js.view/test.js diff --git a/1-js/05-data-types/11-date/8-format-date-relative/solution.md b/1-js/05-data-types/11-date/8-format-date-relative/solution.md new file mode 100644 index 0000000000..3724856857 --- /dev/null +++ b/1-js/05-data-types/11-date/8-format-date-relative/solution.md @@ -0,0 +1,78 @@ +To get the time from `date` till now -- let's substract the dates. + +```js run demo +function formatDate(date) { + let diff = new Date() - date; // the difference in milliseconds + + if (diff < 1000) { // less than 1 second + return 'right now'; + } + + let sec = Math.floor(diff / 1000); // convert diff to seconds + + if (sec < 60) { + return sec + ' sec. ago'; + } + + let min = Math.floor(diff / 60000); // convert diff to minutes + if (min < 60) { + return min + ' min. ago'; + } + + // format the date + // add leading zeroes to single-digit day/month/hours/minutes + let d = date; + d = [ + '0' + d.getDate(), + '0' + (d.getMonth() + 1), + '' + d.getFullYear(), + '0' + d.getHours(), + '0' + d.getMinutes() + ].map(component => component.slice(-2)); // take last 2 digits of every component + + // join the components into date + return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':'); +} + +alert( formatDate(new Date(new Date - 1)) ); // "right now" + +alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" + +alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" + +// yesterday's date like 31.12.2016 20:00 +alert( formatDate(new Date(new Date - 86400 * 1000)) ); +``` + +Alternative solution: + +```js run +function formatDate(date) { + let dayOfMonth = date.getDate(); + let month = date.getMonth() + 1; + let year = date.getFullYear(); + let hour = date.getHours(); + let minutes = date.getMinutes(); + let diffMs = new Date() - date; + let diffSec = Math.round(diffMs / 1000); + let diffMin = diffSec / 60; + let diffHour = diffMin / 60; + + // formatting + year = year.toString().slice(-2); + month = month < 10 ? '0' + month : month; + dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth; + hour = hour < 10 ? '0' + hour : hour; + minutes = minutes < 10 ? '0' + minutes : minutes; + + if (diffSec < 1) { + return 'right now'; + } else if (diffMin < 1) { + return `${diffSec} sec. ago` + } else if (diffHour < 1) { + return `${diffMin} min. ago` + } else { + return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}` + } +} +``` diff --git a/1-js/05-data-types/11-date/8-format-date-relative/task.md b/1-js/05-data-types/11-date/8-format-date-relative/task.md new file mode 100644 index 0000000000..9651b305f5 --- /dev/null +++ b/1-js/05-data-types/11-date/8-format-date-relative/task.md @@ -0,0 +1,25 @@ +importance: 4 + +--- + +# Format the relative date + +Write a function `formatDate(date)` that should format `date` as follows: + +- If since `date` passed less than 1 second, then `"right now"`. +- Otherwise, if since `date` passed less than 1 minute, then `"n sec. ago"`. +- Otherwise, if less than an hour, then `"m min. ago"`. +- Otherwise, the full date in the format `"DD.MM.YY HH:mm"`. That is: `"day.month.year hours:minutes"`, all in 2-digit format, e.g. `31.12.16 10:00`. + +For instance: + +```js +alert( formatDate(new Date(new Date - 1)) ); // "right now" + +alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" + +alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" + +// yesterday's date like 31.12.16 20:00 +alert( formatDate(new Date(new Date - 86400 * 1000)) ); +``` diff --git a/1-js/05-data-types/11-date/article.md b/1-js/05-data-types/11-date/article.md new file mode 100644 index 0000000000..6958a3a97e --- /dev/null +++ b/1-js/05-data-types/11-date/article.md @@ -0,0 +1,433 @@ +# Date and time + +Let's meet a new built-in object: [Date](mdn:js/Date). It stores the date, time and provides methods for date/time management. + +For instance, we can use it to store creation/modification times, to measure time, or just to print out the current date. + +## Creation + +To create a new `Date` object call `new Date()` with one of the following arguments: + +`new Date()` +: Without arguments -- create a `Date` object for the current date and time: + + ```js run + let now = new Date(); + alert( now ); // shows current date/time + ``` + +`new Date(milliseconds)` +: Create a `Date` object with the time equal to number of milliseconds (1/1000 of a second) passed after the Jan 1st of 1970 UTC+0. + + ```js run + // 0 means 01.01.1970 UTC+0 + let Jan01_1970 = new Date(0); + alert( Jan01_1970 ); + + // now add 24 hours, get 02.01.1970 UTC+0 + let Jan02_1970 = new Date(24 * 3600 * 1000); + alert( Jan02_1970 ); + ``` + + An integer number representing the number of milliseconds that has passed since the beginning of 1970 is called a *timestamp*. + + It's a lightweight numeric representation of a date. We can always create a date from a timestamp using `new Date(timestamp)` and convert the existing `Date` object to a timestamp using the `date.getTime()` method (see below). + + Dates before 01.01.1970 have negative timestamps, e.g.: + ```js run + // 31 Dec 1969 + let Dec31_1969 = new Date(-24 * 3600 * 1000); + alert( Dec31_1969 ); + ``` + +`new Date(datestring)` +: If there is a single argument, and it's a string, then it is parsed automatically. The algorithm is the same as `Date.parse` uses, we'll cover it later. + + ```js run + let date = new Date("2017-01-26"); + alert(date); + // The time is not set, so it's assumed to be midnight GMT and + // is adjusted according to the timezone the code is run in + // So the result could be + // Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time) + // or + // Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time) + ``` + +`new Date(year, month, date, hours, minutes, seconds, ms)` +: Create the date with the given components in the local time zone. Only the first two arguments are obligatory. + + - The `year` should have 4 digits. For compatibility, 2 digits are also accepted and considered `19xx`, e.g. `98` is the same as `1998` here, but always using 4 digits is strongly encouraged. + - The `month` count starts with `0` (Jan), up to `11` (Dec). + - The `date` parameter is actually the day of month, if absent then `1` is assumed. + - If `hours/minutes/seconds/ms` is absent, they are assumed to be equal `0`. + + For instance: + + ```js + new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00 + new Date(2011, 0, 1); // the same, hours etc are 0 by default + ``` + + The maximal precision is 1 ms (1/1000 sec): + + ```js run + let date = new Date(2011, 0, 1, 2, 3, 4, 567); + alert( date ); // 1.01.2011, 02:03:04.567 + ``` + +## Access date components + +There are methods to access the year, month and so on from the `Date` object: + +[getFullYear()](mdn:js/Date/getFullYear) +: Get the year (4 digits) + +[getMonth()](mdn:js/Date/getMonth) +: Get the month, **from 0 to 11**. + +[getDate()](mdn:js/Date/getDate) +: Get the day of month, from 1 to 31, the name of the method does look a little bit strange. + +[getHours()](mdn:js/Date/getHours), [getMinutes()](mdn:js/Date/getMinutes), [getSeconds()](mdn:js/Date/getSeconds), [getMilliseconds()](mdn:js/Date/getMilliseconds) +: Get the corresponding time components. + +```warn header="Not `getYear()`, but `getFullYear()`" +Many JavaScript engines implement a non-standard method `getYear()`. This method is deprecated. It returns 2-digit year sometimes. Please never use it. There is `getFullYear()` for the year. +``` + +Additionally, we can get a day of week: + +[getDay()](mdn:js/Date/getDay) +: Get the day of week, from `0` (Sunday) to `6` (Saturday). The first day is always Sunday, in some countries that's not so, but can't be changed. + +**All the methods above return the components relative to the local time zone.** + +There are also their UTC-counterparts, that return day, month, year and so on for the time zone UTC+0: [getUTCFullYear()](mdn:js/Date/getUTCFullYear), [getUTCMonth()](mdn:js/Date/getUTCMonth), [getUTCDay()](mdn:js/Date/getUTCDay). Just insert the `"UTC"` right after `"get"`. + +If your local time zone is shifted relative to UTC, then the code below shows different hours: + +```js run +// current date +let date = new Date(); + +// the hour in your current time zone +alert( date.getHours() ); + +// the hour in UTC+0 time zone (London time without daylight savings) +alert( date.getUTCHours() ); +``` + +Besides the given methods, there are two special ones that do not have a UTC-variant: + +[getTime()](mdn:js/Date/getTime) +: Returns the timestamp for the date -- a number of milliseconds passed from the January 1st of 1970 UTC+0. + +[getTimezoneOffset()](mdn:js/Date/getTimezoneOffset) +: Returns the difference between UTC and the local time zone, in minutes: + + ```js run + // if you are in timezone UTC-1, outputs 60 + // if you are in timezone UTC+3, outputs -180 + alert( new Date().getTimezoneOffset() ); + + ``` + +## Setting date components + +The following methods allow to set date/time components: + +- [`setFullYear(year, [month], [date])`](mdn:js/Date/setFullYear) +- [`setMonth(month, [date])`](mdn:js/Date/setMonth) +- [`setDate(date)`](mdn:js/Date/setDate) +- [`setHours(hour, [min], [sec], [ms])`](mdn:js/Date/setHours) +- [`setMinutes(min, [sec], [ms])`](mdn:js/Date/setMinutes) +- [`setSeconds(sec, [ms])`](mdn:js/Date/setSeconds) +- [`setMilliseconds(ms)`](mdn:js/Date/setMilliseconds) +- [`setTime(milliseconds)`](mdn:js/Date/setTime) (sets the whole date by milliseconds since 01.01.1970 UTC) + +Every one of them except `setTime()` has a UTC-variant, for instance: `setUTCHours()`. + +As we can see, some methods can set multiple components at once, for example `setHours`. The components that are not mentioned are not modified. + +For instance: + +```js run +let today = new Date(); + +today.setHours(0); +alert(today); // still today, but the hour is changed to 0 + +today.setHours(0, 0, 0, 0); +alert(today); // still today, now 00:00:00 sharp. +``` + +## Autocorrection + +The *autocorrection* is a very handy feature of `Date` objects. We can set out-of-range values, and it will auto-adjust itself. + +For instance: + +```js run +let date = new Date(2013, 0, *!*32*/!*); // 32 Jan 2013 ?!? +alert(date); // ...is 1st Feb 2013! +``` + +Out-of-range date components are distributed automatically. + +Let's say we need to increase the date "28 Feb 2016" by 2 days. It may be "2 Mar" or "1 Mar" in case of a leap-year. We don't need to think about it. Just add 2 days. The `Date` object will do the rest: + +```js run +let date = new Date(2016, 1, 28); +*!* +date.setDate(date.getDate() + 2); +*/!* + +alert( date ); // 1 Mar 2016 +``` + +That feature is often used to get the date after the given period of time. For instance, let's get the date for "70 seconds after now": + +```js run +let date = new Date(); +date.setSeconds(date.getSeconds() + 70); + +alert( date ); // shows the correct date +``` + +We can also set zero or even negative values. For example: + +```js run +let date = new Date(2016, 0, 2); // 2 Jan 2016 + +date.setDate(1); // set day 1 of month +alert( date ); + +date.setDate(0); // min day is 1, so the last day of the previous month is assumed +alert( date ); // 31 Dec 2015 +``` + +## Date to number, date diff + +When a `Date` object is converted to number, it becomes the timestamp same as `date.getTime()`: + +```js run +let date = new Date(); +alert(+date); // the number of milliseconds, same as date.getTime() +``` + +The important side effect: dates can be subtracted, the result is their difference in ms. + +That can be used for time measurements: + +```js run +let start = new Date(); // start measuring time + +// do the job +for (let i = 0; i < 100000; i++) { + let doSomething = i * i * i; +} + +let end = new Date(); // end measuring time + +alert( `The loop took ${end - start} ms` ); +``` + +## Date.now() + +If we only want to measure time, we don't need the `Date` object. + +There's a special method `Date.now()` that returns the current timestamp. + +It is semantically equivalent to `new Date().getTime()`, but it doesn't create an intermediate `Date` object. So it's faster and doesn't put pressure on garbage collection. + +It is used mostly for convenience or when performance matters, like in games in JavaScript or other specialized applications. + +So this is probably better: + +```js run +*!* +let start = Date.now(); // milliseconds count from 1 Jan 1970 +*/!* + +// do the job +for (let i = 0; i < 100000; i++) { + let doSomething = i * i * i; +} + +*!* +let end = Date.now(); // done +*/!* + +alert( `The loop took ${end - start} ms` ); // subtract numbers, not dates +``` + +## Benchmarking + +If we want a reliable benchmark of CPU-hungry function, we should be careful. + +For instance, let's measure two functions that calculate the difference between two dates: which one is faster? + +Such performance measurements are often called "benchmarks". + +```js +// we have date1 and date2, which function faster returns their difference in ms? +function diffSubtract(date1, date2) { + return date2 - date1; +} + +// or +function diffGetTime(date1, date2) { + return date2.getTime() - date1.getTime(); +} +``` + +These two do exactly the same thing, but one of them uses an explicit `date.getTime()` to get the date in ms, and the other one relies on a date-to-number transform. Their result is always the same. + +So, which one is faster? + +The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it at least 100000 times. + +Let's measure: + +```js run +function diffSubtract(date1, date2) { + return date2 - date1; +} + +function diffGetTime(date1, date2) { + return date2.getTime() - date1.getTime(); +} + +function bench(f) { + let date1 = new Date(0); + let date2 = new Date(); + + let start = Date.now(); + for (let i = 0; i < 100000; i++) f(date1, date2); + return Date.now() - start; +} + +alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' ); +alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' ); +``` + +Wow! Using `getTime()` is so much faster! That's because there's no type conversion, it is much easier for engines to optimize. + +Okay, we have something. But that's not a good benchmark yet. + +Imagine that at the time of running `bench(diffSubtract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` that work has finished. + +A pretty real scenario for a modern multi-process OS. + +As a result, the first benchmark will have less CPU resources than the second. That may lead to wrong results. + +**For more reliable benchmarking, the whole pack of benchmarks should be rerun multiple times.** + +For example, like this: + +```js run +function diffSubtract(date1, date2) { + return date2 - date1; +} + +function diffGetTime(date1, date2) { + return date2.getTime() - date1.getTime(); +} + +function bench(f) { + let date1 = new Date(0); + let date2 = new Date(); + + let start = Date.now(); + for (let i = 0; i < 100000; i++) f(date1, date2); + return Date.now() - start; +} + +let time1 = 0; +let time2 = 0; + +*!* +// run bench(diffSubtract) and bench(diffGetTime) each 10 times alternating +for (let i = 0; i < 10; i++) { + time1 += bench(diffSubtract); + time2 += bench(diffGetTime); +} +*/!* + +alert( 'Total time for diffSubtract: ' + time1 ); +alert( 'Total time for diffGetTime: ' + time2 ); +``` + +Modern JavaScript engines start applying advanced optimizations only to "hot code" that executes many times (no need to optimize rarely executed things). So, in the example above, first executions are not well-optimized. We may want to add a heat-up run: + +```js +// added for "heating up" prior to the main loop +bench(diffSubtract); +bench(diffGetTime); + +// now benchmark +for (let i = 0; i < 10; i++) { + time1 += bench(diffSubtract); + time2 += bench(diffGetTime); +} +``` + +```warn header="Be careful doing microbenchmarking" +Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small, such as how an operator works, or a built-in function. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all. + +The great pack of articles about V8 can be found at . +``` + +## Date.parse from a string + +The method [Date.parse(str)](mdn:js/Date/parse) can read a date from a string. + +The string format should be: `YYYY-MM-DDTHH:mm:ss.sssZ`, where: + +- `YYYY-MM-DD` -- is the date: year-month-day. +- The character `"T"` is used as the delimiter. +- `HH:mm:ss.sss` -- is the time: hours, minutes, seconds and milliseconds. +- The optional `'Z'` part denotes the time zone in the format `+-hh:mm`. A single letter `Z` would mean UTC+0. + +Shorter variants are also possible, like `YYYY-MM-DD` or `YYYY-MM` or even `YYYY`. + +The call to `Date.parse(str)` parses the string in the given format and returns the timestamp (number of milliseconds from 1 Jan 1970 UTC+0). If the format is invalid, returns `NaN`. + +For instance: + +```js run +let ms = Date.parse('2012-01-26T13:51:50.417-07:00'); + +alert(ms); // 1327611110417 (timestamp) +``` + +We can instantly create a `new Date` object from the timestamp: + +```js run +let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') ); + +alert(date); +``` + +## Summary + +- Date and time in JavaScript are represented with the [Date](mdn:js/Date) object. We can't create "only date" or "only time": `Date` objects always carry both. +- Months are counted from zero (yes, January is a zero month). +- Days of week in `getDay()` are also counted from zero (that's Sunday). +- `Date` auto-corrects itself when out-of-range components are set. Good for adding/subtracting days/months/hours. +- Dates can be subtracted, giving their difference in milliseconds. That's because a `Date` becomes the timestamp when converted to a number. +- Use `Date.now()` to get the current timestamp fast. + +Note that unlike many other systems, timestamps in JavaScript are in milliseconds, not in seconds. + +Sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point): + +```js run +alert(`Loading started ${performance.now()}ms ago`); +// Something like: "Loading started 34731.26000000001ms ago" +// .26 is microseconds (260 microseconds) +// more than 3 digits after the decimal point are precision errors, only the first 3 are correct +``` + +Node.js has `microtime` module and other ways. Technically, almost any device and environment allows to get more precision, it's just not in `Date`. diff --git a/1-js/05-data-types/11-json/2-serialize-event-circular/task.md b/1-js/05-data-types/11-json/2-serialize-event-circular/task.md deleted file mode 100644 index 8b3963ddf4..0000000000 --- a/1-js/05-data-types/11-json/2-serialize-event-circular/task.md +++ /dev/null @@ -1,42 +0,0 @@ -importance: 5 - ---- - -# Exclude backreferences - -In simple cases of circular references, we can exclude an offending property from serialization by its name. - -But sometimes there are many backreferences. And names may be used both in circular references and normal properties. - -Write `replacer` function to stringify everything, but remove properties that reference `meetup`: - -```js run -let room = { - number: 23 -}; - -let meetup = { - title: "Conference", - occupiedBy: [{name: "John"}, {name: "Alice"}], - place: room -}; - -*!* -// circular references -room.occupiedBy = meetup; -meetup.self = meetup; -*/!* - -alert( JSON.stringify(meetup, function replacer(key, value) { - /* your code */ -})); - -/* result should be: -{ - "title":"Conference", - "occupiedBy":[{"name":"John"},{"name":"Alice"}], - "place":{"number":23} -} -*/ -``` - diff --git a/1-js/05-data-types/11-json/article.md b/1-js/05-data-types/11-json/article.md deleted file mode 100644 index 174b2b7faf..0000000000 --- a/1-js/05-data-types/11-json/article.md +++ /dev/null @@ -1,527 +0,0 @@ -# JSON methods, toJSON - -Let's say we have a complex object, and we'd like to convert it into a string, to send it over a network, or just to output it for logging purposes. - -Naturally, such a string should include all important properties. - -We could implement the conversion like this: - -```js run -let user = { - name: "John", - age: 30, - -*!* - toString() { - return `{name: "${this.name}", age: ${this.age}}`; - } -*/!* -}; - -alert(user); // {name: "John", age: 30} -``` - -...But in the process of development, new properties are added, old properties are renamed and removed. Updating such `toString` every time can become a pain. We could try to loop over properties in it, but what if the object is complex and has nested objects in properties? We'd need to implement their conversion as well. And, if we're sending the object over a network, then we also need to supply the code to "read" our object on the receiving side. - -Luckily, there's no need to write the code to handle all this. The task has been solved already. - -## JSON.stringify - -The [JSON](http://en.wikipedia.org/wiki/JSON) (JavaScript Object Notation) is a general format to represent values and objects. It is described as in [RFC 4627](http://tools.ietf.org/html/rfc4627) standard. Initially it was made for JavaScript, but many other languages have libraries to handle it as well. So it's easy to use JSON for data exchange when the client uses JavaScript and the server is written on Ruby/PHP/Java/Whatever. - -JavaScript provides methods: - -- `JSON.stringify` to convert objects into JSON. -- `JSON.parse` to convert JSON back into an object. - -For instance, here we `JSON.stringify` a student: -```js run -let student = { - name: 'John', - age: 30, - isAdmin: false, - courses: ['html', 'css', 'js'], - wife: null -}; - -*!* -let json = JSON.stringify(student); -*/!* - -alert(typeof json); // we've got a string! - -alert(json); -*!* -/* JSON-encoded object: -{ - "name": "John", - "age": 30, - "isAdmin": false, - "courses": ["html", "css", "js"], - "wife": null -} -*/ -*/!* -``` - -The method `JSON.stringify(student)` takes the object and converts it into a string. - -The resulting `json` string is a called *JSON-encoded* or *serialized* or *stringified* or *marshalled* object. We are ready to send it over the wire or put into plain data store. - - -Please note that JSON-encoded object has several important differences from the object literal: - -- Strings use double quotes. No single quotes or backticks in JSON. So `'John'` becomes `"John"`. -- Object property names are double-quoted also. That's obligatory. So `age:30` becomes `"age":30`. - -`JSON.stringify` can be applied to primitives as well. - -Natively supported JSON types are: - -- Objects `{ ... }` -- Arrays `[ ... ]` -- Primitives: - - strings, - - numbers, - - boolean values `true/false`, - - `null`. - -For instance: - -```js run -// a number in JSON is just a number -alert( JSON.stringify(1) ) // 1 - -// a string in JSON is still a string, but double-quoted -alert( JSON.stringify('test') ) // "test" - -alert( JSON.stringify(true) ); // true - -alert( JSON.stringify([1, 2, 3]) ); // [1,2,3] -``` - -JSON is data-only cross-language specification, so some JavaScript-specific object properties are skipped by `JSON.stringify`. - -Namely: - -- Function properties (methods). -- Symbolic properties. -- Properties that store `undefined`. - -```js run -let user = { - sayHi() { // ignored - alert("Hello"); - }, - [Symbol("id")]: 123, // ignored - something: undefined // ignored -}; - -alert( JSON.stringify(user) ); // {} (empty object) -``` - -Usually that's fine. If that's not what we want, then soon we'll see how to customize the process. - -The great thing is that nested objects are supported and converted automatically. - -For instance: - -```js run -let meetup = { - title: "Conference", -*!* - room: { - number: 23, - participants: ["john", "ann"] - } -*/!* -}; - -alert( JSON.stringify(meetup) ); -/* The whole structure is stringified: -{ - "title":"Conference", - "room":{"number":23,"participants":["john","ann"]}, -} -*/ -``` - -The important limitation: there must be no circular references. - -For instance: - -```js run -let room = { - number: 23 -}; - -let meetup = { - title: "Conference", - participants: ["john", "ann"] -}; - -meetup.place = room; // meetup references room -room.occupiedBy = meetup; // room references meetup - -*!* -JSON.stringify(meetup); // Error: Converting circular structure to JSON -*/!* -``` - -Here, the conversion fails, because of circular reference: `room.occupiedBy` references `meetup`, and `meetup.place` references `room`: - -![](json-meetup.png) - - -## Excluding and transforming: replacer - -The full syntax of `JSON.stringify` is: - -```js -let json = JSON.stringify(value[, replacer, space]) -``` - -value -: A value to encode. - -replacer -: Array of properties to encode or a mapping function `function(key, value)`. - -space -: Amount of space to use for formatting - -Most of time, `JSON.stringify` is used with first argument only. But if we need to fine-tune the replacement process, like to filter out circular references, we can use the second argument of `JSON.stringify`. - -If we pass an array of properties to it, only these properties will be encoded. - -For instance: - -```js run -let room = { - number: 23 -}; - -let meetup = { - title: "Conference", - participants: [{name: "John"}, {name: "Alice"}], - place: room // meetup references room -}; - -room.occupiedBy = meetup; // room references meetup - -alert( JSON.stringify(meetup, *!*['title', 'participants']*/!*) ); -// {"title":"Conference","participants":[{},{}]} -``` - -Here we are probably too strict. The property list is applied to the whole object structure. So participants are empty, because `name` is not in the list. - -Let's include every property except `room.occupiedBy` that would cause the circular reference: - -```js run -let room = { - number: 23 -}; - -let meetup = { - title: "Conference", - participants: [{name: "John"}, {name: "Alice"}], - place: room // meetup references room -}; - -room.occupiedBy = meetup; // room references meetup - -alert( JSON.stringify(meetup, *!*['title', 'participants', 'place', 'name', 'number']*/!*) ); -/* -{ - "title":"Conference", - "participants":[{"name":"John"},{"name":"Alice"}], - "place":{"number":23} -} -*/ -``` - -Now everything except `occupiedBy` is serialized. But the list of properties is quite long. - -Fortunately, we can use a function instead of an array as the `replacer`. - -The function will be called for every `(key,value)` pair and should return the "replaced" value, which will be used instead of the original one. - -In our case, we can return `value` "as is" for everything except `occupiedBy`. To ignore `occupiedBy`, the code below returns `undefined`: - -```js run -let room = { - number: 23 -}; - -let meetup = { - title: "Conference", - participants: [{name: "John"}, {name: "Alice"}], - place: room // meetup references room -}; - -room.occupiedBy = meetup; // room references meetup - -alert( JSON.stringify(meetup, function replacer(key, value) { - alert(`${key}: ${value}`); // to see what replacer gets - return (key == 'occupiedBy') ? undefined : value; -})); - -/* key:value pairs that come to replacer: -: [object Object] -title: Conference -participants: [object Object],[object Object] -0: [object Object] -name: John -1: [object Object] -name: Alice -place: [object Object] -number: 23 -*/ -``` - -Please note that `replacer` function gets every key/value pair including nested objects and array items. It is applied recursively. The value of `this` inside `replacer` is the object that contains the current property. - -The first call is special. It is made using a special "wrapper object": `{"": meetup}`. In other words, the first `(key,value)` pair has an empty key, and the value is the target object as a whole. That's why the first line is `":[object Object]"` in the example above. - -The idea is to provide as much power for `replacer` as possible: it has a chance to analyze and replace/skip the whole object if necessary. - - -## Formatting: spacer - -The third argument of `JSON.stringify(value, replacer, spaces)` is the number of spaces to use for pretty formatting. - -Previously, all stringified objects had no indents and extra spaces. That's fine if we want to send an object over a network. The `spacer` argument is used exclusively for a nice output. - -Here `spacer = 2` tells JavaScript to show nested objects on multiple lines, with indentation of 2 spaces inside an object: - -```js run -let user = { - name: "John", - age: 25, - roles: { - isAdmin: false, - isEditor: true - } -}; - -alert(JSON.stringify(user, null, 2)); -/* two-space indents: -{ - "name": "John", - "age": 25, - "roles": { - "isAdmin": false, - "isEditor": true - } -} -*/ - -/* for JSON.stringify(user, null, 4) the result would be more indented: -{ - "name": "John", - "age": 25, - "roles": { - "isAdmin": false, - "isEditor": true - } -} -*/ -``` - -The `spaces` parameter is used solely for logging and nice-output purposes. - -## Custom "toJSON" - -Like `toString` for a string conversion, an object may provide method `toJSON` for to-JSON conversion. `JSON.stringify` automatically calls it if available. - -For instance: - -```js run -let room = { - number: 23 -}; - -let meetup = { - title: "Conference", - date: new Date(Date.UTC(2017, 0, 1)), - room -}; - -alert( JSON.stringify(meetup) ); -/* - { - "title":"Conference", -*!* - "date":"2017-01-01T00:00:00.000Z", // (1) -*/!* - "room": {"number":23} // (2) - } -*/ -``` - -Here we can see that `date` `(1)` became a string. That's because all dates have a built-in `toJSON` method which returns such kind of string. - -Now let's add a custom `toJSON` for our object `room`: - -```js run -let room = { - number: 23, -*!* - toJSON() { - return this.number; - } -*/!* -}; - -let meetup = { - title: "Conference", - room -}; - -*!* -alert( JSON.stringify(room) ); // 23 -*/!* - -alert( JSON.stringify(meetup) ); -/* - { - "title":"Conference", -*!* - "room": 23 -*/!* - } -*/ -``` - -As we can see, `toJSON` is used both for the direct call `JSON.stringify(room)` and for the nested object. - - -## JSON.parse - -To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/parse). - -The syntax: -```js -let value = JSON.parse(str[, reviver]); -``` - -str -: JSON-string to parse. - -reviver -: Optional function(key,value) that will be called for each `(key,value)` pair and can transform the value. - -For instance: - -```js run -// stringified array -let numbers = "[0, 1, 2, 3]"; - -numbers = JSON.parse(numbers); - -alert( numbers[1] ); // 1 -``` - -Or for nested objects: - -```js run -let user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; - -user = JSON.parse(user); - -alert( user.friends[1] ); // 1 -``` - -The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the format. - -Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging purposes): - -```js -let json = `{ - *!*name*/!*: "John", // mistake: property name without quotes - "surname": *!*'Smith'*/!*, // mistake: single quotes in value (must be double) - *!*'isAdmin'*/!*: false // mistake: single quotes in key (must be double) - "birthday": *!*new Date(2000, 2, 3)*/!*, // mistake: no "new" is allowed, only bare values - "friends": [0,1,2,3] // here all fine -}`; -``` - -Besides, JSON does not support comments. Adding a comment to JSON makes it invalid. - -There's another format named [JSON5](http://json5.org/), which allows unquoted keys, comments etc. But this is a standalone library, not in the specification of the language. - -The regular JSON is that strict not because its developers are lazy, but to allow easy, reliable and very fast implementations of the parsing algorithm. - -## Using reviver - -Imagine, we got a stringified `meetup` object from the server. - -It looks like this: - -```js -// title: (meetup title), date: (meetup date) -let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; -``` - -...And now we need to *deserialize* it, to turn back into JavaScript object. - -Let's do it by calling `JSON.parse`: - -```js run -let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; - -let meetup = JSON.parse(str); - -*!* -alert( meetup.date.getDate() ); // Error! -*/!* -``` - -Whoops! An error! - -The value of `meetup.date` is a string, not a `Date` object. How could `JSON.parse` know that it should transform that string into a `Date`? - -Let's pass to `JSON.parse` the reviving function that returns all values "as is", but `date` will become a `Date`: - -```js run -let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; - -*!* -let meetup = JSON.parse(str, function(key, value) { - if (key == 'date') return new Date(value); - return value; -}); -*/!* - -alert( meetup.date.getDate() ); // now works! -``` - -By the way, that works for nested objects as well: - -```js run -let schedule = `{ - "meetups": [ - {"title":"Conference","date":"2017-11-30T12:00:00.000Z"}, - {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} - ] -}`; - -schedule = JSON.parse(schedule, function(key, value) { - if (key == 'date') return new Date(value); - return value; -}); - -*!* -alert( schedule.meetups[1].date.getDate() ); // works! -*/!* -``` - - - -## Summary - -- JSON is a data format that has its own independent standard and libraries for most programming languages. -- JSON supports plain objects, arrays, strings, numbers, booleans and `null`. -- JavaScript provides methods [JSON.stringify](mdn:js/JSON/stringify) to serialize into JSON and [JSON.parse](mdn:js/JSON/parse) to read from JSON. -- Both methods support transformer functions for smart reading/writing. -- If an object has `toJSON`, then it is called by `JSON.stringify`. diff --git a/1-js/05-data-types/11-json/json-meetup.png b/1-js/05-data-types/11-json/json-meetup.png deleted file mode 100644 index 0a26e0a679..0000000000 Binary files a/1-js/05-data-types/11-json/json-meetup.png and /dev/null differ diff --git a/1-js/05-data-types/11-json/json-meetup@2x.png b/1-js/05-data-types/11-json/json-meetup@2x.png deleted file mode 100644 index b5f6a40129..0000000000 Binary files a/1-js/05-data-types/11-json/json-meetup@2x.png and /dev/null differ diff --git a/1-js/05-data-types/11-json/1-serialize-object/solution.md b/1-js/05-data-types/12-json/1-serialize-object/solution.md similarity index 100% rename from 1-js/05-data-types/11-json/1-serialize-object/solution.md rename to 1-js/05-data-types/12-json/1-serialize-object/solution.md diff --git a/1-js/05-data-types/11-json/1-serialize-object/task.md b/1-js/05-data-types/12-json/1-serialize-object/task.md similarity index 100% rename from 1-js/05-data-types/11-json/1-serialize-object/task.md rename to 1-js/05-data-types/12-json/1-serialize-object/task.md diff --git a/1-js/05-data-types/11-json/2-serialize-event-circular/solution.md b/1-js/05-data-types/12-json/2-serialize-event-circular/solution.md similarity index 100% rename from 1-js/05-data-types/11-json/2-serialize-event-circular/solution.md rename to 1-js/05-data-types/12-json/2-serialize-event-circular/solution.md diff --git a/1-js/05-data-types/12-json/2-serialize-event-circular/task.md b/1-js/05-data-types/12-json/2-serialize-event-circular/task.md new file mode 100644 index 0000000000..3755a24aa2 --- /dev/null +++ b/1-js/05-data-types/12-json/2-serialize-event-circular/task.md @@ -0,0 +1,41 @@ +importance: 5 + +--- + +# Exclude backreferences + +In simple cases of circular references, we can exclude an offending property from serialization by its name. + +But sometimes we can't just use the name, as it may be used both in circular references and normal properties. So we can check the property by its value. + +Write `replacer` function to stringify everything, but remove properties that reference `meetup`: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + occupiedBy: [{name: "John"}, {name: "Alice"}], + place: room +}; + +*!* +// circular references +room.occupiedBy = meetup; +meetup.self = meetup; +*/!* + +alert( JSON.stringify(meetup, function replacer(key, value) { + /* your code */ +})); + +/* result should be: +{ + "title":"Conference", + "occupiedBy":[{"name":"John"},{"name":"Alice"}], + "place":{"number":23} +} +*/ +``` diff --git a/1-js/05-data-types/12-json/article.md b/1-js/05-data-types/12-json/article.md new file mode 100644 index 0000000000..133ffb3532 --- /dev/null +++ b/1-js/05-data-types/12-json/article.md @@ -0,0 +1,530 @@ +# JSON methods, toJSON + +Let's say we have a complex object, and we'd like to convert it into a string, to send it over a network, or just to output it for logging purposes. + +Naturally, such a string should include all important properties. + +We could implement the conversion like this: + +```js run +let user = { + name: "John", + age: 30, + +*!* + toString() { + return `{name: "${this.name}", age: ${this.age}}`; + } +*/!* +}; + +alert(user); // {name: "John", age: 30} +``` + +...But in the process of development, new properties are added, old properties are renamed and removed. Updating such `toString` every time can become a pain. We could try to loop over properties in it, but what if the object is complex and has nested objects in properties? We'd need to implement their conversion as well. + +Luckily, there's no need to write the code to handle all this. The task has been solved already. + +## JSON.stringify + +The [JSON](https://en.wikipedia.org/wiki/JSON) (JavaScript Object Notation) is a general format to represent values and objects. It is described as in [RFC 4627](https://tools.ietf.org/html/rfc4627) standard. Initially it was made for JavaScript, but many other languages have libraries to handle it as well. So it's easy to use JSON for data exchange when the client uses JavaScript and the server is written on Ruby/PHP/Java/Whatever. + +JavaScript provides methods: + +- `JSON.stringify` to convert objects into JSON. +- `JSON.parse` to convert JSON back into an object. + +For instance, here we `JSON.stringify` a student: +```js run +let student = { + name: 'John', + age: 30, + isAdmin: false, + courses: ['html', 'css', 'js'], + spouse: null +}; + +*!* +let json = JSON.stringify(student); +*/!* + +alert(typeof json); // we've got a string! + +alert(json); +*!* +/* JSON-encoded object: +{ + "name": "John", + "age": 30, + "isAdmin": false, + "courses": ["html", "css", "js"], + "spouse": null +} +*/ +*/!* +``` + +The method `JSON.stringify(student)` takes the object and converts it into a string. + +The resulting `json` string is called a *JSON-encoded* or *serialized* or *stringified* or *marshalled* object. We are ready to send it over the wire or put into a plain data store. + + +Please note that a JSON-encoded object has several important differences from the object literal: + +- Strings use double quotes. No single quotes or backticks in JSON. So `'John'` becomes `"John"`. +- Object property names are double-quoted also. That's obligatory. So `age:30` becomes `"age":30`. + +`JSON.stringify` can be applied to primitives as well. + +JSON supports following data types: + +- Objects `{ ... }` +- Arrays `[ ... ]` +- Primitives: + - strings, + - numbers, + - boolean values `true/false`, + - `null`. + +For instance: + +```js run +// a number in JSON is just a number +alert( JSON.stringify(1) ) // 1 + +// a string in JSON is still a string, but double-quoted +alert( JSON.stringify('test') ) // "test" + +alert( JSON.stringify(true) ); // true + +alert( JSON.stringify([1, 2, 3]) ); // [1,2,3] +``` + +JSON is data-only language-independent specification, so some JavaScript-specific object properties are skipped by `JSON.stringify`. + +Namely: + +- Function properties (methods). +- Symbolic keys and values. +- Properties that store `undefined`. + +```js run +let user = { + sayHi() { // ignored + alert("Hello"); + }, + [Symbol("id")]: 123, // ignored + something: undefined // ignored +}; + +alert( JSON.stringify(user) ); // {} (empty object) +``` + +Usually that's fine. If that's not what we want, then soon we'll see how to customize the process. + +The great thing is that nested objects are supported and converted automatically. + +For instance: + +```js run +let meetup = { + title: "Conference", +*!* + room: { + number: 23, + participants: ["john", "ann"] + } +*/!* +}; + +alert( JSON.stringify(meetup) ); +/* The whole structure is stringified: +{ + "title":"Conference", + "room":{"number":23,"participants":["john","ann"]}, +} +*/ +``` + +The important limitation: there must be no circular references. + +For instance: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: ["john", "ann"] +}; + +meetup.place = room; // meetup references room +room.occupiedBy = meetup; // room references meetup + +*!* +JSON.stringify(meetup); // Error: Converting circular structure to JSON +*/!* +``` + +Here, the conversion fails, because of circular reference: `room.occupiedBy` references `meetup`, and `meetup.place` references `room`: + +![](json-meetup.svg) + + +## Excluding and transforming: replacer + +The full syntax of `JSON.stringify` is: + +```js +let json = JSON.stringify(value[, replacer, space]) +``` + +value +: A value to encode. + +replacer +: Array of properties to encode or a mapping function `function(key, value)`. + +space +: Amount of space to use for formatting + +Most of the time, `JSON.stringify` is used with the first argument only. But if we need to fine-tune the replacement process, like to filter out circular references, we can use the second argument of `JSON.stringify`. + +If we pass an array of properties to it, only these properties will be encoded. + +For instance: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: [{name: "John"}, {name: "Alice"}], + place: room // meetup references room +}; + +room.occupiedBy = meetup; // room references meetup + +alert( JSON.stringify(meetup, *!*['title', 'participants']*/!*) ); +// {"title":"Conference","participants":[{},{}]} +``` + +Here we are probably too strict. The property list is applied to the whole object structure. So the objects in `participants` are empty, because `name` is not in the list. + +Let's include in the list every property except `room.occupiedBy` that would cause the circular reference: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: [{name: "John"}, {name: "Alice"}], + place: room // meetup references room +}; + +room.occupiedBy = meetup; // room references meetup + +alert( JSON.stringify(meetup, *!*['title', 'participants', 'place', 'name', 'number']*/!*) ); +/* +{ + "title":"Conference", + "participants":[{"name":"John"},{"name":"Alice"}], + "place":{"number":23} +} +*/ +``` + +Now everything except `occupiedBy` is serialized. But the list of properties is quite long. + +Fortunately, we can use a function instead of an array as the `replacer`. + +The function will be called for every `(key, value)` pair and should return the "replaced" value, which will be used instead of the original one. Or `undefined` if the value is to be skipped. + +In our case, we can return `value` "as is" for everything except `occupiedBy`. To ignore `occupiedBy`, the code below returns `undefined`: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + participants: [{name: "John"}, {name: "Alice"}], + place: room // meetup references room +}; + +room.occupiedBy = meetup; // room references meetup + +alert( JSON.stringify(meetup, function replacer(key, value) { + alert(`${key}: ${value}`); + return (key == 'occupiedBy') ? undefined : value; +})); + +/* key:value pairs that come to replacer: +: [object Object] +title: Conference +participants: [object Object],[object Object] +0: [object Object] +name: John +1: [object Object] +name: Alice +place: [object Object] +number: 23 +occupiedBy: [object Object] +*/ +``` + +Please note that `replacer` function gets every key/value pair including nested objects and array items. It is applied recursively. The value of `this` inside `replacer` is the object that contains the current property. + +The first call is special. It is made using a special "wrapper object": `{"": meetup}`. In other words, the first `(key, value)` pair has an empty key, and the value is the target object as a whole. That's why the first line is `":[object Object]"` in the example above. + +The idea is to provide as much power for `replacer` as possible: it has a chance to analyze and replace/skip even the whole object if necessary. + + +## Formatting: space + +The third argument of `JSON.stringify(value, replacer, space)` is the number of spaces to use for pretty formatting. + +Previously, all stringified objects had no indents and extra spaces. That's fine if we want to send an object over a network. The `space` argument is used exclusively for a nice output. + +Here `space = 2` tells JavaScript to show nested objects on multiple lines, with indentation of 2 spaces inside an object: + +```js run +let user = { + name: "John", + age: 25, + roles: { + isAdmin: false, + isEditor: true + } +}; + +alert(JSON.stringify(user, null, 2)); +/* two-space indents: +{ + "name": "John", + "age": 25, + "roles": { + "isAdmin": false, + "isEditor": true + } +} +*/ + +/* for JSON.stringify(user, null, 4) the result would be more indented: +{ + "name": "John", + "age": 25, + "roles": { + "isAdmin": false, + "isEditor": true + } +} +*/ +``` + +The third argument can also be a string. In this case, the string is used for indentation instead of a number of spaces. + +The `space` parameter is used solely for logging and nice-output purposes. + +## Custom "toJSON" + +Like `toString` for string conversion, an object may provide method `toJSON` for to-JSON conversion. `JSON.stringify` automatically calls it if available. + +For instance: + +```js run +let room = { + number: 23 +}; + +let meetup = { + title: "Conference", + date: new Date(Date.UTC(2017, 0, 1)), + room +}; + +alert( JSON.stringify(meetup) ); +/* + { + "title":"Conference", +*!* + "date":"2017-01-01T00:00:00.000Z", // (1) +*/!* + "room": {"number":23} // (2) + } +*/ +``` + +Here we can see that `date` `(1)` became a string. That's because all dates have a built-in `toJSON` method which returns such kind of string. + +Now let's add a custom `toJSON` for our object `room` `(2)`: + +```js run +let room = { + number: 23, +*!* + toJSON() { + return this.number; + } +*/!* +}; + +let meetup = { + title: "Conference", + room +}; + +*!* +alert( JSON.stringify(room) ); // 23 +*/!* + +alert( JSON.stringify(meetup) ); +/* + { + "title":"Conference", +*!* + "room": 23 +*/!* + } +*/ +``` + +As we can see, `toJSON` is used both for the direct call `JSON.stringify(room)` and when `room` is nested in another encoded object. + + +## JSON.parse + +To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/parse). + +The syntax: +```js +let value = JSON.parse(str[, reviver]); +``` + +str +: JSON-string to parse. + +reviver +: Optional function(key,value) that will be called for each `(key, value)` pair and can transform the value. + +For instance: + +```js run +// stringified array +let numbers = "[0, 1, 2, 3]"; + +numbers = JSON.parse(numbers); + +alert( numbers[1] ); // 1 +``` + +Or for nested objects: + +```js run +let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; + +let user = JSON.parse(userData); + +alert( user.friends[1] ); // 1 +``` + +The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the same JSON format. + +Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging purposes): + +```js +let json = `{ + *!*name*/!*: "John", // mistake: property name without quotes + "surname": *!*'Smith'*/!*, // mistake: single quotes in value (must be double) + *!*'isAdmin'*/!*: false // mistake: single quotes in key (must be double) + "birthday": *!*new Date(2000, 2, 3)*/!*, // mistake: no "new" is allowed, only bare values + "friends": [0,1,2,3] // here all fine +}`; +``` + +Besides, JSON does not support comments. Adding a comment to JSON makes it invalid. + +There's another format named [JSON5](https://json5.org/), which allows unquoted keys, comments etc. But this is a standalone library, not in the specification of the language. + +The regular JSON is that strict not because its developers are lazy, but to allow easy, reliable and very fast implementations of the parsing algorithm. + +## Using reviver + +Imagine, we got a stringified `meetup` object from the server. + +It looks like this: + +```js +// title: (meetup title), date: (meetup date) +let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; +``` + +...And now we need to *deserialize* it, to turn back into JavaScript object. + +Let's do it by calling `JSON.parse`: + +```js run +let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; + +let meetup = JSON.parse(str); + +*!* +alert( meetup.date.getDate() ); // Error! +*/!* +``` + +Whoops! An error! + +The value of `meetup.date` is a string, not a `Date` object. How could `JSON.parse` know that it should transform that string into a `Date`? + +Let's pass to `JSON.parse` the reviving function as the second argument, that returns all values "as is", but `date` will become a `Date`: + +```js run +let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; + +*!* +let meetup = JSON.parse(str, function(key, value) { + if (key == 'date') return new Date(value); + return value; +}); +*/!* + +alert( meetup.date.getDate() ); // now works! +``` + +By the way, that works for nested objects as well: + +```js run +let schedule = `{ + "meetups": [ + {"title":"Conference","date":"2017-11-30T12:00:00.000Z"}, + {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} + ] +}`; + +schedule = JSON.parse(schedule, function(key, value) { + if (key == 'date') return new Date(value); + return value; +}); + +*!* +alert( schedule.meetups[1].date.getDate() ); // works! +*/!* +``` + + + +## Summary + +- JSON is a data format that has its own independent standard and libraries for most programming languages. +- JSON supports plain objects, arrays, strings, numbers, booleans, and `null`. +- JavaScript provides methods [JSON.stringify](mdn:js/JSON/stringify) to serialize into JSON and [JSON.parse](mdn:js/JSON/parse) to read from JSON. +- Both methods support transformer functions for smart reading/writing. +- If an object has `toJSON`, then it is called by `JSON.stringify`. diff --git a/1-js/05-data-types/12-json/json-meetup.svg b/1-js/05-data-types/12-json/json-meetup.svg new file mode 100644 index 0000000000..3fa32a261c --- /dev/null +++ b/1-js/05-data-types/12-json/json-meetup.svg @@ -0,0 +1 @@ +number: 23title: "Conference"...placeoccupiedByparticipants \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md index 237b9ef9e4..11667f940b 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 @@ -37,4 +37,4 @@ P.S. Naturally, the formula is the fastest solution. It uses only 3 operations f The loop variant is the second in terms of speed. In both the recursive and the loop variant we sum the same numbers. But the recursion involves nested calls and execution stack management. That also takes resources, so it's slower. -P.P.S. The standard describes a "tail call" optimization: if the recursive call is the very last one in the function (like in `sumTo` above), then the outer function will not need to resume the execution and we don't need to remember its execution context. In that case `sumTo(100000)` is countable. But if your JavaScript engine does not support it, there will be an error: maximum stack size exceeded, because there's usually a limitation on the total stack size. +P.P.S. Some engines support the "tail call" optimization: if a recursive call is the very last one in the function, with no other calculations performed, then the outer function will not need to resume the execution, so the engine doesn't need to remember its execution context. That removes the burden on memory. But if the JavaScript engine does not support tail call optimization (most of them don't), there will be an error: maximum stack size exceeded, because there's usually a limitation on the total stack size. diff --git a/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md b/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md index 59040a2b7a..09e511db5c 100644 --- a/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md +++ b/1-js/06-advanced-functions/01-recursion/02-factorial/solution.md @@ -1,4 +1,4 @@ -By definition, a factorial is `n!` can be written as `n * (n-1)!`. +By definition, a factorial `n!` can be written as `n * (n-1)!`. In other words, the result of `factorial(n)` can be calculated as `n` multiplied by the result of `factorial(n-1)`. And the call for `n-1` can recursively descend lower, and lower, till `1`. diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png deleted file mode 100644 index c45418ff4d..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg new file mode 100644 index 0000000000..1313837f30 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg @@ -0,0 +1 @@ +fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png deleted file mode 100644 index 6fc39ae130..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md index 91bcecc055..36524a45aa 100644 --- a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md @@ -29,7 +29,7 @@ Here we can see that the value of `fib(3)` is needed for both `fib(5)` and `fib( Here's the full recursion tree: -![fibonacci recursion tree](fibonacci-recursion-tree.png) +![fibonacci recursion tree](fibonacci-recursion-tree.svg) We can clearly notice that `fib(3)` is evaluated two times and `fib(2)` is evaluated three times. The total amount of computations grows much faster than `n`, making it enormous even for `n=77`. diff --git a/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md index 4e9de14692..cfcbffea57 100644 --- a/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md +++ b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md @@ -43,7 +43,7 @@ function printList(list) { } ``` -...But that would be unwise. In the future we may need to extend a function, do something else with the list. If we change `list`, then we loose such ability. +...But that would be unwise. In the future we may need to extend a function, do something else with the list. If we change `list`, then we lose such ability. Talking about good variable names, `list` here is the list itself. The first element of it. And it should remain like that. That's clear and reliable. diff --git a/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md index a9ba0baf58..0eb76ea1ca 100644 --- a/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md +++ b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md @@ -33,11 +33,11 @@ printReverseList(list); # Using a loop -The loop variant is also a little bit more complicated then the direct output. +The loop variant is also a little bit more complicated than the direct output. There is no way to get the last value in our `list`. We also can't "go back". -So what we can do is to first go through the items in the direct order and rememeber them in an array, and then output what we remembered in the reverse order: +So what we can do is to first go through the items in the direct order and remember them in an array, and then output what we remembered in the reverse order: ```js run let list = { diff --git a/1-js/06-advanced-functions/01-recursion/article.md b/1-js/06-advanced-functions/01-recursion/article.md index 036ae7f516..5ae8944742 100644 --- a/1-js/06-advanced-functions/01-recursion/article.md +++ b/1-js/06-advanced-functions/01-recursion/article.md @@ -61,7 +61,7 @@ When `pow(x, n)` is called, the execution splits into two branches: if n==1 = x / pow(x, n) = - \ + \ else = x * pow(x, n - 1) ``` @@ -70,7 +70,7 @@ pow(x, n) = We can also say that `pow` *recursively calls itself* till `n == 1`. -![recursive diagram of pow](recursion-pow.png) +![recursive diagram of pow](recursion-pow.svg) For example, to calculate `pow(2, 4)` the recursive variant does these steps: @@ -85,7 +85,7 @@ So, the recursion reduces a function call to a simpler one, and then -- to even ````smart header="Recursion is usually shorter" A recursive solution is usually shorter than an iterative one. -Here we can rewrite the same using the ternary `?` operator instead of `if` to make `pow(x, n)` more terse and still very readable: +Here we can rewrite the same using the conditional operator `?` instead of `if` to make `pow(x, n)` more terse and still very readable: ```js run function pow(x, n) { @@ -96,15 +96,15 @@ function pow(x, n) { The maximal number of nested calls (including the first one) is called *recursion depth*. In our case, it will be exactly `n`. -The maximal recursion depth is limited by JavaScript engine. We can make sure about 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases. +The maximal recursion depth is limited by JavaScript engine. We can rely on it being 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases. That limits the application of recursion, but it still remains very wide. There are many tasks where recursive way of thinking gives simpler code, easier to maintain. -## The execution stack +## The execution context and stack Now let's examine how recursive calls work. For that we'll look under the hood of functions. -The information about a function run is stored in its *execution context*. +The information about the process of execution of a running function is stored in its *execution context*. The [execution context](https://tc39.github.io/ecma262/#sec-execution-contexts) is an internal data structure that contains details about the execution of a function: where the control flow is now, the current variables, the value of `this` (we don't use it here) and few other internal details. @@ -132,7 +132,7 @@ We can sketch it as: -That's when the function starts to execute. The condition `n == 1` is false, so the flow continues into the second branch of `if`: +That's when the function starts to execute. The condition `n == 1` is falsy, so the flow continues into the second branch of `if`: ```js run function pow(x, n) { @@ -185,7 +185,13 @@ Here's the context stack when we entered the subcall `pow(2, 2)`: The new current execution context is on top (and bold), and previous remembered contexts are below. -When we finish the subcall -- it is easy to resume the previous context, because it keeps both variables and the exact place of the code where it stopped. Here in the picture we use the word "line", but of course it's more precise. +When we finish the subcall -- it is easy to resume the previous context, because it keeps both variables and the exact place of the code where it stopped. + +```smart +Here in the picture we use the word "line", as in our example there's only one subcall in line, but generally a single line of code may contain multiple subcalls, like `pow(…) + pow(…) + somethingElse(…)`. + +So it would be more precise to say that the execution resumes "immediately after the subcall". +``` ### pow(2, 1) @@ -279,7 +285,7 @@ The iterative `pow` uses a single context changing `i` and `result` in the proce **Any recursion can be rewritten as a loop. The loop variant usually can be made more effective.** -...But sometimes the rewrite is non-trivial, especially when function uses different recursive subcalls depending on conditions and merges their results or when the branching is more intricate. And the optimization may be unneeded and totally not worth the efforts. +...But sometimes the rewrite is non-trivial, especially when a function uses different recursive subcalls depending on conditions and merges their results or when the branching is more intricate. And the optimization may be unneeded and totally not worth the efforts. Recursion can give a shorter code, easier to understand and support. Optimizations are not required in every place, mostly we need a good code, that's why it's used. @@ -296,7 +302,7 @@ let company = { salary: 1000 }, { name: 'Alice', - salary: 600 + salary: 1600 }], development: { @@ -319,32 +325,32 @@ let company = { In other words, a company has departments. - A department may have an array of staff. For instance, `sales` department has 2 employees: John and Alice. -- Or a department may split into subdepartments, like `development` has two branches: `sites` and `internals`. Each of them has the own staff. +- Or a department may split into subdepartments, like `development` has two branches: `sites` and `internals`. Each of them has their own staff. - It is also possible that when a subdepartment grows, it divides into subsubdepartments (or teams). For instance, the `sites` department in the future may be split into teams for `siteA` and `siteB`. And they, potentially, can split even more. That's not on the picture, just something to have in mind. Now let's say we want a function to get the sum of all salaries. How can we do that? -An iterative approach is not easy, because the structure is not simple. The first idea may be to make a `for` loop over `company` with nested subloop over 1st level departments. But then we need more nested subloops to iterate over the staff in 2nd level departments like `sites`. ...And then another subloop inside those for 3rd level departments that might appear in the future? Should we stop on level 3 or make 4 levels of loops? If we put 3-4 nested subloops in the code to traverse a single object, it becomes rather ugly. +An iterative approach is not easy, because the structure is not simple. The first idea may be to make a `for` loop over `company` with nested subloop over 1st level departments. But then we need more nested subloops to iterate over the staff in 2nd level departments like `sites`... And then another subloop inside those for 3rd level departments that might appear in the future? If we put 3-4 nested subloops in the code to traverse a single object, it becomes rather ugly. Let's try recursion. As we can see, when our function gets a department to sum, there are two possible cases: -1. Either it's a "simple" department with an *array of people* -- then we can sum the salaries in a simple loop. -2. Or it's *an object with `N` subdepartments* -- then we can make `N` recursive calls to get the sum for each of the subdeps and combine the results. +1. Either it's a "simple" department with an *array* of people -- then we can sum the salaries in a simple loop. +2. Or it's *an object* with `N` subdepartments -- then we can make `N` recursive calls to get the sum for each of the subdeps and combine the results. -The (1) is the base of recursion, the trivial case. +The 1st case is the base of recursion, the trivial case, when we get an array. -The (2) is the recursive step. A complex task is split into subtasks for smaller departments. They may in turn split again, but sooner or later the split will finish at (1). +The 2nd case when we get an object is the recursive step. A complex task is split into subtasks for smaller departments. They may in turn split again, but sooner or later the split will finish at (1). The algorithm is probably even easier to read from the code: ```js run let company = { // the same object, compressed for brevity - sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 600 }], + sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }], development: { sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }], internals: [{name: 'Jack', salary: 1300}] @@ -366,14 +372,14 @@ function sumSalaries(department) { } */!* -alert(sumSalaries(company)); // 6700 +alert(sumSalaries(company)); // 7700 ``` The code is short and easy to understand (hopefully?). That's the power of recursion. It also works for any level of subdepartment nesting. Here's the diagram of calls: -![recursive salaries](recursive-salaries.png) +![recursive salaries](recursive-salaries.svg) We can easily see the principle: for an object `{...}` subcalls are made, while arrays `[...]` are the "leaves" of the recursion tree, they give immediate result. @@ -416,7 +422,7 @@ let arr = [obj1, obj2, obj3]; ...But there's a problem with arrays. The "delete element" and "insert element" operations are expensive. For instance, `arr.unshift(obj)` operation has to renumber all elements to make room for a new `obj`, and if the array is big, it takes time. Same with `arr.shift()`. -The only structural modifications that do not require mass-renumbering are those that operate with the end of array: `arr.push/pop`. So an array can be quite slow for big queues. +The only structural modifications that do not require mass-renumbering are those that operate with the end of array: `arr.push/pop`. So an array can be quite slow for big queues, when we have to work with the beginning. Alternatively, if we really need fast insertion/deletion, we can choose another data structure called a [linked list](https://en.wikipedia.org/wiki/Linked_list). @@ -444,7 +450,7 @@ let list = { Graphical representation of the list: -![linked list](linked-list.png) +![linked list](linked-list.svg) An alternative code for creation: @@ -453,9 +459,10 @@ 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; ``` -Here we can even more clearer see that there are multiple objects, each one has the `value` and `next` pointing to the neighbour. The `list` variable is the first object in the chain, so following `next` pointers from it we can reach any element. +Here we can even more clearly see that there are multiple objects, each one has the `value` and `next` pointing to the neighbour. The `list` variable is the first object in the chain, so following `next` pointers from it we can reach any element. The list can be easily split into multiple parts and later joined back: @@ -464,7 +471,7 @@ let secondList = list.next.next; list.next.next = null; ``` -![linked list split](linked-list-split.png) +![linked list split](linked-list-split.svg) To join: @@ -488,7 +495,7 @@ list = { value: "new item", next: list }; */!* ``` -![linked list](linked-list-0.png) +![linked list](linked-list-0.svg) To remove a value from the middle, change `next` of the previous one: @@ -496,7 +503,7 @@ To remove a value from the middle, change `next` of the previous one: list.next = list.next.next; ``` -![linked list](linked-list-remove-1.png) +![linked list](linked-list-remove-1.svg) We made `list.next` jump over `1` to value `2`. The value `1` is now excluded from the chain. If it's not stored anywhere else, it will be automatically removed from the memory. @@ -506,14 +513,17 @@ Naturally, lists are not always better than arrays. Otherwise everyone would use The main drawback is that we can't easily access an element by its number. In an array that's easy: `arr[n]` is a direct reference. But in the list we need to start from the first item and go `next` `N` times to get the Nth element. -...But we don't always need such operations. For instance, when we need a queue or even a [deque](https://en.wikipedia.org/wiki/Double-ended_queue) -- the ordered structure that must allow very fast adding/removing elements from both ends. +...But we don't always need such operations. For instance, when we need a queue or even a [deque](https://en.wikipedia.org/wiki/Double-ended_queue) -- the ordered structure that must allow very fast adding/removing elements from both ends, but access to its middle is not needed. -Sometimes it's worth to add another variable named `tail` to track the last element of the list (and update it when adding/removing elements from the end). For large sets of elements the speed difference versus arrays is huge. +Lists can be enhanced: +- We can add property `prev` in addition to `next` to reference the previous element, to move back easily. +- We can also add a variable named `tail` referencing the last element of the list (and update it when adding/removing elements from the end). +- ...The data structure may vary according to our needs. ## Summary Terms: -- *Recursion* is a programming term that means a "self-calling" function. Such functions can be used to solve certain tasks in elegant ways. +- *Recursion* is a programming term that means calling a function from itself. Recursive functions can be used to solve tasks in elegant ways. When a function calls itself, that's called a *recursion step*. The *basis* of recursion is function arguments that make the task so simple that the function does not make further calls. @@ -525,7 +535,7 @@ Terms: list = { value, next -> list } ``` - Trees like HTML elements tree or the department tree from this chapter are also naturally recursive: they branch and every branch can have other branches. + Trees like HTML elements tree or the department tree from this chapter are also naturally recursive: they have branches and every branch can have other branches. Recursive functions can be used to walk them as we've seen in the `sumSalary` example. diff --git a/1-js/06-advanced-functions/01-recursion/head.html b/1-js/06-advanced-functions/01-recursion/head.html index c2ac3e8cee..bb7fe8772f 100644 --- a/1-js/06-advanced-functions/01-recursion/head.html +++ b/1-js/06-advanced-functions/01-recursion/head.html @@ -7,7 +7,7 @@ .function-execution-context { border: 1px solid black; - font-family: "Consolas", monospace; + font-family: 'DejaVu Sans Mono', 'Lucida Console', 'Menlo', 'Monaco', monospace; padding: 4px 6px; margin: 0 4px; } @@ -23,4 +23,4 @@ .function-execution-context-list li:first-child { font-weight: bold; } - \ No newline at end of file + diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0.png b/1-js/06-advanced-functions/01-recursion/linked-list-0.png deleted file mode 100644 index 000a80da8c..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-0.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0.svg b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg new file mode 100644 index 0000000000..5d23c7a4c6 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg @@ -0,0 +1 @@ +value1nextvalue"new item"nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png deleted file mode 100644 index 5a2368694f..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png deleted file mode 100644 index 477989ad8a..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg new file mode 100644 index 0000000000..2f37449c47 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg @@ -0,0 +1 @@ +value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png deleted file mode 100644 index 41de7661c4..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.png b/1-js/06-advanced-functions/01-recursion/linked-list-split.png deleted file mode 100644 index ac22034905..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-split.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.svg b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg new file mode 100644 index 0000000000..6c30721307 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg @@ -0,0 +1 @@ +value1nextvalue2nextvalue3nextvalue4nextnullnullsecondListlist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png deleted file mode 100644 index 201c66f150..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.png b/1-js/06-advanced-functions/01-recursion/linked-list.png deleted file mode 100644 index 64b6fb2b72..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.svg b/1-js/06-advanced-functions/01-recursion/linked-list.svg new file mode 100644 index 0000000000..c02744f390 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list.svg @@ -0,0 +1 @@ +value1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list@2x.png deleted file mode 100644 index c28fa82594..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.png b/1-js/06-advanced-functions/01-recursion/recursion-pow.png deleted file mode 100644 index 30577f89c2..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursion-pow.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg new file mode 100644 index 0000000000..2b970a04a9 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg @@ -0,0 +1 @@ +pow(x,n)xx * pow(x, n-1)n == 1 ?YesNorecursive call until n==1 \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png b/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png deleted file mode 100644 index c19973420d..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.png b/1-js/06-advanced-functions/01-recursion/recursive-salaries.png deleted file mode 100644 index 2b90154090..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursive-salaries.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg new file mode 100644 index 0000000000..bd874c5ba6 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png b/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png deleted file mode 100644 index 261ab144ed..0000000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png and /dev/null differ 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 deleted file mode 100644 index c2ca81681b..0000000000 --- a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md +++ /dev/null @@ -1,243 +0,0 @@ -# Rest parameters and spread operator - -Many JavaScript built-in functions support an arbitrary number of arguments. - -For instance: - -- `Math.max(arg1, arg2, ..., argN)` -- returns the greatest of the arguments. -- `Object.assign(dest, src1, ..., srcN)` -- copies properties from `src1..N` into `dest`. -- ...and so on. - -In this chapter we'll learn how to do the same. And, more importantly, how to feel comfortable working with such functions and arrays. - -## Rest parameters `...` - -A function can be called with any number of arguments, no matter how it is defined. - -Like here: -```js run -function sum(a, b) { - return a + b; -} - -alert( sum(1, 2, 3, 4, 5) ); -``` - -There will be no error because of "excessive" arguments. But of course in the result only the first two will be counted. - -The rest parameters can be mentioned in a function definition with three dots `...`. They literally mean "gather the remaining parameters into an array". - -For instance, to gather all arguments into array `args`: - -```js run -function sumAll(...args) { // args is the name for the array - let sum = 0; - - for (let arg of args) sum += arg; - - return sum; -} - -alert( sumAll(1) ); // 1 -alert( sumAll(1, 2) ); // 3 -alert( sumAll(1, 2, 3) ); // 6 -``` - -We can choose to get the first parameters as variables, and gather only the rest. - -Here the first two arguments go into variables and the rest go into `titles` array: - -```js run -function showName(firstName, lastName, ...titles) { - alert( firstName + ' ' + lastName ); // Julius Caesar - - // the rest go into titles array - // i.e. titles = ["Consul", "Imperator"] - alert( titles[0] ); // Consul - alert( titles[1] ); // Imperator - alert( titles.length ); // 2 -} - -showName("Julius", "Caesar", "Consul", "Imperator"); -``` - -````warn header="The rest parameters must be at the end" -The rest parameters gather all remaining arguments, so the following has no sense: - -```js -function f(arg1, ...rest, arg2) { // arg2 after ...rest ?! - // error -} -``` - -The `...rest` must always be last. -```` - -## The "arguments" variable - -There is also a special array-like object named `arguments` that contains all arguments by their index. - -For instance: - -```js run -function showName() { - alert( arguments.length ); - alert( arguments[0] ); - alert( arguments[1] ); - - // it's iterable - // for(let arg of arguments) alert(arg); -} - -// shows: 2, Julius, Caesar -showName("Julius", "Caesar"); - -// shows: 1, Ilya, undefined (no second argument) -showName("Ilya"); -``` - -In old times, rest parameters did not exist in the language, and using `arguments` was the only way to get all arguments of the function, no matter their total number. - -And it still works, we can use it today. - -But the downside is that although `arguments` is both array-like and iterable, it's not an array. It does not support array methods, so we can't call `arguments.map(...)` for example. - -Also, it always contains all arguments. We can't capture them partially, like we did with rest parameters. - -So when we need these features, then rest parameters are preferred. - -````smart header="Arrow functions do not have `\"arguments\"`" -If we access the `arguments` object from an arrow function, it takes them from the outer "normal" function. - -Here's an example: - -```js run -function f() { - let showArg = () => alert(arguments[0]); - showArg(); -} - -f(1); // 1 -``` -As we remember, arrow functions don't have their own `this`. Now we know they don't have the special `arguments` object either. - -## Spread operator [#spread-operator] - -We've just seen how to get an array from the list of parameters. - -But sometimes we need to do exactly the reverse. - -For instance, there's a built-in function [Math.max](mdn:js/Math/max) that returns the greatest number from a list: - -```js run -alert( Math.max(3, 5, 1) ); // 5 -``` - -Now let's say we have an array `[3, 5, 1]`. How do we call `Math.max` with it? - -Passing it "as is" won't work, because `Math.max` expects a list of numeric arguments, not a single array: - -```js run -let arr = [3, 5, 1]; - -*!* -alert( Math.max(arr) ); // NaN -*/!* -``` - -And surely we can't manually list items in the code `Math.max(arr[0], arr[1], arr[2])`, because we may be unsure how many there are. As our script executes, there could be a lot, or there could be none. And that would get ugly. - -*Spread operator* to the rescue! It looks similar to rest parameters, also using `...`, but does quite the opposite. - -When `...arr` is used in the function call, it "expands" an iterable object `arr` into the list of arguments. - -For `Math.max`: - -```js run -let arr = [3, 5, 1]; - -alert( Math.max(...arr) ); // 5 (spread turns array into a list of arguments) -``` - -We also can pass multiple iterables this way: - -```js run -let arr1 = [1, -2, 3, 4]; -let arr2 = [8, 3, -8, 1]; - -alert( Math.max(...arr1, ...arr2) ); // 8 -``` - -We can even combine the spread operator with normal values: - - -```js run -let arr1 = [1, -2, 3, 4]; -let arr2 = [8, 3, -8, 1]; - -alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 -``` - -Also, the spread operator can be used to merge arrays: - -```js run -let arr = [3, 5, 1]; -let arr2 = [8, 9, 15]; - -*!* -let merged = [0, ...arr, 2, ...arr2]; -*/!* - -alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2) -``` - -In the examples above we used an array to demonstrate the spread operator, but any iterable will do. - -For instance, here we use the spread operator to turn the string into array of characters: - -```js run -let str = "Hello"; - -alert( [...str] ); // H,e,l,l,o -``` - -The spread operator internally uses iterators to gather elements, the same way as `for..of` does. - -So, for a string, `for..of` returns characters and `...str` becomes `"H","e","l","l","o"`. The list of characters is passed to array initializer `[...str]`. - -For this particular task we could also use `Array.from`, because it converts an iterable (like a string) into an array: - -```js run -let str = "Hello"; - -// Array.from converts an iterable into an array -alert( Array.from(str) ); // H,e,l,l,o -``` - -The result is the same as `[...str]`. - -But there's a subtle difference between `Array.from(obj)` and `[...obj]`: - -- `Array.from` operates on both array-likes and iterables. -- The spread operator operates only on iterables. - -So, for the task of turning something into an array, `Array.from` tends to be more universal. - - -## Summary - -When we see `"..."` in the code, it is either rest parameters or the spread operator. - -There's an easy way to distinguish between them: - -- When `...` is at the end of function parameters, it's "rest parameters" and gathers the rest of the list of arguments into an array. -- When `...` occurs in a function call or alike, it's called a "spread operator" and expands an array into a list. - -Use patterns: - -- Rest parameters are used to create functions that accept any number of arguments. -- The spread operator is used to pass an array to functions that normally require a list of many arguments. - -Together they help to travel between a list and an array of parameters with ease. - -All arguments of a function call are also available in "old-style" `arguments`: array-like iterable object. diff --git a/1-js/06-advanced-functions/02-rest-parameters-spread/article.md b/1-js/06-advanced-functions/02-rest-parameters-spread/article.md new file mode 100644 index 0000000000..dbdfbd6c0d --- /dev/null +++ b/1-js/06-advanced-functions/02-rest-parameters-spread/article.md @@ -0,0 +1,295 @@ +# Rest parameters and spread syntax + +Many JavaScript built-in functions support an arbitrary number of arguments. + +For instance: + +- `Math.max(arg1, arg2, ..., argN)` -- returns the greatest of the arguments. +- `Object.assign(dest, src1, ..., srcN)` -- copies properties from `src1..N` into `dest`. +- ...and so on. + +In this chapter we'll learn how to do the same. And also, how to pass arrays to such functions as parameters. + +## Rest parameters `...` + +A function can be called with any number of arguments, no matter how it is defined. + +Like here: +```js run +function sum(a, b) { + return a + b; +} + +alert( sum(1, 2, 3, 4, 5) ); +``` + +There will be no error because of "excessive" arguments. But of course in the result only the first two will be counted, so the result in the code above is `3`. + +The rest of the parameters can be included in the function definition by using three dots `...` followed by the name of the array that will contain them. The dots literally mean "gather the remaining parameters into an array". + +For instance, to gather all arguments into array `args`: + +```js run +function sumAll(...args) { // args is the name for the array + let sum = 0; + + for (let arg of args) sum += arg; + + return sum; +} + +alert( sumAll(1) ); // 1 +alert( sumAll(1, 2) ); // 3 +alert( sumAll(1, 2, 3) ); // 6 +``` + +We can choose to get the first parameters as variables, and gather only the rest. + +Here the first two arguments go into variables and the rest go into `titles` array: + +```js run +function showName(firstName, lastName, ...titles) { + alert( firstName + ' ' + lastName ); // Julius Caesar + + // the rest go into titles array + // i.e. titles = ["Consul", "Imperator"] + alert( titles[0] ); // Consul + alert( titles[1] ); // Imperator + alert( titles.length ); // 2 +} + +showName("Julius", "Caesar", "Consul", "Imperator"); +``` + +````warn header="The rest parameters must be at the end" +The rest parameters gather all remaining arguments, so the following does not make sense and causes an error: + +```js +function f(arg1, ...rest, arg2) { // arg2 after ...rest ?! + // error +} +``` + +The `...rest` must always be last. +```` + +## The "arguments" variable + +There is also a special array-like object named `arguments` that contains all arguments by their index. + +For instance: + +```js run +function showName() { + alert( arguments.length ); + alert( arguments[0] ); + alert( arguments[1] ); + + // it's iterable + // for(let arg of arguments) alert(arg); +} + +// shows: 2, Julius, Caesar +showName("Julius", "Caesar"); + +// shows: 1, Ilya, undefined (no second argument) +showName("Ilya"); +``` + +In old times, rest parameters did not exist in the language, and using `arguments` was the only way to get all arguments of the function. And it still works, we can find it in the old code. + +But the downside is that although `arguments` is both array-like and iterable, it's not an array. It does not support array methods, so we can't call `arguments.map(...)` for example. + +Also, it always contains all arguments. We can't capture them partially, like we did with rest parameters. + +So when we need these features, then rest parameters are preferred. + +````smart header="Arrow functions do not have `\"arguments\"`" +If we access the `arguments` object from an arrow function, it takes them from the outer "normal" function. + +Here's an example: + +```js run +function f() { + let showArg = () => alert(arguments[0]); + showArg(); +} + +f(1); // 1 +``` + +As we remember, arrow functions don't have their own `this`. Now we know they don't have the special `arguments` object either. +```` + + +## Spread syntax [#spread-syntax] + +We've just seen how to get an array from the list of parameters. + +But sometimes we need to do exactly the reverse. + +For instance, there's a built-in function [Math.max](mdn:js/Math/max) that returns the greatest number from a list: + +```js run +alert( Math.max(3, 5, 1) ); // 5 +``` + +Now let's say we have an array `[3, 5, 1]`. How do we call `Math.max` with it? + +Passing it "as is" won't work, because `Math.max` expects a list of numeric arguments, not a single array: + +```js run +let arr = [3, 5, 1]; + +*!* +alert( Math.max(arr) ); // NaN +*/!* +``` + +And surely we can't manually list items in the code `Math.max(arr[0], arr[1], arr[2])`, because we may be unsure how many there are. As our script executes, there could be a lot, or there could be none. And that would get ugly. + +*Spread syntax* to the rescue! It looks similar to rest parameters, also using `...`, but does quite the opposite. + +When `...arr` is used in the function call, it "expands" an iterable object `arr` into the list of arguments. + +For `Math.max`: + +```js run +let arr = [3, 5, 1]; + +alert( Math.max(...arr) ); // 5 (spread turns array into a list of arguments) +``` + +We also can pass multiple iterables this way: + +```js run +let arr1 = [1, -2, 3, 4]; +let arr2 = [8, 3, -8, 1]; + +alert( Math.max(...arr1, ...arr2) ); // 8 +``` + +We can even combine the spread syntax with normal values: + + +```js run +let arr1 = [1, -2, 3, 4]; +let arr2 = [8, 3, -8, 1]; + +alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 +``` + +Also, the spread syntax can be used to merge arrays: + +```js run +let arr = [3, 5, 1]; +let arr2 = [8, 9, 15]; + +*!* +let merged = [0, ...arr, 2, ...arr2]; +*/!* + +alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2) +``` + +In the examples above we used an array to demonstrate the spread syntax, but any iterable will do. + +For instance, here we use the spread syntax to turn the string into array of characters: + +```js run +let str = "Hello"; + +alert( [...str] ); // H,e,l,l,o +``` + +The spread syntax internally uses iterators to gather elements, the same way as `for..of` does. + +So, for a string, `for..of` returns characters and `...str` becomes `"H","e","l","l","o"`. The list of characters is passed to array initializer `[...str]`. + +For this particular task we could also use `Array.from`, because it converts an iterable (like a string) into an array: + +```js run +let str = "Hello"; + +// Array.from converts an iterable into an array +alert( Array.from(str) ); // H,e,l,l,o +``` + +The result is the same as `[...str]`. + +But there's a subtle difference between `Array.from(obj)` and `[...obj]`: + +- `Array.from` operates on both array-likes and iterables. +- The spread syntax works only with iterables. + +So, for the task of turning something into an array, `Array.from` tends to be more universal. + + +## Copy an array/object + +Remember when we talked about `Object.assign()` [in the past](info:object-copy#cloning-and-merging-object-assign)? + +It is possible to do the same thing with the spread syntax. + +```js run +let arr = [1, 2, 3]; + +*!* +let arrCopy = [...arr]; // spread the array into a list of parameters + // then put the result into a new array +*/!* + +// do the arrays have the same contents? +alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true + +// are the arrays equal? +alert(arr === arrCopy); // false (not same reference) + +// modifying our initial array does not modify the copy: +arr.push(4); +alert(arr); // 1, 2, 3, 4 +alert(arrCopy); // 1, 2, 3 +``` + +Note that it is possible to do the same thing to make a copy of an object: + +```js run +let obj = { a: 1, b: 2, c: 3 }; + +*!* +let objCopy = { ...obj }; // spread the object into a list of parameters + // then return the result in a new object +*/!* + +// do the objects have the same contents? +alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true + +// are the objects equal? +alert(obj === objCopy); // false (not same reference) + +// modifying our initial object does not modify the copy: +obj.d = 4; +alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4} +alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3} +``` + +This way of copying an object is much shorter than `let objCopy = Object.assign({}, obj)` or for an array `let arrCopy = Object.assign([], arr)` so we prefer to use it whenever we can. + + +## Summary + +When we see `"..."` in the code, it is either rest parameters or the spread syntax. + +There's an easy way to distinguish between them: + +- When `...` is at the end of function parameters, it's "rest parameters" and gathers the rest of the list of arguments into an array. +- When `...` occurs in a function call or alike, it's called a "spread syntax" and expands an array into a list. + +Use patterns: + +- Rest parameters are used to create functions that accept any number of arguments. +- The spread syntax is used to pass an array to functions that normally require a list of many arguments. + +Together they help to travel between a list and an array of parameters with ease. + +All arguments of a function call are also available in "old-style" `arguments`: array-like iterable object. 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..7cbd85ab75 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md @@ -0,0 +1,5 @@ +The answer is: **Pete**. + +A function gets outer variables as they are now, it uses the most recent values. + +Old variable values are not saved anywhere. When a function wants a variable, it takes the current value from its own Lexical Environment or the outer one. 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..819189773c --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Does a function pickup latest changes? + +The function sayHi uses an external variable name. When the function runs, which value is it going to use? + +```js +let name = "John"; + +function sayHi() { + alert("Hi, " + name); +} + +name = "Pete"; + +sayHi(); // what will it show: "John" or "Pete"? +``` + +Such situations are common both in browser and server-side development. A function may be scheduled to execute later than it is created, for instance after a user action or a network request. + +So, the question is: does it pick up the latest changes? 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..f8c7bd6ac4 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-empty.svg @@ -0,0 +1 @@ +outer<empty>makeArmy() LexicalEnvironmentwhile iteration LexicalEnvironment<empty><empty><empty>i: 10 \ No newline at end of file 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..7611d0ef80 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy-for-fixed.svg @@ -0,0 +1 @@ +outermakeArmy() LexicalEnvironmentfor iteration LexicalEnvironmenti: 0i: 1i: 2i: 10... \ No newline at end of file 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..d83ecbe76f --- /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() LexicalEnvironmentwhile iteration LexicalEnvironment \ No newline at end of file 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..9d99aa7178 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/solution.md @@ -0,0 +1,129 @@ + +Let's examine what exactly happens inside `makeArmy`, and the solution will become obvious. + +1. It creates an empty array `shooters`: + + ```js + let shooters = []; + ``` +2. Fills it with functions via `shooters.push(function)` in the loop. + + Every element is a function, so the resulting array looks like this: + + ```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. The array is returned from the function. + + Then, later, the call to any member, e.g. `army[5]()` will get the element `army[5]` from the array (which is a function) and calls it. + + Now why do all such functions show the same value, `10`? + + That's because there's no local variable `i` inside `shooter` functions. When such a function is called, it takes `i` from its outer lexical environment. + + Then, what will be the value of `i`? + + If we look at the source: + + ```js + function makeArmy() { + ... + let i = 0; + while (i < 10) { + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + shooters.push(shooter); // add function to the array + i++; + } + ... + } + ``` + + We can see that all `shooter` functions are created in the lexical environment of `makeArmy()` function. But when `army[5]()` is called, `makeArmy` has already finished its job, and the final value of `i` is `10` (`while` stops at `i=10`). + + As the result, all `shooter` functions get the same value from the outer lexical environment and that is, the last value, `i=10`. + + ![](lexenv-makearmy-empty.svg) + + As you can see above, on each iteration of a `while {...}` block, a new lexical environment is created. So, to fix this, we can copy the value of `i` into a variable within the `while {...}` block, like this: + + ```js run + function makeArmy() { + let shooters = []; + + let i = 0; + while (i < 10) { + *!* + let j = i; + */!* + let shooter = function() { // shooter function + alert( *!*j*/!* ); // should show its number + }; + shooters.push(shooter); + i++; + } + + return shooters; + } + + let army = makeArmy(); + + // Now the code works correctly + army[0](); // 0 + army[5](); // 5 + ``` + + Here `let j = i` declares an "iteration-local" variable `j` and copies `i` into it. Primitives are copied "by value", so we actually get an independent copy of `i`, belonging to the current loop iteration. + + The shooters work correctly, because the value of `i` now lives a little bit closer. Not in `makeArmy()` Lexical Environment, but in the Lexical Environment that corresponds to the current loop iteration: + + ![](lexenv-makearmy-while-fixed.svg) + + Such a problem could also be avoided if we used `for` in the beginning, like this: + + ```js run demo + function makeArmy() { + + let shooters = []; + + *!* + for(let i = 0; i < 10; i++) { + */!* + let shooter = function() { // shooter function + alert( i ); // should show its number + }; + shooters.push(shooter); + } + + return shooters; + } + + let army = makeArmy(); + + army[0](); // 0 + army[5](); // 5 + ``` + + That's essentially the same, because `for` on each iteration generates a new lexical environment, with its own variable `i`. So `shooter` generated in every iteration references its own `i`, from that very iteration. + + ![](lexenv-makearmy-for-fixed.svg) + +Now, as you've put so much effort into reading this, and the final recipe is so simple - just use `for`, you may wonder -- was it worth that? + +Well, if you could easily answer the question, you wouldn't read the solution. So, hopefully this task must have helped you to understand things a bit better. + +Besides, there are indeed cases when one prefers `while` to `for`, and other scenarios, where such problems are real. + diff --git a/1-js/06-advanced-functions/03-closure/10-make-army/task.md b/1-js/06-advanced-functions/03-closure/10-make-army/task.md new file mode 100644 index 0000000000..f50c7dc209 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/10-make-army/task.md @@ -0,0 +1,41 @@ +importance: 5 + +--- + +# Army of functions + +The following code creates an array of `shooters`. + +Every function is meant to output its number. But something is wrong... + +```js run +function makeArmy() { + let shooters = []; + + let i = 0; + while (i < 10) { + let shooter = function() { // create a shooter function, + alert( i ); // that should show its number + }; + shooters.push(shooter); // and add it to the array + i++; + } + + // ...and return the array of shooters + return shooters; +} + +let army = makeArmy(); + +*!* +// all shooters show 10 instead of their numbers 0, 1, 2, 3... +army[0](); // 10 from the shooter number 0 +army[1](); // 10 from the shooter number 1 +army[2](); // 10 ...and so on. +*/!* +``` + +Why do all of the shooters show the same value? + +Fix the code so that they work as intended. + 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..8dfd8bd638 --- /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"<empty>outerouterouternullname: "Pete" \ No newline at end of file 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..0a522132fe --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md @@ -0,0 +1,9 @@ +The answer is: **Pete**. + +The `work()` function in the code below gets `name` from the place of its origin through the outer lexical environment reference: + +![](lexenv-nested-work.svg) + +So, the result is `"Pete"` here. + +But if there were no `let name` in `makeWorker()`, then the search would go outside and take the global variable as we can see from the chain above. In that case the result would be `"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..d12a385c81 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# Which variables are available? + +The function `makeWorker` below makes another function and returns it. That new function can be called from somewhere else. + +Will it have access to the outer variables from its creation place, or the invocation place, or both? + +```js +function makeWorker() { + let name = "Pete"; + + return function() { + alert(name); + }; +} + +let name = "John"; + +// create a function +let work = makeWorker(); + +// call it +work(); // what will it show? +``` + +Which value it will show? "Pete" or "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/3-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md deleted file mode 100644 index d0dbbeb111..0000000000 --- a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md +++ /dev/null @@ -1,20 +0,0 @@ - -# Function in if - -Look at the code. What will be result of the call at the last line? - -```js run -let phrase = "Hello"; - -if (true) { - let user = "John"; - - function sayHi() { - alert(`${phrase}, ${user}`); - } -} - -*!* -sayHi(); -*/!* -``` diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md b/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md deleted file mode 100644 index e8c8c465c2..0000000000 --- a/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md +++ /dev/null @@ -1,17 +0,0 @@ -For the second brackets to work, the first ones must return a function. - -Like this: - -```js run -function sum(a) { - - return function(b) { - return a + b; // takes "a" from the outer lexical environment - }; - -} - -alert( sum(1)(2) ); // 3 -alert( sum(5)(-1) ); // 4 -``` - diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md b/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md deleted file mode 100644 index c2f3eabeb3..0000000000 --- a/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md +++ /dev/null @@ -1,17 +0,0 @@ -importance: 4 - ---- - -# Sum with closures - -Write function `sum` that works like this: `sum(a)(b) = a+b`. - -Yes, exactly this way, via double brackets (not a mistype). - -For instance: - -```js -sum(1)(2) = 3 -sum(5)(-1) = 4 -``` - 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/5-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md new file mode 100644 index 0000000000..4e386eec5e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md @@ -0,0 +1,22 @@ +importance: 5 + +--- +# Function in if + +Look at the code. What will be the result of the call at the last line? + +```js run +let phrase = "Hello"; + +if (true) { + let user = "John"; + + function sayHi() { + alert(`${phrase}, ${user}`); + } +} + +*!* +sayHi(); +*/!* +``` diff --git a/1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md new file mode 100644 index 0000000000..a6679cd209 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md @@ -0,0 +1,17 @@ +For the second parentheses to work, the first ones must return a function. + +Like this: + +```js run +function sum(a) { + + return function(b) { + return a + b; // takes "a" from the outer lexical environment + }; + +} + +alert( sum(1)(2) ); // 3 +alert( sum(5)(-1) ); // 4 +``` + diff --git a/1-js/06-advanced-functions/03-closure/6-closure-sum/task.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/task.md new file mode 100644 index 0000000000..b45758562e --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/6-closure-sum/task.md @@ -0,0 +1,17 @@ +importance: 4 + +--- + +# Sum with closures + +Write function `sum` that works like this: `sum(a)(b) = a+b`. + +Yes, exactly this way, using double parentheses (not a mistype). + +For instance: + +```js +sum(1)(2) = 3 +sum(5)(-1) = 4 +``` + diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md b/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md deleted file mode 100644 index 5bbc33b025..0000000000 --- a/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md +++ /dev/null @@ -1,26 +0,0 @@ - -# Filter inBetween - -```js run -function inBetween(a, b) { - return function(x) { - return x >= a && x <= b; - }; -} - -let arr = [1, 2, 3, 4, 5, 6, 7]; -alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 -``` - -# Filter inArray - -```js run -function inArray(arr) { - return function(x) { - return arr.includes(x); - }; -} - -let arr = [1, 2, 3, 4, 5, 6, 7]; -alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 -``` 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..b16b352902 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md @@ -0,0 +1,40 @@ +The result is: **error**. + +Try running it: + +```js run +let x = 1; + +function func() { +*!* + console.log(x); // ReferenceError: Cannot access 'x' before initialization +*/!* + let x = 2; +} + +func(); +``` + +In this example we can observe the peculiar difference between a "non-existing" and "uninitialized" variable. + +As you may have read in the article [](info:closure), a variable starts in the "uninitialized" state from the moment when the execution enters a code block (or a function). And it stays uninitalized until the corresponding `let` statement. + +In other words, a variable technically exists, but can't be used before `let`. + +The code above demonstrates it. + +```js +function func() { +*!* + // the local variable x is known to the engine from the beginning of the function, + // but "uninitialized" (unusable) until let ("dead zone") + // hence the error +*/!* + + console.log(x); // ReferenceError: Cannot access 'x' before initialization + + let x = 2; +} +``` + +This zone of temporary unusability of a variable (from the beginning of the code block till `let`) is sometimes called the "dead zone". 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..fb7445e66a --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-let-scope/task.md @@ -0,0 +1,21 @@ +importance: 4 + +--- + +# Is variable visible? + +What will be the result of this code? + +```js +let x = 1; + +function func() { + console.log(x); // ? + + let x = 2; +} + +func(); +``` + +P.S. There's a pitfall in this task. The solution is not obvious. 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/8-filter-through-function/solution.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md new file mode 100644 index 0000000000..46c5514a8d --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md @@ -0,0 +1,26 @@ + +# Filter inBetween + +```js run +function inBetween(a, b) { + return function(x) { + return x >= a && x <= b; + }; +} + +let arr = [1, 2, 3, 4, 5, 6, 7]; +alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 +``` + +# Filter inArray + +```js run demo +function inArray(arr) { + return function(x) { + return arr.includes(x); + }; +} + +let arr = [1, 2, 3, 4, 5, 6, 7]; +alert( arr.filter(inArray([1, 2, 10])) ); // 1,2 +``` 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.png b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png deleted file mode 100644 index d51e8167f8..0000000000 Binary files a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png and /dev/null differ diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png deleted file mode 100644 index e70edbd6d4..0000000000 Binary files a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png and /dev/null differ 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 03c34b0754..0000000000 --- a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md +++ /dev/null @@ -1,121 +0,0 @@ - -Let's examine what's done inside `makeArmy`, and the solution will become obvious. - -1. It creates an empty array `shooters`: - - ```js - let shooters = []; - ``` -2. Fills it in the loop via `shooters.push(function...)`. - - Every element is a function, so the resulting array looks like this: - - ```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. The array is returned from the function. - -Then, later, the call to `army[5]()` will get the element `army[5]` from the array (it will be a function) and call it. - -Now why all such functions show the same? - -That's because there's no local variable `i` inside `shooter` functions. When such a function is called, it takes `i` from its outer lexical environment. - -What will be the value of `i`? - -If we look at the source: - -```js -function makeArmy() { - ... - let i = 0; - while (i < 10) { - let shooter = function() { // shooter function - alert( i ); // should show its number - }; - ... - } - ... -} -``` - -...We can see that it lives in the lexical environment associated with the current `makeArmy()` run. But when `army[5]()` is called, `makeArmy` has already finished its job, and `i` has the last value: `10` (the end of `while`). - -As a result, all `shooter` functions get from the outer lexical envrironment the same, last value `i=10`. - -The fix can be very simple: - -```js run -function makeArmy() { - - let shooters = []; - -*!* - for(let i = 0; i < 10; i++) { -*/!* - let shooter = function() { // shooter function - alert( i ); // should show its number - }; - shooters.push(shooter); - } - - return shooters; -} - -let army = makeArmy(); - -army[0](); // 0 -army[5](); // 5 -``` - -Now it works correctly, because every time the code block in `for (..) {...}` is executed, a new Lexical Environment is created for it, with the corresponding value of `i`. - -So, the value of `i` now lives a little bit closer. Not in `makeArmy()` Lexical Environment, but in the Lexical Environment that corresponds the current loop iteration. A `shooter` gets the value exactly from the one where it was created. - -![](lexenv-makearmy.png) - -Here we rewrote `while` into `for`. - -Another trick could be possible, let's see it for better understanding of the subject: - - -```js run -function makeArmy() { - let shooters = []; - - let i = 0; - while (i < 10) { -*!* - let j = i; -*/!* - let shooter = function() { // shooter function - alert( *!*j*/!* ); // should show its number - }; - shooters.push(shooter); - i++; - } - - return shooters; -} - -let army = makeArmy(); - -army[0](); // 0 -army[5](); // 5 -``` - -The `while` loop, just like `for`, makes a new Lexical Environment for each run. So here we make sure that it gets the right value for a `shooter`. - -We copy `let j = i`. This makes a loop body local `j` and copies the value of `i` to it. Primitives are copied "by value", so we actually get a complete independent copy of `i`, belonging to the current loop iteration. diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/task.md b/1-js/06-advanced-functions/03-closure/8-make-army/task.md deleted file mode 100644 index ede8fd0458..0000000000 --- a/1-js/06-advanced-functions/03-closure/8-make-army/task.md +++ /dev/null @@ -1,35 +0,0 @@ -importance: 5 - ---- - -# Army of functions - -The following code creates an array of `shooters`. - -Every function is meant to output its number. But something is wrong... - -```js run -function makeArmy() { - let shooters = []; - - let i = 0; - while (i < 10) { - let shooter = function() { // shooter function - alert( i ); // should show its number - }; - shooters.push(shooter); - i++; - } - - return shooters; -} - -let army = makeArmy(); - -army[0](); // the shooter number 0 shows 10 -army[5](); // and number 5 also outputs 10... -// ... all shooters show 10 instead of their 0, 1, 2, 3... -``` - -Why all shooters show the same? Fix the code so that they work as intended. - 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 100% 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 diff --git a/1-js/06-advanced-functions/03-closure/article.md b/1-js/06-advanced-functions/03-closure/article.md index 3babdad476..cb43a7968f 100644 --- a/1-js/06-advanced-functions/03-closure/article.md +++ b/1-js/06-advanced-functions/03-closure/article.md @@ -1,193 +1,102 @@ -# Closure +# Variable scope, closure -JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created at one moment, then copied to another variable or passed as an argument to another function and called from a totally different place later. +JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created at any moment, passed as an argument to another function, and then called from a totally different place of code later. -We know that a function can access variables outside of it; this feature is used quite often. +We already know that a function can access variables outside of it ("outer" variables). -But what happens when an outer variable changes? Does a function get the most recent value or the one that existed when the function was created? +But what happens if outer variables change since a function is created? Will the function get newer values or the old ones? -Also, what happens when a function travels to another place in the code and is called from there -- does it get access to the outer variables of the new place? +And what if a function is passed along as an argument and called from another place of code, will it get access to outer variables at the new place? -Different languages behave differently here, and in this chapter we cover the behaviour of JavaScript. +Let's expand our knowledge to understand these scenarios and more complex ones. -## A couple of questions +```smart header="We'll talk about `let/const` variables here" +In JavaScript, there are 3 ways to declare a variable: `let`, `const` (the modern ones), and `var` (the remnant of the past). -Let's consider two situations to begin with, and then study the internal mechanics piece-by-piece, so that you'll be able to answer the following questions and more complex ones in the future. - -1. The function `sayHi` uses an external variable `name`. When the function runs, which value is it going to use? - - ```js - let name = "John"; - - function sayHi() { - alert("Hi, " + name); - } - - name = "Pete"; - - *!* - sayHi(); // what will it show: "John" or "Pete"? - */!* - ``` - - Such situations are common both in browser and server-side development. A function may be scheduled to execute later than it is created, for instance after a user action or a network request. - - So, the question is: does it pick up the latest changes? - - -2. The function `makeWorker` makes another function and returns it. That new function can be called from somewhere else. Will it have access to the outer variables from its creation place, or the invocation place, or both? - - ```js - function makeWorker() { - let name = "Pete"; - - return function() { - alert(name); - }; - } - - let name = "John"; - - // create a function - let work = makeWorker(); - - // call it - *!* - work(); // what will it show? "Pete" (name where created) or "John" (name where called)? - */!* - ``` - - -## Lexical Environment - -To understand what's going on, let's first discuss what a "variable" actually is. - -In JavaScript, every running function, code block, and the script as a whole have an associated object known as the *Lexical Environment*. - -The Lexical Environment object consists of two parts: - -1. *Environment Record* -- an object that has all local variables as its properties (and some other information like the value of `this`). -2. A reference to the *outer lexical environment*, usually the one associated with the code lexically right outside of it (outside of the current curly brackets). - -So, a "variable" is just a property of the special internal object, Environment Record. "To get or change a variable" means "to get or change a property of the Lexical Environment". - -For instance, in this simple code, there is only one Lexical Environment: - -![lexical environment](lexical-environment-global.png) - -This is a so-called global Lexical Environment, associated with the whole script. For browsers, all ` - ``` +That said, using global variables is generally discouraged. There should be as few global variables as possible. The code design where a function gets "input" variables and produces certain "outcome" is clearer, less prone to errors and easier to test than if it uses outer or global variables. - Here, first two alerts use the current window, and the latter two take variables from `iframe` window. Can be any variables if `iframe` originates from the same protocol/host/port. +## Using for polyfills -## "this" and global object +We use the global object to test for support of modern language features. -Sometimes, the value of `this` is exactly the global object. That's rarely used, but some scripts rely on that. +For instance, test if a built-in `Promise` object exists (it doesn't in really old browsers): +```js run +if (!window.Promise) { + alert("Your browser is really old!"); +} +``` -1. In the browser, the value of `this` in the global area is `window`: +If there's none (say, we're in an old browser), we can create "polyfills": add functions that are not supported by the environment, but exist in the modern standard. - ```js run - // outside of functions - alert( this === window ); // true - ``` +```js run +if (!window.Promise) { + window.Promise = ... // custom implementation of the modern language feature +} +``` - Other, non-browser environments, may use another value for `this` in such cases. +## Summary -2. When a function with `this` is called in non-strict mode, it gets the global object as `this`: - ```js run no-strict - // not in strict mode (!) - function f() { - alert(this); // [object Window] - } +- The global object holds variables that should be available everywhere. - f(); // called without an object - ``` + That includes JavaScript built-ins, such as `Array` and environment-specific values, such as `window.innerHeight` -- the window height in the browser. +- The global object has a universal name `globalThis`. - By specification, `this` in this case must be the global object, even in non-browser environments like Node.JS. That's for compatibility with old scripts, in strict mode `this` would be `undefined`. + ...But more often is referred by "old-school" environment-specific names, such as `window` (browser) and `global` (Node.js). +- We should store values in the global object only if they're truly global for our project. And keep their number at minimum. +- In-browser, unless we're using [modules](info:modules), global functions and variables declared with `var` become a property of the global object. +- To make our code future-proof and easier to understand, we should access properties of the global object directly, as `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 e34bb67bb0..a11821d679 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 @@ -7,8 +7,8 @@ importance: 5 Modify the code of `makeCounter()` so that the counter can also decrease and set the number: - `counter()` should return the next number (as before). -- `counter.set(value)` should set the `count` to `value`. -- `counter.decrease(value)` should decrease the `count` by 1. +- `counter.set(value)` should set the counter to `value`. +- `counter.decrease()` should decrease the counter by 1. See the sandbox code for the complete usage example. diff --git a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/solution.js b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/solution.js new file mode 100644 index 0000000000..c7d7d734ea --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/solution.js @@ -0,0 +1,15 @@ +function sum(a) { + + let currentSum = a; + + function f(b) { + currentSum += b; + return f; + } + + f.toString = function() { + return currentSum; + }; + + return f; +} diff --git a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/source.js b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/source.js new file mode 100644 index 0000000000..f10dca5dc8 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/source.js @@ -0,0 +1,12 @@ +function sum(a){ + // Your code goes here. + +} + +/* +sum(1)(2) == 3; // 1 + 2 +sum(1)(2)(3) == 6; // 1 + 2 + 3 +sum(5)(-1)(2) == 6 +sum(6)(-1)(-2)(-3) == 0 +sum(0)(1)(2)(3)(4)(5) == 15 +*/ diff --git a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/test.js b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/test.js new file mode 100644 index 0000000000..ed567d3306 --- /dev/null +++ b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/_js.view/test.js @@ -0,0 +1,19 @@ +describe("sum", function(){ + + it("sum(1)(2) == 3", function(){ + assert.equal(3, sum(1)(2)); + }); + + it("sum(5)(-1)(2) == 6", function(){ + assert.equal(6, sum(5)(-1)(2)); + }); + + it("sum(6)(-1)(-2)(-3) == 0", function(){ + assert.equal(0, sum(6)(-1)(-2)(-3)); + }); + + it("sum(0)(1)(2)(3)(4)(5) == 15", function(){ + assert.equal(15, sum(0)(1)(2)(3)(4)(5)); + }); +}); + diff --git a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md index 5c93269123..e97039f729 100644 --- a/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md +++ b/1-js/06-advanced-functions/06-function-object/5-sum-many-brackets/solution.md @@ -5,7 +5,7 @@ Now the code: -```js run +```js demo run function sum(a) { let currentSum = a; @@ -52,4 +52,4 @@ function f(b) { } ``` -This `f` will be used in the next call, again return itself, so many times as needed. Then, when used as a number or a string -- the `toString` returns the `currentSum`. We could also use `Symbol.toPrimitive` or `valueOf` here for the conversion. +This `f` will be used in the next call, again return itself, as many times as needed. Then, when used as a number or a string -- the `toString` returns the `currentSum`. We could also use `Symbol.toPrimitive` or `valueOf` here for the conversion. 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 f6234a6dba..c84f4e52f7 100644 --- a/1-js/06-advanced-functions/06-function-object/article.md +++ b/1-js/06-advanced-functions/06-function-object/article.md @@ -1,7 +1,7 @@ # Function object, NFE -As we already know, functions in JavaScript are values. +As we already know, a function in JavaScript is a value. Every value in JavaScript has a type. What type is a function? @@ -12,7 +12,7 @@ A good way to imagine functions is as callable "action objects". We can not only ## The "name" property -Function objects contain a few useable properties. +Function objects contain some useable properties. For instance, a function's name is accessible as the "name" property: @@ -24,14 +24,14 @@ function sayHi() { alert(sayHi.name); // sayHi ``` -What's more funny, the name-assigning logic is smart. It also assigns the correct name to functions that are used in assignments: +What's kind of funny, the name-assigning logic is smart. It also assigns the correct name to a function even if it's created without one, and then immediately assigned: ```js run let sayHi = function() { alert("Hi"); -} +}; -alert(sayHi.name); // sayHi (works!) +alert(sayHi.name); // sayHi (there's a name!) ``` It also works if the assignment is done via a default value: @@ -65,9 +65,9 @@ alert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye ``` -There's no magic though. There are cases when there's no way to figure out the right name. In that case, the name property empty, like here: +There's no magic though. There are cases when there's no way to figure out the right name. In that case, the name property is empty, like here: -```js +```js run // function created inside array let arr = [function() {}]; @@ -93,7 +93,7 @@ alert(many.length); // 2 Here we can see that rest parameters are not counted. -The `length` property is sometimes used for introspection in functions that operate on other functions. +The `length` property is sometimes used for [introspection](https://en.wikipedia.org/wiki/Type_introspection) in functions that operate on other functions. For instance, in the code below the `ask` function accepts a `question` to ask and an arbitrary number of `handler` functions to call. @@ -102,9 +102,9 @@ Once a user provides their answer, the function calls the handlers. We can pass - A zero-argument function, which is only called when the user gives a positive answer. - A function with arguments, which is called in either case and returns an answer. -The idea is that we have a simple, no-arguments handler syntax for positive cases (most frequent variant), but are able to provide universal handlers as well. +To call `handler` the right way, we examine the `handler.length` property. -To call `handlers` the right way, we examine the `length` property: +The idea is that we have a simple, no-arguments handler syntax for positive cases (most frequent variant), but are able to support universal handlers as well: ```js run function ask(question, ...handlers) { @@ -153,7 +153,7 @@ alert( `Called ${sayHi.counter} times` ); // Called 2 times ```warn header="A property is not a variable" A property assigned to a function like `sayHi.counter = 0` does *not* define a local variable `counter` inside it. In other words, a property `counter` and a variable `let counter` are two unrelated things. -We can treat a function as an object, store properties in it, but that has no effect on its execution. Variables never use function properties and vice versa. These are just parallel words. +We can treat a function as an object, store properties in it, but that has no effect on its execution. Variables are not function properties and vice versa. These are just parallel worlds. ``` Function properties can replace closures sometimes. For instance, we can rewrite the counter function example from the chapter to use a function property: @@ -241,7 +241,7 @@ let sayHi = function *!*func*/!*(who) { sayHi("John"); // Hello, John ``` -There are two special things about the name `func`: +There are two special things about the name `func`, that are the reasons for it: 1. It allows the function to reference itself internally. 2. It is not visible outside of the function. @@ -282,7 +282,7 @@ let sayHi = function(who) { }; ``` -The problem with that code is that the value of `sayHi` may change. The function may go to another variable, and the code will start to give errors: +The problem with that code is that `sayHi` may change in the outer code. If the function gets assigned to another variable instead, the code will start to give errors: ```js run let sayHi = function(who) { @@ -326,10 +326,10 @@ welcome(); // Hello, Guest (nested call works) Now it works, because the name `"func"` is function-local. It is not taken from outside (and not visible there). The specification guarantees that it will always reference the current function. -The outer code still has it's variable `sayHi` or `welcome`. And `func` is an "internal function name", how the function can call itself internally. +The outer code still has its variable `sayHi` or `welcome`. And `func` is an "internal function name", the way for the function to can call itself reliably. ```smart header="There's no such thing for Function Declaration" -The "internal name" feature described here is only available for Function Expressions, not to Function Declarations. For Function Declarations, there's just no syntax possibility to add a one more "internal" name. +The "internal name" feature described here is only available for Function Expressions, not for Function Declarations. For Function Declarations, there is no syntax for adding an "internal" name. Sometimes, when we need a reliable internal name, it's the reason to rewrite a Function Declaration to Named Function Expression form. ``` @@ -340,13 +340,14 @@ Functions are objects. Here we covered their properties: -- `name` -- the function name. Exists not only when given in the function definition, but also for assignments and object properties. +- `name` -- the function name. Usually taken from the function definition, but if there's none, JavaScript tries to guess it from the context (e.g. an assignment). - `length` -- the number of arguments in the function definition. Rest parameters are not counted. If the function is declared as a Function Expression (not in the main code flow), and it carries the name, then it is called a Named Function Expression. The name can be used inside to reference itself, for recursive calls or such. Also, functions may carry additional properties. Many well-known JavaScript libraries make great use of this feature. -They create a "main" function and attach many other "helper" functions to it. For instance, the [jquery](https://jquery.com) library creates a function named `$`. The [lodash](https://lodash.com) library creates a function `_`. And then adds `_.clone`, `_.keyBy` and other properties to (see the [docs](https://lodash.com/docs) when you want learn more about them). Actually, they do it to lessen their pollution of the global space, so that a single library gives only one global variable. That reduces the possibility of naming conflicts. +They create a "main" function and attach many other "helper" functions to it. For instance, the [jQuery](https://jquery.com) library creates a function named `$`. The [lodash](https://lodash.com) library creates a function `_`, and then adds `_.clone`, `_.keyBy` and other properties to it (see the [docs](https://lodash.com/docs) when you want to learn more about them). Actually, they do it to lessen their pollution of the global space, so that a single library gives only one global variable. That reduces the possibility of naming conflicts. + So, a function can do a useful job by itself and also carry a bunch of other functionality in properties. 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 e0750e3218..ffe264a4e7 100644 --- a/1-js/06-advanced-functions/07-new-function/article.md +++ b/1-js/06-advanced-functions/07-new-function/article.md @@ -8,20 +8,20 @@ There's one more way to create a function. It's rarely used, but sometimes there The syntax for creating a function: ```js -let func = new Function ([arg1[, arg2[, ...argN]],] functionBody) +let func = new Function ([arg1, arg2, ...argN], functionBody); ``` -In other words, function parameters (or, more precisely, names for them) go first, and the body is last. All arguments are strings. +The function is created with the arguments `arg1...argN` and the given `functionBody`. It's easier to understand by looking at an example. Here's a function with two arguments: ```js run -let sum = new Function('a', 'b', 'return a + b'); +let sum = new Function('a', 'b', 'return a + b'); alert( sum(1, 2) ); // 3 ``` -If there are no arguments, then there's only a single argument, the function body: +And here there's a function without arguments, with only the function body: ```js run let sayHi = new Function('alert("Hello")'); @@ -29,7 +29,7 @@ let sayHi = new Function('alert("Hello")'); sayHi(); // Hello ``` -The major difference from other ways we've seen is that the function is created literally from a string, that is passed at run time. +The major difference from other ways we've seen is that the function is created literally from a string, that is passed at run time. All previous declarations required us, programmers, to write the function code in the script. @@ -42,16 +42,17 @@ let func = new Function(str); func(); ``` -It is used in very specific cases, like when we receive code from a server, or to dynamically compile a function from a template. The need for that usually arises at advanced stages of development. +It is used in very specific cases, like when we receive code from a server, or to dynamically compile a function from a template, in complex web-applications. ## Closure -Usually, a function remembers where it was born in the special property `[[Environment]]`. It references the Lexical Environment from where it's created. +Usually, a function remembers where it was born in the special property `[[Environment]]`. It references the Lexical Environment from where it's created (we covered that in the chapter ). -But when a function is created using `new Function`, its `[[Environment]]` references not the current Lexical Environment, but instead the global one. +But when a function is created using `new Function`, its `[[Environment]]` is set to reference not the current Lexical Environment, but the global one. -```js run +So, such function doesn't have access to outer variables, only to the global ones. +```js run function getFunc() { let value = "test"; @@ -67,7 +68,7 @@ getFunc()(); // error: value is not defined Compare it with the regular behavior: -```js run +```js run function getFunc() { let value = "test"; @@ -87,51 +88,36 @@ Imagine that we must create a function from a string. The code of that function Our new function needs to interact with the main script. -Perhaps we want it to be able to access outer local variables? +What if it could access the outer variables? The problem is that before JavaScript is published to production, it's compressed using a *minifier* -- a special program that shrinks code by removing extra comments, spaces and -- what's important, renames local variables into shorter ones. -For instance, if a function has `let userName`, minifier replaces it `let a` (or another letter if this one is occupied), and does it everywhere. That's usually a safe thing to do, because the variable is local, nothing outside the function can access it. And inside the function, minifier replaces every mention of it. Minifiers are smart, they analyze the code structure, so they don't break anything. They're not just a dumb find-and-replace. +For instance, if a function has `let userName`, minifier replaces it with `let a` (or another letter if this one is occupied), and does it everywhere. That's usually a safe thing to do, because the variable is local, nothing outside the function can access it. And inside the function, minifier replaces every mention of it. Minifiers are smart, they analyze the code structure, so they don't break anything. They're not just a dumb find-and-replace. -But, if `new Function` could access outer variables, then it would be unable to find `userName`, since this is passed in as a string *after* the code is minified. +So if `new Function` had access to outer variables, it would be unable to find renamed `userName`. -**Even if we could access outer lexical environment in `new Function`, we would have problems with minifiers.** +**If `new Function` had access to outer variables, it would have problems with minifiers.** -The "special feature" of `new Function` saves us from mistakes. +Besides, such code would be architecturally bad and prone to errors. -And it enforces better code. If we need to pass something to a function created by `new Function`, we should pass it explicitly as an argument. - -Our "sum" function actually does that right: - -```js run -*!* -let sum = new Function('a', 'b', 'return a + b'); -*/!* - -let a = 1, b = 2; - -*!* -// outer values are passed as arguments -alert( sum(a, b) ); // 3 -*/!* -``` +To pass something to a function, created as `new Function`, we should use its arguments. ## Summary The syntax: ```js -let func = new Function(arg1, arg2, ..., body); +let func = new Function ([arg1, arg2, ...argN], functionBody); ``` -For historical reasons, arguments can also be given as a comma-separated list. +For historical reasons, arguments can also be given as a comma-separated list. -These three mean the same: +These three declarations mean the same: -```js +```js new Function('a', 'b', 'return a + b'); // basic syntax new Function('a,b', 'return a + b'); // comma-separated new Function('a , b', 'return a + b'); // comma-separated with spaces ``` -Functions created with `new Function`, have `[[Environment]]` referencing the global Lexical Environment, not the outer one. Hence, they cannot use outer variables. But that's actually good, because it saves us from errors. Passing parameters explicitly is a much better method architecturally and causes no problems with minifiers. +Functions created with `new Function`, have `[[Environment]]` referencing the global Lexical Environment, not the outer one. Hence, they cannot use outer variables. But that's actually good, because it insures us from errors. Passing parameters explicitly is a much better method architecturally and causes no problems with minifiers. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md index 13f01debfb..b5b1da7a6a 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/solution.md @@ -18,7 +18,7 @@ function printNumbers(from, to) { printNumbers(5, 10); ``` -Using recursive `setTimeout`: +Using nested `setTimeout`: ```js run @@ -38,5 +38,27 @@ function printNumbers(from, to) { printNumbers(5, 10); ``` -Note that in both solutions, there is an initial delay before the first output. Sometimes we need to add a line to make the first output immediately, that's easy to do. +Note that in both solutions, there is an initial delay before the first output. The function is called after `1000ms` the first time. +If we also want the function to run immediately, then we can add an additional call on a separate line, like this: + +```js run +function printNumbers(from, to) { + let current = from; + + function go() { + alert(current); + if (current == to) { + clearInterval(timerId); + } + current++; + } + +*!* + go(); +*/!* + let timerId = setInterval(go, 1000); +} + +printNumbers(5, 10); +``` diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md index 87e723c67b..84bb0c39c0 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/1-output-numbers-100ms/task.md @@ -9,5 +9,4 @@ Write a function `printNumbers(from, to)` that outputs a number every second, st Make two variants of the solution. 1. Using `setInterval`. -2. Using recursive `setTimeout`. - +2. Using nested `setTimeout`. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/solution.md b/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/solution.md deleted file mode 100644 index 0183a47997..0000000000 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/solution.md +++ /dev/null @@ -1,23 +0,0 @@ - - -```js run -let i = 0; - -let start = Date.now(); - -let timer = setInterval(count, 0); - -function count() { - - for(let j = 0; j < 1000000; j++) { - i++; - } - - if (i == 1000000000) { - alert("Done in " + (Date.now() - start) + 'ms'); - clearInterval(timer); - } - -} -``` - diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/task.md b/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/task.md deleted file mode 100644 index 3c78817017..0000000000 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/3-rewrite-settimeout/task.md +++ /dev/null @@ -1,32 +0,0 @@ -importance: 4 - ---- - -# Rewrite setTimeout with setInterval - -Here's the function that uses nested `setTimeout` to split a job into pieces. - -Rewrite it to `setInterval`: - -```js run -let i = 0; - -let start = Date.now(); - -function count() { - - if (i == 1000000000) { - alert("Done in " + (Date.now() - start) + 'ms'); - } else { - setTimeout(count, 0); - } - - // a piece of heavy job - for(let j = 0; j < 1000000; j++) { - i++; - } - -} - -count(); -``` 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 412e2e3e90..f969599884 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/article.md +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md @@ -4,18 +4,17 @@ We may decide to execute a function not right now, but at a certain time later. There are two methods for it: -- `setTimeout` allows to run a function once after the interval of time. -- `setInterval` allows to run a function regularly with the interval between the runs. - -These methods are not a part of JavaScript specification. But most environments have the internal scheduler and provide these methods. In particular, they are supported in all browsers and Node.JS. +- `setTimeout` allows us to run a function once after the interval of time. +- `setInterval` allows us to run a function repeatedly, starting after the interval of time, then repeating continuously at that interval. +These methods are not a part of JavaScript specification. But most environments have the internal scheduler and provide these methods. In particular, they are supported in all browsers and Node.js. ## setTimeout The syntax: ```js -let timerId = setTimeout(func|code, delay[, arg1, arg2...]) +let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...) ``` Parameters: @@ -25,10 +24,10 @@ Parameters: Usually, that's a function. For historical reasons, a string of code can be passed, but that's not recommended. `delay` -: The delay before run, in milliseconds (1000 ms = 1 second). +: The delay before run, in milliseconds (1000 ms = 1 second), by default 0. `arg1`, `arg2`... -: Arguments for the function (not supported in IE9-) +: Arguments for the function For instance, this code calls `sayHi()` after one second: @@ -62,7 +61,7 @@ So, this will also work: setTimeout("alert('Hello')", 1000); ``` -But using strings is not recommended, use functions instead of them, like this: +But using strings is not recommended, use arrow functions instead of them, like this: ```js run no-beautify setTimeout(() => alert('Hello'), 1000); @@ -75,7 +74,7 @@ Novice developers sometimes make a mistake by adding brackets `()` after the fun // wrong! setTimeout(sayHi(), 1000); ``` -That doesn't work, because `setTimeout` expects a reference to function. And here `sayHi()` runs the function, and the *result of its execution* is passed to `setTimeout`. In our case the result of `sayHi()` is `undefined` (the function returns nothing), so nothing is scheduled. +That doesn't work, because `setTimeout` expects a reference to a function. And here `sayHi()` runs the function, and the *result of its execution* is passed to `setTimeout`. In our case the result of `sayHi()` is `undefined` (the function returns nothing), so nothing is scheduled. ```` ### Canceling with clearTimeout @@ -99,18 +98,18 @@ clearTimeout(timerId); alert(timerId); // same identifier (doesn't become null after canceling) ``` -As we can see from `alert` output, in a browser the timer identifier is a number. In other environments, this can be something else. For instance, Node.JS returns a timer object with additional methods. +As we can see from `alert` output, in a browser the timer identifier is a number. In other environments, this can be something else. For instance, Node.js returns a timer object with additional methods. Again, there is no universal specification for these methods, so that's fine. -For browsers, timers are described in the [timers section](https://www.w3.org/TR/html5/webappapis.html#timers) of HTML5 standard. +For browsers, timers are described in the [timers section](https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers) of HTML Living Standard. ## setInterval The `setInterval` method has the same syntax as `setTimeout`: ```js -let timerId = setInterval(func|code, delay[, arg1, arg2...]) +let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) ``` All arguments have the same meaning. But unlike `setTimeout` it runs the function not only once, but regularly after the given interval of time. @@ -127,17 +126,17 @@ let timerId = setInterval(() => alert('tick'), 2000); setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); ``` -```smart header="Modal windows freeze time in Chrome/Opera/Safari" -In browsers IE and Firefox the internal timer continues "ticking" while showing `alert/confirm/prompt`, but in Chrome, Opera and Safari the internal timer becomes "frozen". +```smart header="Time goes on while `alert` is shown" +In most browsers, including Chrome and Firefox the internal timer continues "ticking" while showing `alert/confirm/prompt`. -So if you run the code above and don't dismiss the `alert` window for some time, then in Firefox/IE next `alert` will be shown immediately as you do it (2 seconds passed from the previous invocation), and in Chrome/Opera/Safari -- after 2 more seconds (timer did not tick during the `alert`). +So if you run the code above and don't dismiss the `alert` window for some time, then the next `alert` will be shown immediately as you do it. The actual interval between alerts will be shorter than 2 seconds. ``` -## Recursive setTimeout +## Nested setTimeout There are two ways of running something regularly. -One is `setInterval`. The other one is a recursive `setTimeout`, like this: +One is `setInterval`. The other one is a nested `setTimeout`, like this: ```js /** instead of: @@ -154,7 +153,7 @@ let timerId = setTimeout(function tick() { The `setTimeout` above schedules the next call right at the end of the current one `(*)`. -The recursive `setTimeout` is a more flexible method than `setInterval`. This way the next call may be scheduled differently, depending on the results of the current one. +The nested `setTimeout` is a more flexible method than `setInterval`. This way the next call may be scheduled differently, depending on the results of the current one. For instance, we need to write a service that sends a request to the server every 5 seconds asking for data, but in case the server is overloaded, it should increase the interval to 10, 20, 40 seconds... @@ -176,32 +175,32 @@ let timerId = setTimeout(function request() { ``` -And if we regularly have CPU-hungry tasks, then we can measure the time taken by the execution and plan the next call sooner or later. +And if the functions that we're scheduling are CPU-hungry, then we can measure the time taken by the execution and plan the next call sooner or later. -**Recursive `setTimeout` guarantees a delay between the executions, `setInterval` -- does not.** +**Nested `setTimeout` allows to set the delay between the executions more precisely than `setInterval`.** Let's compare two code fragments. The first one uses `setInterval`: ```js let i = 1; setInterval(function() { - func(i); + func(i++); }, 100); ``` -The second one uses recursive `setTimeout`: +The second one uses nested `setTimeout`: ```js let i = 1; setTimeout(function run() { - func(i); + func(i++); setTimeout(run, 100); }, 100); ``` -For `setInterval` the internal scheduler will run `func(i)` every 100ms: +For `setInterval` the internal scheduler will run `func(i++)` every 100ms: -![](setinterval-interval.png) +![](setinterval-interval.svg) Did you notice? @@ -215,15 +214,15 @@ In this case the engine waits for `func` to complete, then checks the scheduler In the edge case, if the function always executes longer than `delay` ms, then the calls will happen without a pause at all. -And here is the picture for the recursive `setTimeout`: +And here is the picture for the nested `setTimeout`: -![](settimeout-interval.png) +![](settimeout-interval.svg) -**The recursive `setTimeout` guarantees the fixed delay (here 100ms).** +**The nested `setTimeout` guarantees the fixed delay (here 100ms).** That's because a new call is planned at the end of the previous one. -````smart header="Garbage collection" +````smart header="Garbage collection and setInterval/setTimeout callback" When a function is passed in `setInterval/setTimeout`, an internal reference is created to it and saved in the scheduler. It prevents the function from being garbage collected, even if there are no other references to it. ```js @@ -233,135 +232,33 @@ setTimeout(function() {...}, 100); For `setInterval` the function stays in memory until `clearInterval` is called. -There's a side-effect. A function references the outer lexical environment, so, while it lives, outer variables live too. They may take much more memory than the function itself. So when we don't need the scheduled function anymore, it's better to cancel it, even if it's very small. +There's a side effect. A function references the outer lexical environment, so, while it lives, outer variables live too. They may take much more memory than the function itself. So when we don't need the scheduled function anymore, it's better to cancel it, even if it's very small. ```` -## setTimeout(...,0) +## Zero delay setTimeout -There's a special use case: `setTimeout(func, 0)`. +There's a special use case: `setTimeout(func, 0)`, or just `setTimeout(func)`. -This schedules the execution of `func` as soon as possible. But scheduler will invoke it only after the current code is complete. +This schedules the execution of `func` as soon as possible. But the scheduler will invoke it only after the currently executing script is complete. -So the function is scheduled to run "right after" the current code. In other words, *asynchronously*. +So the function is scheduled to run "right after" the current script. For instance, this outputs "Hello", then immediately "World": ```js run -setTimeout(() => alert("World"), 0); +setTimeout(() => alert("World")); alert("Hello"); ``` -The first line "puts the call into calendar after 0ms". But the scheduler will only "check the calendar" after the current code is complete, so `"Hello"` is first, and `"World"` -- after it. - -### Splitting CPU-hungry tasks - -There's a trick to split CPU-hungry tasks using `setTimeout`. - -For instance, a syntax-highlighting script (used to colorize code examples on this page) is quite CPU-heavy. To highlight the code, it performs the analysis, creates many colored elements, adds them to the document -- for a big text that takes a lot. It may even cause the browser to "hang", which is unacceptable. - -So we can split the long text into pieces. First 100 lines, then plan another 100 lines using `setTimeout(...,0)`, and so on. - -For clarity, let's take a simpler example for consideration. We have a function to count from `1` to `1000000000`. - -If you run it, the CPU will hang. For server-side JS that's clearly noticeable, and if you are running it in-browser, then try to click other buttons on the page -- you'll see that whole JavaScript actually is paused, no other actions work until it finishes. - -```js run -let i = 0; - -let start = Date.now(); - -function count() { - - // do a heavy job - for (let j = 0; j < 1e9; j++) { - i++; - } - - alert("Done in " + (Date.now() - start) + 'ms'); -} - -count(); -``` - -The browser may even show "the script takes too long" warning (but hopefully it won't, because the number is not very big). - -Let's split the job using the nested `setTimeout`: - -```js run -let i = 0; - -let start = Date.now(); - -function count() { - - // do a piece of the heavy job (*) - do { - i++; - } while (i % 1e6 != 0); - - if (i == 1e9) { - alert("Done in " + (Date.now() - start) + 'ms'); - } else { - setTimeout(count, 0); // schedule the new call (**) - } - -} - -count(); -``` - -Now the browser UI is fully functional during the "counting" process. - -We do a part of the job `(*)`: - -1. First run: `i=1...1000000`. -2. Second run: `i=1000001..2000000`. -3. ...and so on, the `while` checks if `i` is evenly divided by `1000000`. - -Then the next call is scheduled in `(*)` if we're not done yet. - -Pauses between `count` executions provide just enough "breath" for the JavaScript engine to do something else, to react to other user actions. - -The notable thing is that both variants -- with and without splitting the job by `setTimeout` -- are comparable in speed. There's no much difference in the overall counting time. - -To make them closer, let's make an improvement. - -We'll move the scheduling in the beginning of the `count()`: - -```js run -let i = 0; - -let start = Date.now(); - -function count() { - - // move the scheduling at the beginning - if (i < 1e9 - 1e6) { - setTimeout(count, 0); // schedule the new call - } - - do { - i++; - } while (i % 1e6 != 0); - - if (i == 1e9) { - alert("Done in " + (Date.now() - start) + 'ms'); - } - -} - -count(); -``` - -Now when we start to `count()` and know that we'll need to `count()` more, we schedule that immediately, before doing the job. +The first line "puts the call into calendar after 0ms". But the scheduler will only "check the calendar" after the current script is complete, so `"Hello"` is first, and `"World"` -- after it. -If you run it, it's easy to notice that it takes significantly less time. +There are also advanced browser-related use cases of zero-delay timeout, that we'll discuss in the chapter . -````smart header="Minimal delay of nested timers in-browser" -In the browser, there's a limitation of how often nested timers can run. The [HTML5 standard](https://www.w3.org/TR/html5/webappapis.html#timers) says: "after five nested timers, the interval is forced to be at least four milliseconds.". +````smart header="Zero delay is in fact not zero (in a browser)" +In the browser, there's a limitation of how often nested timers can run. The [HTML Living Standard](https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers) says: "after five nested timers, the interval is forced to be at least 4 milliseconds.". -Let's demonstrate what it means with the example below. The `setTimeout` call in it re-schedules itself after `0ms`. Each call remembers the real time from the previous one in the `times` array. What do the real delays look like? Let's see: +Let's demonstrate what it means with the example below. The `setTimeout` call in it re-schedules itself with zero delay. Each call remembers the real time from the previous one in the `times` array. What do the real delays look like? Let's see: ```js run let start = Date.now(); @@ -371,92 +268,35 @@ setTimeout(function run() { times.push(Date.now() - start); // remember delay from the previous call if (start + 100 < Date.now()) alert(times); // show the delays after 100ms - else setTimeout(run, 0); // else re-schedule -}, 0); + else setTimeout(run); // else re-schedule +}); // an example of the output: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100 ``` -First timers run immediately (just as written in the spec), and then the delay comes into play and we see `9, 15, 20, 24...`. +First timers run immediately (just as written in the spec), and then we see `9, 15, 20, 24...`. The 4+ ms obligatory delay between invocations comes into play. + +The similar thing happens if we use `setInterval` instead of `setTimeout`: `setInterval(f)` runs `f` few times with zero-delay, and afterwards with 4+ ms delay. That limitation comes from ancient times and many scripts rely on it, so it exists for historical reasons. -For server-side JavaScript, that limitation does not exist, and there exist other ways to schedule an immediate asynchronous job, like [process.nextTick](https://nodejs.org/api/process.html) and [setImmediate](https://nodejs.org/api/timers.html) for Node.JS. So the notion is browser-specific only. +For server-side JavaScript, that limitation does not exist, and there exist other ways to schedule an immediate asynchronous job, like [setImmediate](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args) for Node.js. So this note is browser-specific. ```` -### Allowing the browser to render - -Another benefit for in-browser scripts is that they can show a progress bar or something to the user. That's because the browser usually does all "repainting" after the script is complete. - -So if we do a single huge function then even if it changes something, the changes are not reflected in the document till it finishes. - -Here's the demo: -```html run -
- - -``` - -If you run it, the changes to `i` will show up after the whole count finishes. - -And if we use `setTimeout` to split it into pieces then changes are applied in-between the runs, so this looks better: - -```html run -
- - -``` - -Now the `
` shows increasing values of `i`. - ## Summary -- Methods `setInterval(func, delay, ...args)` and `setTimeout(func, delay, ...args)` allow to run the `func` regularly/once after `delay` milliseconds. -- To cancel the execution, we should call `clearInterval/clearTimeout` with the value returned by `setInterval/setTimeout`. -- Nested `setTimeout` calls is a more flexible alternative to `setInterval`. Also they can guarantee the minimal time *between* the executions. -- Zero-timeout scheduling `setTimeout(...,0)` is used to schedule the call "as soon as possible, but after the current code is complete". - -Some use cases of `setTimeout(...,0)`: -- To split CPU-hungry tasks into pieces, so that the script doesn't "hang" -- To let the browser do something else while the process is going on (paint the progress bar). +- Methods `setTimeout(func, delay, ...args)` and `setInterval(func, delay, ...args)` allow us to run the `func` once/regularly after `delay` milliseconds. +- To cancel the execution, we should call `clearTimeout/clearInterval` with the value returned by `setTimeout/setInterval`. +- Nested `setTimeout` calls are a more flexible alternative to `setInterval`, allowing us to set the time *between* executions more precisely. +- Zero delay scheduling with `setTimeout(func, 0)` (the same as `setTimeout(func)`) is used to schedule the call "as soon as possible, but after the current script is complete". +- The browser limits the minimal delay for five or more nested calls of `setTimeout` or for `setInterval` (after 5th call) to 4ms. That's for historical reasons. -Please note that all scheduling methods do not *guarantee* the exact delay. We should not rely on that in the scheduled code. +Please note that all scheduling methods do not *guarantee* the exact delay. For example, the in-browser timer may slow down for a lot of reasons: - The CPU is overloaded. - The browser tab is in the background mode. -- The laptop is on battery. +- The laptop is on battery saving mode. -All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and settings. +All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and OS-level performance settings. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png deleted file mode 100644 index 060b2c293b..0000000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png and /dev/null differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg new file mode 100644 index 0000000000..bce7d6a843 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg @@ -0,0 +1 @@ +func(1)func(2)func(3)100200300 \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png deleted file mode 100644 index 4071849cd5..0000000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png deleted file mode 100644 index 6c473a33cc..0000000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png and /dev/null differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg new file mode 100644 index 0000000000..d6d233b2ba --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg @@ -0,0 +1 @@ +func(1)func(2)func(3)100100 \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png deleted file mode 100644 index dd45e324f8..0000000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png and /dev/null differ 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..d5a09efb36 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) { + // using ...args instead of arguments to store "real" array in 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 19a072014b..0c8a211b49 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 @@ -Here we can use `calls.push(args)` to store all arguments in the log and `f.apply(this, args)` to forward the call. +The wrapper returned by `spy(f)` should store all arguments and then use `f.apply` to forward the call. 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 9ebb7abfca..a3843107c9 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. That decorator is sometimes useful for unit-testing, it's advanced form is `sinon.spy` in [Sinon.JS](http://sinonjs.org/) library. +P.S. That decorator is sometimes useful for unit-testing. Its advanced form is `sinon.spy` in [Sinon.JS](http://sinonjs.org/) library. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md index 44b5024e18..24bb4d4484 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/02-delay/solution.md @@ -1,6 +1,6 @@ The solution: -```js +```js run demo function delay(f, ms) { return function() { @@ -8,20 +8,25 @@ function delay(f, ms) { }; } + +let f1000 = delay(alert, 1000); + +f1000("test"); // shows "test" after 1000ms ``` Please note how an arrow function is used here. As we know, arrow functions do not have own `this` and `arguments`, so `f.apply(this, arguments)` takes `this` and `arguments` from the wrapper. -If we pass a regular function, `setTimeout` would call it without arguments and `this=window` (in-browser), so we'd need to write a bit more code to pass them from the wrapper: +If we pass a regular function, `setTimeout` would call it without arguments and `this=window` (assuming we're in the browser). + +We still can pass the right `this` by using an intermediate variable, but that's a little bit more cumbersome: ```js function delay(f, ms) { - // added variables to pass this and arguments from the wrapper inside setTimeout return function(...args) { - let savedThis = this; + let savedThis = this; // store this into an intermediate variable setTimeout(function() { - f.apply(savedThis, args); + f.apply(savedThis, args); // use it here }, ms); }; 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..661dd0cf41 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 16dc171e1a..750e649f83 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,48 @@ -describe("debounce", function() { - before(function() { +describe('debounce', function () { + before(function () { this.clock = sinon.useFakeTimers(); }); - after(function() { + after(function () { this.clock.restore(); }); - it("calls the function at maximum once in ms milliseconds", function() { - let log = ''; + it('for one call - runs it after given ms', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); - function f(a) { - log += a; - } + debounced('test'); + assert(f.notCalled, 'not called immediately'); + this.clock.tick(1000); + assert(f.calledOnceWith('test'), 'called after 1000ms'); + }); - f = debounce(f, 1000); + it('for 3 calls - runs the last one after given ms', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); - f(1); // runs at once - f(2); // ignored + debounced('a'); + setTimeout(() => debounced('b'), 200); // ignored (too early) + setTimeout(() => debounced('c'), 500); // runs (1000 ms passed) + this.clock.tick(1000); - setTimeout(() => f(3), 100); // ignored (too early) - setTimeout(() => f(4), 1100); // runs (1000 ms passed) - setTimeout(() => f(5), 1500); // ignored (less than 1000 ms from the last run) + assert(f.notCalled, 'not called after 1000ms'); - this.clock.tick(5000); - assert.equal(log, "14"); + this.clock.tick(500); + + assert(f.calledOnceWith('c'), 'called after 1500ms'); }); - it("keeps the context of the call", function() { + it('keeps the context of the call', 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..e624ce0203 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg @@ -0,0 +1 @@ +200ms1500ms1000ms0cf(a)f(b)f(c)500mstimecalls: after 1000ms \ 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..e3b4d5842f --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html @@ -0,0 +1,24 @@ + + + +Function handler is called on this input: +
+ + +

+ +Debounced function debounce(handler, 1000) is called on this input: +
+ + +

+ + + \ 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 1516aca350..83e75f3158 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,30 +1,13 @@ - - -```js run no-beautify -function debounce(f, ms) { - - let isCooldown = false; - +```js demo +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); }; - } -``` -The call to `debounce` returns a wrapper. There may be two states: - -- `isCooldown = false` -- ready to run. -- `isCooldown = true` -- waiting for the timeout. - -In the first call `isCooldown` is falsy, so the call proceeds, and the state changes to `true`. +``` -While `isCooldown` is true, all other calls are ignored. +A call to `debounce` returns a wrapper. When called, it schedules the original function call after given `ms` and cancels the previous such timeout. -Then `setTimeout` reverts it to `false` after the given delay. 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 466c6bc3f7..5b0fcc5f87 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,21 +4,48 @@ importance: 5 # Debounce decorator -The result of `debounce(f, ms)` decorator should be a wrapper that passes the call to `f` at maximum once per `ms` milliseconds. +The result of `debounce(f, ms)` decorator is a wrapper that suspends calls to `f` until there's `ms` milliseconds of inactivity (no calls, "cooldown period"), then invokes `f` once with the latest arguments. -In other words, when we call a "debounced" function, it guarantees that all other future in the closest `ms` milliseconds will be ignored. +In other words, `debounce` is like a secretary that accepts "phone calls", and waits until there's `ms` milliseconds of being quiet. And only then it transfers the latest call information to "the boss" (calls the actual `f`). -For instance: +For instance, we had a function `f` and replaced it with `f = debounce(f, 1000)`. -```js no-beautify -let f = debounce(alert, 1000); +Then if the wrapped function is called at 0ms, 200ms and 500ms, and then there are no calls, then the actual `f` will be only called once, at 1500ms. That is: after the cooldown period of 1000ms from the last call. -f(1); // runs immediately -f(2); // ignored +![](debounce.svg) -setTimeout( () => f(3), 100); // ignored ( only 100 ms passed ) -setTimeout( () => f(4), 1100); // runs -setTimeout( () => f(5), 1500); // ignored (less than 1000 ms from the last run) +...And it will get the arguments of the very last call, other calls are ignored. + +Here's the code for it (uses the debounce decorator from the [Lodash library](https://lodash.com/docs/4.17.15#debounce)): + +```js +let f = _.debounce(alert, 1000); + +f("a"); +setTimeout( () => f("b"), 200); +setTimeout( () => f("c"), 500); +// debounced function waits 1000ms after the last call and then runs: alert("c") +``` + +Now a practical example. Let's say, the user types something, and we'd like to send a request to the server when the input is finished. + +There's no point in sending the request for every character typed. Instead we'd like to wait, and then process the whole result. + +In a web-browser, we can setup an event handler -- a function that's called on every change of an input field. Normally, an event handler is called very often, for every typed key. But if we `debounce` it by 1000ms, then it will be only called once, after 1000ms after the last input. + +```online + +In this live example, the handler puts the result into a box below, try it: + +[iframe border=1 src="debounce" height=200] + +See? The second input calls the debounced function, so its content is processed after 1000ms from the last input. ``` -In practice `debounce` is useful for functions that retrieve/update something when we know that nothing new can be done in such a short period of time, so it's better not to waste resources. \ No newline at end of file +So, `debounce` is a great way to process a sequence of events: be it a sequence of key presses, mouse movements or something else. + +It waits the given time after the last call, and then runs its function, that can process the result. + +The task is to implement `debounce` decorator. + +Hint: that's just a few lines if you think about it :) diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js index 5339c8d117..e671438f6f 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/_js.view/test.js @@ -7,8 +7,8 @@ describe("throttle(f, 1000)", function() { } before(function() { - f1000 = throttle(f, 1000); this.clock = sinon.useFakeTimers(); + f1000 = throttle(f, 1000); }); it("the first call runs now", function() { @@ -44,4 +44,20 @@ describe("throttle(f, 1000)", function() { this.clock.restore(); }); -}); \ No newline at end of file +}); + +describe('throttle', () => { + + it('runs a forwarded call once', done => { + let log = ''; + const f = str => log += str; + const f10 = throttle(f, 10); + f10('once'); + + setTimeout(() => { + assert.equal(log, 'once'); + done(); + }, 20); + }); + +}); 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 372ebedd1e..6950664be1 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 @@ -1,4 +1,4 @@ -```js +```js demo function throttle(func, ms) { let isThrottled = false, @@ -12,11 +12,10 @@ function throttle(func, ms) { savedThis = this; return; } + isThrottled = true; func.apply(this, arguments); // (1) - isThrottled = true; - setTimeout(function() { isThrottled = false; // (3) if (savedArgs) { @@ -33,7 +32,7 @@ function throttle(func, ms) { A call to `throttle(func, ms)` returns `wrapper`. 1. During the first call, the `wrapper` just runs `func` and sets the cooldown state (`isThrottled = true`). -2. In this state all calls memorized in `savedArgs/savedThis`. Please note that both the context and the arguments are equally important and should be memorized. We need them simultaneously to reproduce the call. -3. ...Then after `ms` milliseconds pass, `setTimeout` triggers. The cooldown state is removed (`isThrottled = false`). And if we had ignored calls, then `wrapper` is executed with last memorized arguments and context. +2. In this state all calls are memorized in `savedArgs/savedThis`. Please note that both the context and the arguments are equally important and should be memorized. We need them simultaneously to reproduce the call. +3. After `ms` milliseconds pass, `setTimeout` triggers. The cooldown state is removed (`isThrottled = false`) and, if we had ignored calls, `wrapper` is executed with the last memorized arguments and context. The 3rd step runs not `func`, but `wrapper`, because we not only need to execute `func`, but once again enter the cooldown state and setup the timeout to reset it. 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 547f336c53..cbd4731960 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,35 +4,40 @@ importance: 5 # Throttle decorator -Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper, passing the call to `f` at maximum once per `ms` milliseconds. Those calls that fall into the "cooldown" period, are ignored. +Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper. -**The difference with `debounce` -- if an ignored call is the last during the cooldown, then it executes at the end of the delay.** +When it's called multiple times, it passes the call to `f` at maximum once per `ms` milliseconds. + +Compared to the debounce decorator, the behavior is completely different: +- `debounce` runs the function once after the "cooldown" period. Good for processing the final result. +- `throttle` runs it not more often than given `ms` time. Good for regular updates that shouldn't be very often. + +In other words, `throttle` is like a secretary that accepts phone calls, but bothers the boss (calls the actual `f`) not more often than once per `ms` milliseconds. Let's check the real-life application to better understand that requirement and to see where it comes from. **For instance, we want to track mouse movements.** -In browser we can setup a function to run at every mouse micro-movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms). - -**The tracking function should update some information on the web-page.** +In a browser we can setup a function to run at every mouse movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms). +**We'd like to update some information on the web-page when the pointer moves.** -Updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in making it more often than once per 100ms. +...But updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in updating more often than once per 100ms. -So we'll assign `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but `update()` will be called at maximum once per 100ms. +So we'll wrap it into the decorator: use `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but forward the call to `update()` at maximum once per 100ms. Visually, it will look like this: -1. For the first mouse movement the decorated variant passes the call to `update`. That's important, the user sees our reaction to their move immediately. +1. For the first mouse movement the decorated variant immediately passes the call to `update`. That's important, the user sees our reaction to their move immediately. 2. Then as the mouse moves on, until `100ms` nothing happens. The decorated variant ignores calls. -3. At the end of `100ms` -- one more `update` happens with the last coordinates. -4. Then, finally, the mouse stops somewhere. The decorated variant waits until `100ms` expire and then runs `update` runs with last coordinates. So, perhaps the most important, the final mouse coordinates are processed. +3. At the end of `100ms` -- one more `update` happens with the last coordinates. +4. Then, finally, the mouse stops somewhere. The decorated variant waits until `100ms` expire and then runs `update` with last coordinates. So, quite important, the final mouse coordinates are processed. A code example: ```js function f(a) { - console.log(a) -}; + console.log(a); +} // f1000 passes calls to f at maximum once per 1000 ms let f1000 = throttle(f, 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 3f86b0d307..c5d785493c 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 @@ -6,9 +6,9 @@ JavaScript gives exceptional flexibility when dealing with functions. They can b Let's say we have a function `slow(x)` which is CPU-heavy, but its results are stable. In other words, for the same `x` it always returns the same result. -If the function is called often, we may want to cache (remember) the results for different `x` to avoid spending extra-time on recalculations. +If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations. -But instead of adding that functionality into `slow()` we'll create a wrapper. As we'll see, there are many benefits of doing so. +But instead of adding that functionality into `slow()` we'll create a wrapper function, that adds caching. As we'll see, there are many benefits of doing so. Here's the code, and explanations follow: @@ -23,24 +23,24 @@ function cachingDecorator(func) { let cache = new Map(); return function(x) { - if (cache.has(x)) { // if the result is in the map - return cache.get(x); // return it + if (cache.has(x)) { // if there's such key in cache + return cache.get(x); // read the result from it } - let result = func(x); // otherwise call func + let result = func(x); // otherwise call func - cache.set(x, result); // and cache (remember) the result + cache.set(x, result); // and cache (remember) the result return result; }; } slow = cachingDecorator(slow); -alert( slow(1) ); // slow(1) is cached -alert( "Again: " + slow(1) ); // the same +alert( slow(1) ); // slow(1) is cached and the result returned +alert( "Again: " + slow(1) ); // slow(1) result returned from cache -alert( slow(2) ); // slow(2) is cached -alert( "Again: " + slow(2) ); // the same as the previous line +alert( slow(2) ); // slow(2) is cached and the result returned +alert( "Again: " + slow(2) ); // slow(2) result returned from cache ``` In the code above `cachingDecorator` is a *decorator*: a special function that takes another function and alters its behavior. @@ -49,21 +49,18 @@ The idea is that we can call `cachingDecorator` for any function, and it will re By separating caching from the main function code we also keep the main code simpler. -Now let's get into details of how it works. - The result of `cachingDecorator(func)` is a "wrapper": `function(x)` that "wraps" the call of `func(x)` into caching logic: -![](decorator-makecaching-wrapper.png) +![](decorator-makecaching-wrapper.svg) -As we can see, the wrapper returns the result of `func(x)` "as is". From an outside code, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior. +From an outside code, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior. To summarize, there are several benefits of using a separate `cachingDecorator` instead of altering the code of `slow` itself: - The `cachingDecorator` is reusable. We can apply it to another function. -- The caching logic is separate, it did not increase the complexity of `slow` itself (if there were any). +- The caching logic is separate, it did not increase the complexity of `slow` itself (if there was any). - We can combine multiple decorators if needed (other decorators will follow). - ## Using "func.call" for the context The caching decorator mentioned above is not suited to work with object methods. @@ -78,7 +75,7 @@ let worker = { }, slow(x) { - // actually, there can be a scary CPU-heavy task here + // scary CPU-heavy task here alert("Called with " + x); return x * this.someMethod(); // (*) } @@ -152,8 +149,8 @@ let user = { name: "John" }; let admin = { name: "Admin" }; // use call to pass different objects as "this" -sayHi.call( user ); // this = John -sayHi.call( admin ); // this = Admin +sayHi.call( user ); // John +sayHi.call( admin ); // Admin ``` And here we use `call` to call `say` with the given context and phrase: @@ -170,10 +167,8 @@ let user = { name: "John" }; say.call( user, "Hello" ); // John: Hello ``` - In our case, we can use `call` in the wrapper to pass the context to the original function: - ```js run let worker = { someMethod() { @@ -214,7 +209,7 @@ To make it all clear, let's see more deeply how `this` is passed along: 2. So when `worker.slow(2)` is executed, the wrapper gets `2` as an argument and `this=worker` (it's the object before dot). 3. Inside the wrapper, assuming the result is not yet cached, `func.call(this, x)` passes the current `this` (`=worker`) and the current argument (`=2`) to the original method. -## Going multi-argument with "func.apply" +## Going multi-argument Now let's make `cachingDecorator` even more universal. Till now it was working only with single-argument functions. @@ -231,9 +226,7 @@ let worker = { worker.slow = cachingDecorator(worker.slow); ``` -We have two tasks to solve here. - -First is how to use both arguments `min` and `max` for the key in `cache` map. Previously, for a single argument `x` we could just `cache.set(x, result)` to save the result and `cache.get(x)` to retrieve it. But now we need to remember the result for a *combination of arguments* `(min,max)`. The native `Map` takes single value only as the key. +Previously, for a single argument `x` we could just `cache.set(x, result)` to save the result and `cache.get(x)` to retrieve it. But now we need to remember the result for a *combination of arguments* `(min,max)`. The native `Map` takes single value only as the key. There are many solutions possible: @@ -241,85 +234,11 @@ There are many solutions possible: 2. Use nested maps: `cache.set(min)` will be a `Map` that stores the pair `(max, result)`. So we can get `result` as `cache.get(min).get(max)`. 3. Join two values into one. In our particular case we can just use a string `"min,max"` as the `Map` key. For flexibility, we can allow to provide a *hashing function* for the decorator, that knows how to make one value from many. - For many practical applications, the 3rd variant is good enough, so we'll stick to it. -The second task to solve is how to pass many arguments to `func`. Currently, the wrapper `function(x)` assumes a single argument, and `func.call(this, x)` passes it. - -Here we can use another built-in method [func.apply](mdn:js/Function/apply). - -The syntax is: - -```js -func.apply(context, args) -``` - -It runs the `func` setting `this=context` and using an array-like object `args` as the list of arguments. - - -For instance, these two calls are almost the same: - -```js -func(1, 2, 3); -func.apply(context, [1, 2, 3]) -``` - -Both run `func` giving it arguments `1,2,3`. But `apply` also sets `this=context`. - -For instance, here `say` is called with `this=user` and `messageData` as a list of arguments: - -```js run -function say(time, phrase) { - alert(`[${time}] ${this.name}: ${phrase}`); -} - -let user = { name: "John" }; - -let messageData = ['10:00', 'Hello']; // become time and phrase - -*!* -// user becomes this, messageData is passed as a list of arguments (time, phrase) -say.apply(user, messageData); // [10:00] John: Hello (this=user) -*/!* -``` - -The only syntax difference between `call` and `apply` is that `call` expects a list of arguments, while `apply` takes an array-like object with them. - -We already know the spread operator `...` from the chapter that can pass an array (or any iterable) as a list of arguments. So if we use it with `call`, we can achieve almost the same as `apply`. - -These two calls are almost equivalent: - -```js -let args = [1, 2, 3]; - -*!* -func.call(context, ...args); // pass an array as list with spread operator -func.apply(context, args); // is same as using apply -*/!* -``` - -If we look more closely, there's a minor difference between such uses of `call` and `apply`. - -- The spread operator `...` allows to pass *iterable* `args` as the list to `call`. -- The `apply` accepts only *array-like* `args`. - -So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works. - -And if `args` is both iterable and array-like, like a real array, then we technically could use any of them, but `apply` will probably be faster, because it's a single operation. Most JavaScript engines internally optimize is better than a pair `call + spread`. - -One of the most important uses of `apply` is passing the call to another function, like this: - -```js -let wrapper = function() { - return anotherFunction.apply(this, arguments); -}; -``` - -That's called *call forwarding*. The `wrapper` passes everything it gets: the context `this` and arguments to `anotherFunction` and returns back its result. +Also we need to pass not just `x`, but all arguments in `func.call`. Let's recall that in a `function()` we can get a pseudo-array of its arguments as `arguments`, so `func.call(this, x)` should be replaced with `func.call(this, ...arguments)`. -When an external code calls such `wrapper`, it is indistinguishable from the call of the original function. - -Now let's bake it all into the more powerful `cachingDecorator`: +Here's a more powerful `cachingDecorator`: ```js run let worker = { @@ -340,7 +259,7 @@ function cachingDecorator(func, hash) { } *!* - let result = func.apply(this, arguments); // (**) + let result = func.call(this, ...arguments); // (**) */!* cache.set(key, result); @@ -358,13 +277,54 @@ alert( worker.slow(3, 5) ); // works alert( "Again " + worker.slow(3, 5) ); // same (cached) ``` -Now the wrapper operates with any number of arguments. +Now it works with any number of arguments (though the hash function would also need to be adjusted to allow any number of arguments. An interesting way to handle this will be covered below). There are two changes: - In the line `(*)` it calls `hash` to create a single key from `arguments`. Here we use a simple "joining" function that turns arguments `(3, 5)` into the key `"3,5"`. More complex cases may require other hashing functions. -- Then `(**)` uses `func.apply` to pass both the context and all arguments the wrapper got (no matter how many) to the original function. +- Then `(**)` uses `func.call(this, ...arguments)` to pass both the context and all arguments the wrapper got (not just the first one) to the original function. + +## func.apply + +Instead of `func.call(this, ...arguments)` we could use `func.apply(this, arguments)`. + +The syntax of built-in method [func.apply](mdn:js/Function/apply) is: + +```js +func.apply(context, args) +``` + +It runs the `func` setting `this=context` and using an array-like object `args` as the list of arguments. + +The only syntax difference between `call` and `apply` is that `call` expects a list of arguments, while `apply` takes an array-like object with them. + +So these two calls are almost equivalent: + +```js +func.call(context, ...args); +func.apply(context, args); +``` + +They perform the same call of `func` with given context and arguments. + +There's only a subtle difference regarding `args`: + +- The spread syntax `...` allows to pass *iterable* `args` as the list to `call`. +- The `apply` accepts only *array-like* `args`. +...And for objects that are both iterable and array-like, such as a real array, we can use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better. + +Passing all arguments along with the context to another function is called *call forwarding*. + +That's the simplest form of it: + +```js +let wrapper = function() { + return func.apply(this, arguments); +}; +``` + +When an external code calls such `wrapper`, it is indistinguishable from the call of the original function `func`. ## Borrowing a method [#method-borrowing] @@ -386,7 +346,7 @@ function hash(args) { } ``` -...Unfortunately, that won't work. Because we are calling `hash(arguments)` and `arguments` object is both iterable and array-like, but not a real array. +...Unfortunately, that won't work. Because we are calling `hash(arguments)`, and `arguments` object is both iterable and array-like, but not a real array. So calling `join` on it would fail, as we can see below: @@ -414,7 +374,7 @@ hash(1, 2); The trick is called *method borrowing*. -We take (borrow) a join method from a regular array `[].join`. And use `[].join.call` to run it in the context of `arguments`. +We take (borrow) a join method from a regular array (`[].join`) and use `[].join.call` to run it in the context of `arguments`. Why does it work? @@ -432,12 +392,20 @@ Taken from the specification almost "as-is": So, technically it takes `this` and joins `this[0]`, `this[1]` ...etc together. It's intentionally written in a way that allows any array-like `this` (not a coincidence, many methods follow this practice). That's why it also works with `this=arguments`. +## Decorators and function properties + +It is generally safe to replace a function or a method with a decorated one, except for one little thing. If the original function had properties on it, like `func.calledCount` or whatever, then the decorated one will not provide them. Because that is a wrapper. So one needs to be careful if one uses them. + +E.g. in the example above if `slow` function had any properties on it, then `cachingDecorator(slow)` is a wrapper without them. + +Some decorators may provide their own properties. E.g. a decorator may count how many times a function was invoked and how much time it took, and expose this information via wrapper properties. + +There exists a way to create decorators that keep access to function properties, but this requires using a special `Proxy` object to wrap a function. We'll discuss it later in the article . + ## Summary *Decorator* is a wrapper around a function that alters its behavior. The main job is still carried out by the function. -It is generally safe to replace a function or a method with a decorated one, except for one little thing. If the original function had properties on it, like `func.calledCount` or whatever, then the decorated one will not provide them. Because that is a wrapper. So one needs to be careful if one uses them. Some decorators provide their own properties. - Decorators can be seen as "features" or "aspects" that can be added to a function. We can add one or add many. And all this without changing its code! To implement `cachingDecorator`, we studied methods: @@ -450,10 +418,9 @@ The generic *call forwarding* is usually done with `apply`: ```js let wrapper = function() { return original.apply(this, arguments); -} +}; ``` -We also saw an example of *method borrowing* when we take a method from an object and `call` it in the context of another object. It is quite common to take array methods and apply them to arguments. The alternative is to use rest parameters object that is a real array. - +We also saw an example of *method borrowing* when we take a method from an object and `call` it in the context of another object. It is quite common to take array methods and apply them to `arguments`. The alternative is to use rest parameters object that is a real array. There are many decorators there in the wild. Check how well you got them by solving the tasks of this chapter. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png deleted file mode 100644 index 171e27910d..0000000000 Binary files a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png and /dev/null differ diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg new file mode 100644 index 0000000000..9b63cb982b --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg @@ -0,0 +1 @@ +wrapperaround the function \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png deleted file mode 100644 index 8d3b5434c2..0000000000 Binary files a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md b/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md index 8cd18ec56c..d6cfb44bf8 100644 --- a/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md +++ b/1-js/06-advanced-functions/10-bind/4-function-property-after-bind/task.md @@ -4,7 +4,7 @@ importance: 5 # Function property after bind -There's a value in the property of a function. Will it change after `bind`? Why, elaborate? +There's a value in the property of a function. Will it change after `bind`? Why, or why not? ```js run function sayHi() { diff --git a/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md b/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md index 0cb673b12b..4a381c0b40 100644 --- a/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md +++ b/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md @@ -1,5 +1,5 @@ -The error occurs because `ask` gets functions `loginOk/loginFail` without the object. +The error occurs because `askPassword` gets functions `loginOk/loginFail` without the object. When it calls them, they naturally assume `this=undefined`. @@ -38,6 +38,6 @@ An alternative solution could be: askPassword(() => user.loginOk(), () => user.loginFail()); ``` -Usually that also works, but may fail in more complex situations where `user` has a chance of being overwritten between the moments of asking and running `() => user.loginOk()`. - +Usually that also works and looks good. +It's a bit less reliable though in more complex situations where `user` variable might change *after* `askPassword` is called, but *before* the visitor answers and calls `() => user.loginOk()`. diff --git a/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md b/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md index eb19e6644d..fe6a9b4eb9 100644 --- a/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md +++ b/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md @@ -2,7 +2,7 @@ importance: 5 --- -# Ask losing this +# Fix a function that loses "this" The call to `askPassword()` in the code below should check the password and then call `user.loginOk/loginFail` depending on the answer. @@ -34,5 +34,3 @@ let user = { askPassword(user.loginOk, user.loginFail); */!* ``` - - diff --git a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md b/1-js/06-advanced-functions/10-bind/6-ask-partial/solution.md similarity index 100% rename from 1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md rename to 1-js/06-advanced-functions/10-bind/6-ask-partial/solution.md diff --git a/1-js/06-advanced-functions/10-bind/6-ask-partial/task.md b/1-js/06-advanced-functions/10-bind/6-ask-partial/task.md new file mode 100644 index 0000000000..c90851c2bd --- /dev/null +++ b/1-js/06-advanced-functions/10-bind/6-ask-partial/task.md @@ -0,0 +1,34 @@ +importance: 5 + +--- + +# Partial application for login + +The task is a little more complex variant of . + +The `user` object was modified. Now instead of two functions `loginOk/loginFail`, it has a single function `user.login(true/false)`. + +What should we pass `askPassword` in the code below, so that it calls `user.login(true)` as `ok` and `user.login(false)` as `fail`? + +```js +function askPassword(ok, fail) { + let password = prompt("Password?", ''); + if (password == "rockstar") ok(); + else fail(); +} + +let user = { + name: 'John', + + login(result) { + alert( this.name + (result ? ' logged in' : ' failed to log in') ); + } +}; + +*!* +askPassword(?, ?); // ? +*/!* +``` + +Your changes should only modify the highlighted fragment. + diff --git a/1-js/06-advanced-functions/10-bind/article.md b/1-js/06-advanced-functions/10-bind/article.md index f93690963c..6d65e7dd10 100644 --- a/1-js/06-advanced-functions/10-bind/article.md +++ b/1-js/06-advanced-functions/10-bind/article.md @@ -5,13 +5,13 @@ libs: # Function binding -When using `setTimeout` with object methods or passing object methods along, there's a known problem: "losing `this`". +When passing object methods as callbacks, for instance to `setTimeout`, there's a known problem: "losing `this`". -Suddenly, `this` just stops working right. The situation is typical for novice developers, but happens with experienced ones as well. +In this chapter we'll see the ways to fix it. ## Losing "this" -We already know that in JavaScript it's easy to lose `this`. Once a method is passed somewhere separately from the object -- `this` is lost. +We've already seen examples of losing `this`. Once a method is passed somewhere separately from the object -- `this` is lost. Here's how it may happen with `setTimeout`: @@ -37,13 +37,13 @@ let f = user.sayHi; setTimeout(f, 1000); // lost user context ``` -The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.JS, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases as we'll see, usually `this` just becomes `undefined`. +The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.js, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases, usually `this` just becomes `undefined`. The task is quite typical -- we want to pass an object method somewhere else (here -- to the scheduler) where it will be called. How to make sure that it will be called in the right context? ## Solution 1: a wrapper -The simplest solution is to use an wrapping function: +The simplest solution is to use a wrapping function: ```js run let user = { @@ -83,10 +83,12 @@ let user = { setTimeout(() => user.sayHi(), 1000); -// ...within 1 second -user = { sayHi() { alert("Another user in setTimeout!"); } }; +// ...the value of user changes within 1 second +user = { + sayHi() { alert("Another user in setTimeout!"); } +}; -// Another user in setTimeout?!? +// Another user in setTimeout! ``` The next solution guarantees that such thing won't happen. @@ -98,9 +100,9 @@ Functions provide a built-in method [bind](mdn:js/Function/bind) that allows to The basic syntax is: ```js -// more complex syntax will be little later +// more complex syntax will come a little later let boundFunc = func.bind(context); -```` +``` The result of `func.bind(context)` is a special function-like "exotic object", that is callable as function and transparently passes the call to `func` setting `this=context`. @@ -159,9 +161,16 @@ let user = { let sayHi = user.sayHi.bind(user); // (*) */!* +// can run it without an object sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! + +// even if the value of user changes within 1 second +// sayHi uses the pre-bound value which is reference to the old user object +user = { + sayHi() { alert("Another user in setTimeout!"); } +}; ``` In the line `(*)` we take the method `user.sayHi` and bind it to `user`. The `sayHi` is a "bound" function, that can be called alone or passed to `setTimeout` -- doesn't matter, the context will be right. @@ -178,8 +187,8 @@ let user = { let say = user.say.bind(user); -say("Hello"); // Hello, John ("Hello" argument is passed to say) -say("Bye"); // Bye, John ("Bye" is passed to say) +say("Hello"); // Hello, John! ("Hello" argument is passed to say) +say("Bye"); // Bye, John! ("Bye" is passed to say) ``` ````smart header="Convenience method: `bindAll`" @@ -193,11 +202,127 @@ for (let key in user) { } ``` -JavaScript libraries also provide functions for convenient mass binding , e.g. [_.bindAll(obj)](http://lodash.com/docs#bindAll) in lodash. +JavaScript libraries also provide functions for convenient mass binding , e.g. [_.bindAll(object, methodNames)](https://lodash.com/docs#bindAll) in lodash. ```` +## Partial functions + +Until now we have only been talking about binding `this`. Let's take it a step further. + +We can bind not only `this`, but also arguments. That's rarely done, but sometimes can be handy. + +The full syntax of `bind`: + +```js +let bound = func.bind(context, [arg1], [arg2], ...); +``` + +It allows to bind context as `this` and starting arguments of the function. + +For instance, we have a multiplication function `mul(a, b)`: + +```js +function mul(a, b) { + return a * b; +} +``` + +Let's use `bind` to create a function `double` on its base: + +```js run +function mul(a, b) { + return a * b; +} + +*!* +let double = mul.bind(null, 2); +*/!* + +alert( double(3) ); // = mul(2, 3) = 6 +alert( double(4) ); // = mul(2, 4) = 8 +alert( double(5) ); // = mul(2, 5) = 10 +``` + +The call to `mul.bind(null, 2)` creates a new function `double` that passes calls to `mul`, fixing `null` as the context and `2` as the first argument. Further arguments are passed "as is". + +That's called [partial function application](https://en.wikipedia.org/wiki/Partial_application) -- we create a new function by fixing some parameters of the existing one. + +Please note that we actually don't use `this` here. But `bind` requires it, so we must put in something like `null`. + +The function `triple` in the code below triples the value: + +```js run +function mul(a, b) { + return a * b; +} + +*!* +let triple = mul.bind(null, 3); +*/!* + +alert( triple(3) ); // = mul(3, 3) = 9 +alert( triple(4) ); // = mul(3, 4) = 12 +alert( triple(5) ); // = mul(3, 5) = 15 +``` + +Why do we usually make a partial function? + +The benefit is that we can create an independent function with a readable name (`double`, `triple`). We can use it and not provide the first argument every time as it's fixed with `bind`. + +In other cases, partial application is useful when we have a very generic function and want a less universal variant of it for convenience. + +For instance, we have a function `send(from, to, text)`. Then, inside a `user` object we may want to use a partial variant of it: `sendTo(to, text)` that sends from the current user. + +## Going partial without context + +What if we'd like to fix some arguments, but not the context `this`? For example, for an object method. + +The native `bind` does not allow that. We can't just omit the context and jump to arguments. + +Fortunately, a function `partial` for binding only arguments can be easily implemented. + +Like this: + +```js run +*!* +function partial(func, ...argsBound) { + return function(...args) { // (*) + return func.call(this, ...argsBound, ...args); + } +} +*/!* + +// Usage: +let user = { + firstName: "John", + say(time, phrase) { + alert(`[${time}] ${this.firstName}: ${phrase}!`); + } +}; + +// add a partial method with fixed time +user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); + +user.sayNow("Hello"); +// Something like: +// [10:00] John: Hello! +``` + +The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that calls `func` with: +- Same `this` as it gets (for `user.sayNow` call it's `user`) +- Then gives it `...argsBound` -- arguments from the `partial` call (`"10:00"`) +- Then gives it `...args` -- arguments given to the wrapper (`"Hello"`) + +So easy to do it with the spread syntax, right? + +Also there's a ready [_.partial](https://lodash.com/docs#partial) implementation from lodash library. + ## Summary Method `func.bind(context, ...args)` returns a "bound variant" of function `func` that fixes the context `this` and first arguments if given. -Usually we apply `bind` to fix `this` in an object method, so that we can pass it somewhere. For example, to `setTimeout`. There are more reasons to `bind` in the modern development, we'll meet them later. +Usually we apply `bind` to fix `this` for an object method, so that we can pass it somewhere. For example, to `setTimeout`. + +When we fix some arguments of an existing function, the resulting (less universal) function is called *partially applied* or *partial*. + +Partials are convenient when we don't want to repeat the same argument over and over again. Like if we have a `send(from, to)` function, and `from` should always be the same for our task, we can get a partial and go on with it. diff --git a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md b/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md deleted file mode 100644 index f8b83d7a20..0000000000 --- a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md +++ /dev/null @@ -1,34 +0,0 @@ -importance: 5 - ---- - -# Partial application for login - -The task is a little more complex variant of . - -The `user` object was modified. Now instead of two functions `loginOk/loginFail`, it has a single function `user.login(true/false)`. - -What to pass `askPassword` in the code below, so that it calls `user.login(true)` as `ok` and `user.login(false)` as `fail`? - -```js -function askPassword(ok, fail) { - let password = prompt("Password?", ''); - if (password == "rockstar") ok(); - else fail(); -} - -let user = { - name: 'John', - - login(result) { - alert( this.name + (result ? ' logged in' : ' failed to log in') ); - } -}; - -*!* -askPassword(?, ?); // ? -*/!* -``` - -Your changes should only modify the highlighted fragment. - diff --git a/1-js/06-advanced-functions/11-currying-partials/article.md b/1-js/06-advanced-functions/11-currying-partials/article.md deleted file mode 100644 index e866f38a75..0000000000 --- a/1-js/06-advanced-functions/11-currying-partials/article.md +++ /dev/null @@ -1,296 +0,0 @@ -libs: - - lodash - ---- - -# Currying and partials - -Until now we have only been talking about binding `this`. Let's take it a step further. - -We can bind not only `this`, but also arguments. That's rarely done, but sometimes can be handy. - -The full syntax of `bind`: - -```js -let bound = func.bind(context, arg1, arg2, ...); -``` - -It allows to bind context as `this` and starting arguments of the function. - -For instance, we have a multiplication function `mul(a, b)`: - -```js -function mul(a, b) { - return a * b; -} -``` - -Let's use `bind` to create a function `double` on its base: - -```js run -*!* -let double = mul.bind(null, 2); -*/!* - -alert( double(3) ); // = mul(2, 3) = 6 -alert( double(4) ); // = mul(2, 4) = 8 -alert( double(5) ); // = mul(2, 5) = 10 -``` - -The call to `mul.bind(null, 2)` creates a new function `double` that passes calls to `mul`, fixing `null` as the context and `2` as the first argument. Further arguments are passed "as is". - -That's called [partial function application](https://en.wikipedia.org/wiki/Partial_application) -- we create a new function by fixing some parameters of the existing one. - -Please note that here we actually don't use `this` here. But `bind` requires it, so we must put in something like `null`. - -The function `triple` in the code below triples the value: - -```js run -*!* -let triple = mul.bind(null, 3); -*/!* - -alert( triple(3) ); // = mul(3, 3) = 9 -alert( triple(4) ); // = mul(3, 4) = 12 -alert( triple(5) ); // = mul(3, 5) = 15 -``` - -Why do we usually make a partial function? - -Here our benefit is that we created an independent function with a readable name (`double`, `triple`). We can use it and don't write the first argument of every time, cause it's fixed with `bind`. - -In other cases, partial application is useful when we have a very generic function, and want a less universal variant of it for convenience. - -For instance, we have a function `send(from, to, text)`. Then, inside a `user` object we may want to use a partial variant of it: `sendTo(to, text)` that sends from the current user. - -## Going partial without context - -What if we'd like to fix some arguments, but not bind `this`? - -The native `bind` does not allow that. We can't just omit the context and jump to arguments. - -Fortunately, a `partial` function for binding only arguments can be easily implemented. - -Like this: - -```js run -*!* -function partial(func, ...argsBound) { - return function(...args) { // (*) - return func.call(this, ...argsBound, ...args); - } -} -*/!* - -// Usage: -let user = { - firstName: "John", - say(time, phrase) { - alert(`[${time}] ${this.firstName}: ${phrase}!`); - } -}; - -// add a partial method that says something now by fixing the first argument -user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); - -user.sayNow("Hello"); -// Something like: -// [10:00] John: Hello! -``` - -The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that calls `func` with: -- Same `this` as it gets (for `user.sayNow` call it's `user`) -- Then gives it `...argsBound` -- arguments from the `partial` call (`"10:00"`) -- Then gives it `...args` -- arguments given to the wrapper (`"Hello"`) - -So easy to do it with the spread operator, right? - -Also there's a ready [_.partial](https://lodash.com/docs#partial) implementation from lodash library. - -## Currying - -Sometimes people mix up partial function application mentioned above with another thing named "currying". That's another interesting technique of working with functions that we just have to mention here. - -[Currying](https://en.wikipedia.org/wiki/Currying) is translating a function from callable as `f(a, b, c)` into callable as `f(a)(b)(c)`. - -Let's make `curry` function that performs currying for binary functions. In other words, it translates `f(a, b)` into `f(a)(b)`: - -```js run -*!* -function curry(func) { - return function(a) { - return function(b) { - return func(a, b); - }; - }; -} -*/!* - -// usage -function sum(a, b) { - return a + b; -} - -let carriedSum = curry(sum); - -alert( carriedSum(1)(2) ); // 3 -``` - -As you can see, the implementation is a series of wrappers. - -- The result of `curry(func)` is a wrapper `function(a)`. -- When it is called like `sum(1)`, the argument is saved in the Lexical Environment, and a new wrapper is returned `function(b)`. -- Then `sum(1)(2)` finally calls `function(b)` providing `2`, and it passes the call to the original multi-argument `sum`. - -More advanced implementations of currying like [_.curry](https://lodash.com/docs#curry) from lodash library do something more sophisticated. They return a wrapper that allows a function to be called normally when all arguments are supplied *or* returns a partial otherwise. - -```js -function curry(f) { - return function(...args) { - // if args.length == f.length (as many arguments as f has), - // then pass the call to f - // otherwise return a partial function that fixes args as first arguments - }; -} -``` - -## Currying? What for? - -Advanced currying allows both to keep the function callable normally and to get partials easily. To understand the benefits we definitely need a worthy real-life example. - -For instance, we have the logging function `log(date, importance, message)` that formats and outputs the information. In real projects such functions also have many other useful features like: sending it over the network or filtering: - -```js -function log(date, importance, message) { - alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); -} -``` - -Let's curry it! - -```js -log = _.curry(log); -``` - -After that `log` still works the normal way: - -```js -log(new Date(), "DEBUG", "some debug"); -``` - -...But also can be called in the curried form: - -```js -log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) -``` - -Let's get a convenience function for today's logs: - -```js -// todayLog will be the partial of log with fixed first argument -let todayLog = log(new Date()); - -// use it -todayLog("INFO", "message"); // [HH:mm] INFO message -``` - -And now a convenience function for today's debug messages: - -```js -let todayDebug = todayLog("DEBUG"); - -todayDebug("message"); // [HH:mm] DEBUG message -``` - -So: -1. We didn't lose anything after currying: `log` is still callable normally. -2. We were able to generate partial functions that are convenient in many cases. - -## Advanced curry implementation - -In case you're interested, here's the "advanced" curry implementation that we could use above. - -```js run -function curry(func) { - - return function curried(...args) { - if (args.length >= func.length) { - return func.apply(this, args); - } else { - return function(...args2) { - return curried.apply(this, args.concat(args2)); - } - } - }; - -} - -function sum(a, b, c) { - return a + b + c; -} - -let curriedSum = curry(sum); - -// still callable normally -alert( curriedSum(1, 2, 3) ); // 6 - -// get the partial with curried(1) and call it with 2 other arguments -alert( curriedSum(1)(2,3) ); // 6 - -// full curried form -alert( curriedSum(1)(2)(3) ); // 6 -``` - -The new `curry` may look complicated, but it's actually pretty easy to understand. - -The result of `curry(func)` is the wrapper `curried` that looks like this: - -```js -// func is the function to transform -function curried(...args) { - if (args.length >= func.length) { // (1) - return func.apply(this, args); - } else { - return function pass(...args2) { // (2) - return curried.apply(this, args.concat(args2)); - } - } -}; -``` - -When we run it, there are two branches: - -1. Call now: if passed `args` count is the same as the original function has in its definition (`func.length`) or longer, then just pass the call to it. -2. Get a partial: otherwise, `func` is not called yet. Instead, another wrapper `pass` is returned, that will re-apply `curried` providing previous arguments together with the new ones. Then on a new call, again, we'll get either a new partial (if not enough arguments) or, finally, the result. - -For instance, let's see what happens in the case of `sum(a, b, c)`. Three arguments, so `sum.length = 3`. - -For the call `curried(1)(2)(3)`: - -1. The first call `curried(1)` remembers `1` in its Lexical Environment, and returns a wrapper `pass`. -2. The wrapper `pass` is called with `(2)`: it takes previous args (`1`), concatenates them with what it got `(2)` and calls `curried(1, 2)` with them together. - - As the argument count is still less than 3, `curry` returns `pass`. -3. The wrapper `pass` is called again with `(3)`, for the next call `pass(3)` takes previous args (`1`, `2`) and adds `3` to them, making the call `curried(1, 2, 3)` -- there are `3` arguments at last, they are given to the original function. - -If that's still not obvious, just trace the calls sequence in your mind or on the paper. - -```smart header="Fixed-length functions only" -The currying requires the function to have a known fixed number of arguments. -``` - -```smart header="A little more than currying" -By definition, currying should convert `sum(a, b, c)` into `sum(a)(b)(c)`. - -But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant. -``` - -## Summary - -- When we fix some arguments of an existing function, the resulting (less universal) function is called *a partial*. We can use `bind` to get a partial, but there are other ways also. - - Partials are convenient when we don't want to repeat the same argument over and over again. Like if we have a `send(from, to)` function, and `from` should always be the same for our task, we can get a partial and go on with it. - -- *Currying* is a transform that makes `f(a,b,c)` callable as `f(a)(b)(c)`. JavaScript implementations usually both keep the function callable normally and return the partial if arguments count is not enough. - - Currying is great when we want easy partials. As we've seen in the logging example: the universal function `log(date, importance, message)` after currying gives us partials when called with one argument like `log(date)` or two arguments `log(date, importance)`. diff --git a/1-js/06-advanced-functions/12-arrow-functions/article.md b/1-js/06-advanced-functions/12-arrow-functions/article.md index 1ade1a4192..8730277ad7 100644 --- a/1-js/06-advanced-functions/12-arrow-functions/article.md +++ b/1-js/06-advanced-functions/12-arrow-functions/article.md @@ -2,9 +2,9 @@ Let's revisit arrow functions. -Arrow functions are not just a "shorthand" for writing small stuff. +Arrow functions are not just a "shorthand" for writing small stuff. They have some very specific and useful features. -JavaScript is full of situations where we need to write a small function, that's executed somewhere else. +JavaScript is full of situations where we need to write a small function that's executed somewhere else. For instance: @@ -14,7 +14,7 @@ For instance: It's in the very spirit of JavaScript to create a function and pass it somewhere. -And in such functions we usually don't want to leave the current context. +And in such functions we usually don't want to leave the current context. That's where arrow functions come in handy. ## Arrow functions have no "this" @@ -52,7 +52,7 @@ let group = { *!* this.students.forEach(function(student) { // Error: Cannot read property 'title' of undefined - alert(this.title + ': ' + student) + alert(this.title + ': ' + student); }); */!* } @@ -87,7 +87,7 @@ For instance, `defer(f, ms)` gets a function and returns a wrapper around it tha ```js run function defer(f, ms) { return function() { - setTimeout(() => f.apply(this, arguments), ms) + setTimeout(() => f.apply(this, arguments), ms); }; } @@ -118,9 +118,9 @@ Here we had to create additional variables `args` and `ctx` so that the function Arrow functions: -- Do not have `this`. -- Do not have `arguments`. -- Can't be called with `new`. -- (They also don't have `super`, but we didn't study it. Will be in the chapter ). +- Do not have `this` +- Do not have `arguments` +- Can't be called with `new` +- They also don't have `super`, but we didn't study it yet. We will on the chapter -That's because they are meant for short pieces of code that do not have their own "context", but rather works in the current one. And they really shine in that use case. +That's because they are meant for short pieces of code that do not have their own "context", but rather work in the current one. And they really shine in that use case. diff --git a/1-js/07-object-oriented-programming/01-property-descriptors/article.md b/1-js/07-object-oriented-programming/01-property-descriptors/article.md deleted file mode 100644 index b189660664..0000000000 --- a/1-js/07-object-oriented-programming/01-property-descriptors/article.md +++ /dev/null @@ -1,318 +0,0 @@ - -# Property flags and descriptors - -As we know, objects can store properties. - -Till now, a property was a simple "key-value" pair to us. But an object property is actually a more complex and tunable thing. - -## Property flags - -Object properties, besides a **`value`**, have three special attributes (so-called "flags"): - -- **`writable`** -- if `true`, can be changed, otherwise it's read-only. -- **`enumerable`** -- if `true`, then listed in loops, otherwise not listed. -- **`configurable`** -- if `true`, the property can be deleted and these attributes can be modified, otherwise not. - -We didn't see them yet, because generally they do not show up. When we create a property "the usual way", all of them are `true`. But we also can change them anytime. - -First, let's see how to get those flags. - -The method [Object.getOwnPropertyDescriptor](mdn:js/Object/getOwnPropertyDescriptor) allows to query the *full* information about a property. - -The syntax is: -```js -let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); -``` - -`obj` -: The object to get information from. - -`propertyName` -: The name of the property. - -The returned value is a so-called "property descriptor" object: it contains the value and all the flags. - -For instance: - -```js run -let user = { - name: "John" -}; - -let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); - -alert( JSON.stringify(descriptor, null, 2 ) ); -/* property descriptor: -{ - "value": "John", - "writable": true, - "enumerable": true, - "configurable": true -} -*/ -``` - -To change the flags, we can use [Object.defineProperty](mdn:js/Object/defineProperty). - -The syntax is: - -```js -Object.defineProperty(obj, propertyName, descriptor) -``` - -`obj`, `propertyName` -: The object and property to work on. - -`descriptor` -: Property descriptor to apply. - -If the property exists, `defineProperty` updates its flags. Otherwise, it creates the property with the given value and flags; in that case, if a flag is not supplied, it is assumed `false`. - -For instance, here a property `name` is created with all falsy flags: - -```js run -let user = {}; - -*!* -Object.defineProperty(user, "name", { - value: "John" -}); -*/!* - -let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); - -alert( JSON.stringify(descriptor, null, 2 ) ); -/* -{ - "value": "John", -*!* - "writable": false, - "enumerable": false, - "configurable": false -*/!* -} - */ -``` - -Compare it with "normally created" `user.name` above: now all flags are falsy. If that's not what we want then we'd better set them to `true` in `descriptor`. - -Now let's see effects of the flags by example. - -## Read-only - -Let's make `user.name` read-only by changing `writable` flag: - -```js run -let user = { - name: "John" -}; - -Object.defineProperty(user, "name", { -*!* - writable: false -*/!* -}); - -*!* -user.name = "Pete"; // Error: Cannot assign to read only property 'name'... -*/!* -``` - -Now no one can change the name of our user, unless they apply their own `defineProperty` to override ours. - -Here's the same operation, but for the case when a property doesn't exist: - -```js run -let user = { }; - -Object.defineProperty(user, "name", { -*!* - value: "Pete", - // for new properties need to explicitly list what's true - enumerable: true, - configurable: true -*/!* -}); - -alert(user.name); // Pete -user.name = "Alice"; // Error -``` - - -## Non-enumerable - -Now let's add a custom `toString` to `user`. - -Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. But if we add `toString` of our own, then by default it shows up in `for..in`, like this: - -```js run -let user = { - name: "John", - toString() { - return this.name; - } -}; - -// By default, both our properties are listed: -for (let key in user) alert(key); // name, toString -``` - -If we don't like it, then we can set `enumerable:false`. Then it won't appear in `for..in` loop, just like the built-in one: - -```js run -let user = { - name: "John", - toString() { - return this.name; - } -}; - -Object.defineProperty(user, "toString", { -*!* - enumerable: false -*/!* -}); - -*!* -// Now our toString disappears: -*/!* -for (let key in user) alert(key); // name -``` - -Non-enumerable properties are also excluded from `Object.keys`: - -```js -alert(Object.keys(user)); // name -``` - -## Non-configurable - -The non-configurable flag (`configurable:false`) is sometimes preset for built-in objects and properties. - -A non-configurable property can not be deleted or altered with `defineProperty`. - -For instance, `Math.PI` is both read-only, non-enumerable and non-configurable: - -```js run -let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI'); - -alert( JSON.stringify(descriptor, null, 2 ) ); -/* -{ - "value": 3.141592653589793, - "writable": false, - "enumerable": false, - "configurable": false -} -*/ -``` -So, a programmer is unable to change the value of `Math.PI` or overwrite it. - -```js run -Math.PI = 3; // Error - -// delete Math.PI won't work either -``` - -Making a property non-configurable is a one-way road. We cannot change it back, because `defineProperty` doesn't work on non-configurable properties. - -Here we are making `user.name` a "forever sealed" constant: - -```js run -let user = { }; - -Object.defineProperty(user, "name", { - value: "John", - writable: false, - configurable: false -}); - -*!* -// won't be able to change user.name or its flags -// all this won't work: -// user.name = "Pete" -// delete user.name -// defineProperty(user, "name", ...) -Object.defineProperty(user, "name", {writable: true}); // Error -*/!* -``` - -```smart header="Errors appear only in use strict" -In the non-strict mode, no errors occur when writing to read-only properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict. -``` - -## Object.defineProperties - -There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once. - -The syntax is: - -```js -Object.defineProperties(obj, { - prop1: descriptor1, - prop2: descriptor2 - // ... -}); -``` - -For instance: - -```js -Object.defineProperties(user, { - name: { value: "John", writable: false }, - surname: { value: "Smith", writable: false }, - // ... -}); -``` - -So, we can set many properties at once. - -## Object.getOwnPropertyDescriptors - -To get all property descriptors at once, we can use the method [Object.getOwnPropertyDescriptors(obj)](mdn:js/Object/getOwnPropertyDescriptors). - -Together with `Object.defineProperties` it can be used as a "flags-aware" way of cloning an object: - -```js -let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj)); -``` - -Normally when we clone an object, we use an assignment to copy properties, like this: - -```js -for (let key in user) { - clone[key] = user[key] -} -``` - -...But that does not copy flags. So if we want a "better" clone then `Object.defineProperties` is preferred. - -Another difference is that `for..in` ignores symbolic properties, but `Object.getOwnPropertyDescriptors` returns *all* property descriptors including symbolic ones. - -## Sealing an object globally - -Property descriptors work at the level of individual properties. - -There are also methods that limit access to the *whole* object: - -[Object.preventExtensions(obj)](mdn:js/Object/preventExtensions) -: Forbids to add properties to the object. - -[Object.seal(obj)](mdn:js/Object/seal) -: Forbids to add/remove properties, sets for all existing properties `configurable: false`. - -[Object.freeze(obj)](mdn:js/Object/freeze) -: Forbids to add/remove/change properties, sets for all existing properties `configurable: false, writable: false`. - -And also there are tests for them: - -[Object.isExtensible(obj)](mdn:js/Object/isExtensible) -: Returns `false` if adding properties is forbidden, otherwise `true`. - -[Object.isSealed(obj)](mdn:js/Object/isSealed) -: Returns `true` if adding/removing properties is forbidden, and all existing properties have `configurable: false`. - -[Object.isFrozen(obj)](mdn:js/Object/isFrozen) -: Returns `true` if adding/removing/changing properties is forbidden, and all current properties are `configurable: false, writable: false`. - -These methods are rarely used in practice. diff --git a/1-js/07-object-oriented-programming/02-property-accessors/article.md b/1-js/07-object-oriented-programming/02-property-accessors/article.md deleted file mode 100644 index fa4d2216d5..0000000000 --- a/1-js/07-object-oriented-programming/02-property-accessors/article.md +++ /dev/null @@ -1,237 +0,0 @@ - -# Property getters and setters - -There are two kinds of properties. - -The first kind is *data properties*. We already know how to work with them. Actually, all properties that we've been using till now were data properties. - -The second type of properties is something new. It's *accessor properties*. They are essentially functions that work on getting and setting a value, but look like regular properties to an external code. - -## Getters and setters - -Accessor properties are represented by "getter" and "setter" methods. In an object literal they are denoted by `get` and `set`: - -```js -let obj = { - *!*get propName()*/!* { - // getter, the code executed on getting obj.propName - }, - - *!*set propName(value)*/!* { - // setter, the code executed on setting obj.propName = value - } -}; -``` - -The getter works when `obj.propName` is read, the setter -- when it is assigned. - -For instance, we have a `user` object with `name` and `surname`: - -```js run -let user = { - name: "John", - surname: "Smith" -}; -``` - -Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor: - -```js run -let user = { - name: "John", - surname: "Smith", - -*!* - get fullName() { - return `${this.name} ${this.surname}`; - } -*/!* -}; - -*!* -alert(user.fullName); // John Smith -*/!* -``` - -From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't *call* `user.fullName` as a function, we *read* it normally: the getter runs behind the scenes. - -As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error. - -Let's fix it by adding a setter for `user.fullName`: - -```js run -let user = { - name: "John", - surname: "Smith", - - get fullName() { - return `${this.name} ${this.surname}`; - }, - -*!* - set fullName(value) { - [this.name, this.surname] = value.split(" "); - } -*/!* -}; - -// set fullName is executed with the given value. -user.fullName = "Alice Cooper"; - -alert(user.name); // Alice -alert(user.surname); // Cooper -``` - -Now we have a "virtual" property. It is readable and writable, but in fact does not exist. - -```smart header="Accessor properties are only accessible with get/set" -A property can either be a "data property" or an "accessor property", but not both. - -Once a property is defined with `get prop()` or `set prop()`, it's an accessor property. So there must be a getter to read it, and must be a setter if we want to assign it. - -Sometimes it's normal that there's only a setter or only a getter. But the property won't be readable or writable in that case. -``` - - -## Accessor descriptors - -Descriptors for accessor properties are different -- as compared with data properties. - -For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions. - -So an accessor descriptor may have: - -- **`get`** -- a function without arguments, that works when a property is read, -- **`set`** -- a function with one argument, that is called when the property is set, -- **`enumerable`** -- same as for data properties, -- **`configurable`** -- same as for data properties. - -For instance, to create an accessor `fullName` with `defineProperty`, we can pass a descriptor with `get` and `set`: - -```js run -let user = { - name: "John", - surname: "Smith" -}; - -*!* -Object.defineProperty(user, 'fullName', { - get() { - return `${this.name} ${this.surname}`; - }, - - set(value) { - [this.name, this.surname] = value.split(" "); - } -*/!* -}); - -alert(user.fullName); // John Smith - -for(let key in user) alert(key); // name, surname -``` - -Please note once again that a property can be either an accessor or a data property, not both. - -If we try to supply both `get` and `value` in the same descriptor, there will be an error: - -```js run -*!* -// Error: Invalid property descriptor. -*/!* -Object.defineProperty({}, 'prop', { - get() { - return 1 - }, - - value: 2 -}); -``` - -## Smarter getters/setters - -Getters/setters can be used as wrappers over "real" property values to gain more control over them. - -For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`. And filter assignments in the setter: - -```js run -let user = { - get name() { - return this._name; - }, - - set name(value) { - if (value.length < 4) { - alert("Name is too short, need at least 4 characters"); - return; - } - this._name = value; - } -}; - -user.name = "Pete"; -alert(user.name); // Pete - -user.name = ""; // Name is too short... -``` - -Technically, the external code may still access the name directly by using `user._name`. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside the object. - - -## Using for compatibility - -One of the great ideas behind getters and setters -- they allow to take control over a "normal" data property and tweak it at any moment. - -For instance, we started implementing user objects using data properties `name` and `age`: - -```js -function User(name, age) { - this.name = name; - this.age = age; -} - -let john = new User("John", 25); - -alert( john.age ); // 25 -``` - -...But sooner or later, things may change. Instead of `age` we may decide to store `birthday`, because it's more precise and convenient: - -```js -function User(name, birthday) { - this.name = name; - this.birthday = birthday; -} - -let john = new User("John", new Date(1992, 6, 1)); -``` - -Now what to do with the old code that still uses `age` property? - -We can try to find all such places and fix them, but that takes time and can be hard to do if that code is written by other people. And besides, `age` is a nice thing to have in `user`, right? In some places it's just what we want. - -Adding a getter for `age` mitigates the problem: - -```js run no-beautify -function User(name, birthday) { - this.name = name; - this.birthday = birthday; - -*!* - // age is calculated from the current date and birthday - Object.defineProperty(this, "age", { - get() { - let todayYear = new Date().getFullYear(); - return todayYear - this.birthday.getFullYear(); - } - }); -*/!* -} - -let john = new User("John", new Date(1992, 6, 1)); - -alert( john.birthday ); // birthday is available -alert( john.age ); // ...as well as the age -``` - -Now the old code works too and we've got a nice additional property. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/task.md deleted file mode 100644 index 002b24b8a2..0000000000 --- a/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/task.md +++ /dev/null @@ -1,31 +0,0 @@ -importance: 5 - ---- - -# Searching algorithm - -The task has two parts. - -We have an object: - -```js -let head = { - glasses: 1 -}; - -let table = { - pen: 3 -}; - -let bed = { - sheet: 1, - pillow: 2 -}; - -let pockets = { - money: 2000 -}; -``` - -1. Use `__proto__` to assign prototypes in a way that any property lookup will follow the path: `pockets` -> `bed` -> `table` -> `head`. For instance, `pockets.pen` should be `3` (found in `table`), and `bed.glasses` should be `1` (found in `head`). -2. Answer the question: is it faster to get `glasses` as `pockets.glasses` or `head.glasses`? Benchmark if needed. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/solution.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/solution.md deleted file mode 100644 index c7d147b9c7..0000000000 --- a/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/solution.md +++ /dev/null @@ -1,6 +0,0 @@ -**The answer: `rabbit`.** - -That's because `this` is an object before the dot, so `rabbit.eat()` modifies `rabbit`. - -Property lookup and execution are two different things. -The method `rabbit.eat` is first found in the prototype, then executed with `this=rabbit` diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/task.md deleted file mode 100644 index b37499bad5..0000000000 --- a/1-js/07-object-oriented-programming/03-prototype-inheritance/3-proto-and-this/task.md +++ /dev/null @@ -1,23 +0,0 @@ -importance: 5 - ---- - -# Where it writes? - -We have `rabbit` inheriting from `animal`. - -If we call `rabbit.eat()`, which object receives the `full` property: `animal` or `rabbit`? - -```js -let animal = { - eat() { - this.full = true; - } -}; - -let rabbit = { - __proto__: animal -}; - -rabbit.eat(); -``` diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/solution.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/solution.md deleted file mode 100644 index fad4b88607..0000000000 --- a/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/solution.md +++ /dev/null @@ -1,80 +0,0 @@ -Let's look carefully at what's going on in the call `speedy.eat("apple")`. - -1. The method `speedy.eat` is found in the prototype (`=hamster`), then executed with `this=speedy` (the object before the dot). - -2. Then `this.stomach.push()` needs to find `stomach` property and call `push` on it. It looks for `stomach` in `this` (`=speedy`), but nothing found. - -3. Then it follows the prototype chain and finds `stomach` in `hamster`. - -4. Then it calls `push` on it, adding the food into *the stomach of the prototype*. - -So all hamsters share a single stomach! - -Every time the `stomach` is taken from the prototype, then `stomach.push` modifies it "at place". - -Please note that such thing doesn't happen in case of a simple assignment `this.stomach=`: - -```js run -let hamster = { - stomach: [], - - eat(food) { -*!* - // assign to this.stomach instead of this.stomach.push - this.stomach = [food]; -*/!* - } -}; - -let speedy = { - __proto__: hamster -}; - -let lazy = { - __proto__: hamster -}; - -// Speedy one found the food -speedy.eat("apple"); -alert( speedy.stomach ); // apple - -// Lazy one's stomach is empty -alert( lazy.stomach ); // -``` - -Now all works fine, because `this.stomach=` does not perform a lookup of `stomach`. The value is written directly into `this` object. - -Also we can totally evade the problem by making sure that each hamster has their own stomach: - -```js run -let hamster = { - stomach: [], - - eat(food) { - this.stomach.push(food); - } -}; - -let speedy = { - __proto__: hamster, -*!* - stomach: [] -*/!* -}; - -let lazy = { - __proto__: hamster, -*!* - stomach: [] -*/!* -}; - -// Speedy one found the food -speedy.eat("apple"); -alert( speedy.stomach ); // apple - -// Lazy one's stomach is empty -alert( lazy.stomach ); // -``` - -As a common solution, all properties that describe the state of a particular object, like `stomach` above, are usually written into that object. That prevents such problems. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/task.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/task.md deleted file mode 100644 index 6f9fb279ec..0000000000 --- a/1-js/07-object-oriented-programming/03-prototype-inheritance/4-hamster-proto/task.md +++ /dev/null @@ -1,35 +0,0 @@ -importance: 5 - ---- - -# Why two hamsters are full? - -We have two hamsters: `speedy` and `lazy` inheriting from the general `hamster` object. - -When we feed one of them, the other one is also full. Why? How to fix it? - -```js run -let hamster = { - stomach: [], - - eat(food) { - this.stomach.push(food); - } -}; - -let speedy = { - __proto__: hamster -}; - -let lazy = { - __proto__: hamster -}; - -// This one found the food -speedy.eat("apple"); -alert( speedy.stomach ); // apple - -// This one also has it, why? fix please. -alert( lazy.stomach ); // apple -``` - diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/article.md b/1-js/07-object-oriented-programming/03-prototype-inheritance/article.md deleted file mode 100644 index fa61cbc89d..0000000000 --- a/1-js/07-object-oriented-programming/03-prototype-inheritance/article.md +++ /dev/null @@ -1,250 +0,0 @@ -# Prototypal inheritance - -In programming, we often want to take something and extend it. - -For instance, we have a `user` object with its properties and methods, and want to make `admin` and `guest` as slightly modified variants of it. We'd like to reuse what we have in `user`, not copy/reimplement its methods, just build a new object on top of it. - -*Prototypal inheritance* is a language feature that helps in that. - -## [[Prototype]] - -In JavaScript, objects have a special hidden property `[[Prototype]]` (as named in the specification), that is either `null` or references another object. That object is called "a prototype": - -![prototype](object-prototype-empty.png) - -That `[[Prototype]]` has a "magical" meaning. When we want to read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, such thing is called "prototypal inheritance". Many cool language features and programming techniques are based on it. - -The property `[[Prototype]]` is internal and hidden, but there are many ways to set it. - -One of them is to use `__proto__`, like this: - -```js run -let animal = { - eats: true -}; -let rabbit = { - jumps: true -}; - -*!* -rabbit.__proto__ = animal; -*/!* -``` - -Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. We'll talk about other ways of setting it later, but for now `__proto__` will do just fine. - -If we look for a property in `rabbit`, and it's missing, JavaScript automatically takes it from `animal`. - -For instance: - -```js run -let animal = { - eats: true -}; -let rabbit = { - jumps: true -}; - -*!* -rabbit.__proto__ = animal; // (*) -*/!* - -// we can find both properties in rabbit now: -*!* -alert( rabbit.eats ); // true (**) -*/!* -alert( rabbit.jumps ); // true -``` - -Here the line `(*)` sets `animal` to be a prototype of `rabbit`. - -Then, when `alert` tries to read property `rabbit.eats` `(**)`, it's not in `rabbit`, so JavaScript follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up): - -![](proto-animal-rabbit.png) - -Here we can say that "`animal` is the prototype of `rabbit`" or "`rabbit` prototypally inherits from `animal`". - -So if `animal` has a lot of useful properties and methods, then they become automatically available in `rabbit`. Such properties are called "inherited". - -If we have a method in `animal`, it can be called on `rabbit`: - -```js run -let animal = { - eats: true, -*!* - walk() { - alert("Animal walk"); - } -*/!* -}; - -let rabbit = { - jumps: true, - __proto__: animal -}; - -// walk is taken from the prototype -*!* -rabbit.walk(); // Animal walk -*/!* -``` - -The method is automatically taken from the prototype, like this: - -![](proto-animal-rabbit-walk.png) - -The prototype chain can be longer: - - -```js run -let animal = { - eats: true, - walk() { - alert("Animal walk"); - } -}; - -let rabbit = { - jumps: true, - __proto__: animal -}; - -let longEar = { - earLength: 10, - __proto__: rabbit -} - -// walk is taken from the prototype chain -longEar.walk(); // Animal walk -alert(longEar.jumps); // true (from rabbit) -``` - -![](proto-animal-rabbit-chain.png) - -There are actually only two limitations: - -1. The references can't go in circles. JavaScript will throw an error if we try to assign `__proto__` in a circle. -2. The value of `__proto__` can be either an object or `null`. All other values (like primitives) are ignored. - -Also it may be obvious, but still: there can be only one `[[Prototype]]`. An object may not inherit from two others. - -## Read/write rules - -The prototype is only used for reading properties. - -For data properties (not getters/setters) write/delete operations work directly with the object. - -In the example below, we assign its own `walk` method to `rabbit`: - -```js run -let animal = { - eats: true, - walk() { - /* this method won't be used by rabbit */ - } -}; - -let rabbit = { - __proto__: animal -} - -*!* -rabbit.walk = function() { - alert("Rabbit! Bounce-bounce!"); -}; -*/!* - -rabbit.walk(); // Rabbit! Bounce-bounce! -``` - -From now on, `rabbit.walk()` call finds the method immediately in the object and executes it, without using the prototype: - -![](proto-animal-rabbit-walk-2.png) - -For getters/setters -- if we read/write a property, they are looked up in the prototype and invoked. - -For instance, check out `admin.fullName` property in the code below: - -```js run -let user = { - name: "John", - surname: "Smith", - - set fullName(value) { - [this.name, this.surname] = value.split(" "); - }, - - get fullName() { - return `${this.name} ${this.surname}`; - } -}; - -let admin = { - __proto__: user, - isAdmin: true -}; - -alert(admin.fullName); // John Smith (*) - -// setter triggers! -admin.fullName = "Alice Cooper"; // (**) -``` - -Here in the line `(*)` the property `admin.fullName` has a getter in the prototype `user`, so it is called. And in the line `(**)` the property has a setter in the prototype, so it is called. - -## The value of "this" - -An interesting question may arise in the example above: what's the value of `this` inside `set fullName(value)`? Where the properties `this.name` and `this.surname` are written: `user` or `admin`? - -The answer is simple: `this` is not affected by prototypes at all. - -**No matter where the method is found: in an object or its prototype. In a method call, `this` is always the object before the dot.** - -So, the setter actually uses `admin` as `this`, not `user`. - -That is actually a super-important thing, because we may have a big object with many methods and inherit from it. Then we can run its methods on inherited objects and they will modify the state of these objects, not the big one. - -For instance, here `animal` represents a "method storage", and `rabbit` makes use of it. - -The call `rabbit.sleep()` sets `this.isSleeping` on the `rabbit` object: - -```js run -// animal has methods -let animal = { - walk() { - if (!this.isSleeping) { - alert(`I walk`); - } - }, - sleep() { - this.isSleeping = true; - } -}; - -let rabbit = { - name: "White Rabbit", - __proto__: animal -}; - -// modifies rabbit.isSleeping -rabbit.sleep(); - -alert(rabbit.isSleeping); // true -alert(animal.isSleeping); // undefined (no such property in the prototype) -``` - -The resulting picture: - -![](proto-animal-rabbit-walk-3.png) - -If we had other objects like `bird`, `snake` etc inheriting from `animal`, they would also gain access to methods of `animal`. But `this` in each method would be the corresponding object, evaluated at the call-time (before dot), not `animal`. So when we write data into `this`, it is stored into these objects. - -As a result, methods are shared, but the object state is not. - -## Summary - -- In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`. -- We can use `obj.__proto__` to access it (there are other ways too, to be covered soon). -- The object referenced by `[[Prototype]]` is called a "prototype". -- If we want to read a property of `obj` or call a method, and it doesn't exist, then JavaScript tries to find it in the prototype. Write/delete operations work directly on the object, they don't use the prototype (unless the property is actually a setter). -- If we call `obj.method()`, and the `method` is taken from the prototype, `this` still references `obj`. So methods always work with the current object even if they are inherited. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty.png deleted file mode 100644 index 1d228b5c8a..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty@2x.png deleted file mode 100644 index a4c195ad69..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/object-prototype-empty@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain.png deleted file mode 100644 index a302343e2d..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain@2x.png deleted file mode 100644 index 4d3311998e..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-chain@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2.png deleted file mode 100644 index 19c1881a2f..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png deleted file mode 100644 index cd38624ab0..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3.png deleted file mode 100644 index 0ea869d3c5..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png deleted file mode 100644 index 194db77edd..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk.png deleted file mode 100644 index 5bd2111474..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk@2x.png deleted file mode 100644 index 1e4ef6efbd..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit-walk@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit.png deleted file mode 100644 index c18e020525..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit@2x.png deleted file mode 100644 index 4d9e8ec094..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin.png deleted file mode 100644 index aed69c59e0..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin@2x.png b/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin@2x.png deleted file mode 100644 index 2b11b2568b..0000000000 Binary files a/1-js/07-object-oriented-programming/03-prototype-inheritance/proto-user-admin@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/solution.md b/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/solution.md deleted file mode 100644 index 771e3061c9..0000000000 --- a/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/solution.md +++ /dev/null @@ -1,20 +0,0 @@ - -Answers: - -1. `true`. - - The assignment to `Rabbit.prototype` sets up `[[Prototype]]` for new objects, but it does not affect the existing ones. - -2. `false`. - - Objects are assigned by reference. The object from `Rabbit.prototype` is not duplicated, it's still a single object is referenced both by `Rabbit.prototype` and by the `[[Prototype]]` of `rabbit`. - - So when we change its content through one reference, it is visible through the other one. - -3. `true`. - - All `delete` operations are applied directly to the object. Here `delete rabbit.eats` tries to remove `eats` property from `rabbit`, but it doesn't have it. So the operation won't have any effect. - -4. `undefined`. - - The property `eats` is deleted from the prototype, it doesn't exist any more. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/task.md b/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/task.md deleted file mode 100644 index 4b8522d3dd..0000000000 --- a/1-js/07-object-oriented-programming/04-function-prototype/1-changing-prototype/task.md +++ /dev/null @@ -1,89 +0,0 @@ -importance: 5 - ---- - -# Changing "prototype" - -In the code below we create `new Rabbit`, and then try to modify its prototype. - -In the start, we have this code: - -```js run -function Rabbit() {} -Rabbit.prototype = { - eats: true -}; - -let rabbit = new Rabbit(); - -alert( rabbit.eats ); // true -``` - - -1. We added one more string (emphasized), what `alert` shows now? - - ```js - function Rabbit() {} - Rabbit.prototype = { - eats: true - }; - - let rabbit = new Rabbit(); - - *!* - Rabbit.prototype = {}; - */!* - - alert( rabbit.eats ); // ? - ``` - -2. ...And if the code is like this (replaced one line)? - - ```js - function Rabbit() {} - Rabbit.prototype = { - eats: true - }; - - let rabbit = new Rabbit(); - - *!* - Rabbit.prototype.eats = false; - */!* - - alert( rabbit.eats ); // ? - ``` - -3. Like this (replaced one line)? - - ```js - function Rabbit() {} - Rabbit.prototype = { - eats: true - }; - - let rabbit = new Rabbit(); - - *!* - delete rabbit.eats; - */!* - - alert( rabbit.eats ); // ? - ``` - -4. The last variant: - - ```js - function Rabbit() {} - Rabbit.prototype = { - eats: true - }; - - let rabbit = new Rabbit(); - - *!* - delete Rabbit.prototype.eats; - */!* - - alert( rabbit.eats ); // ? - ``` diff --git a/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/solution.md b/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/solution.md deleted file mode 100644 index 43190e163b..0000000000 --- a/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/solution.md +++ /dev/null @@ -1,44 +0,0 @@ -We can use such approach if we are sure that `"constructor"` property has the correct value. - -For instance, if we don't touch the default `"prototype"`, then this code works for sure: - -```js run -function User(name) { - this.name = name; -} - -let user = new User('John'); -let user2 = new user.constructor('Pete'); - -alert( user2.name ); // Pete (worked!) -``` - -It worked, because `User.prototype.constructor == User`. - -..But if someone, so to say, overwrites `User.prototype` and forgets to recreate `"constructor"`, then it would fail. - -For instance: - -```js run -function User(name) { - this.name = name; -} -*!* -User.prototype = {}; // (*) -*/!* - -let user = new User('John'); -let user2 = new user.constructor('Pete'); - -alert( user2.name ); // undefined -``` - -Why `user2.name` is `undefined`? - -Here's how `new user.constructor('Pete')` works: - -1. First, it looks for `constructor` in `user`. Nothing. -2. Then it follows the prototype chain. The prototype of `user` is `User.prototype`, and it also has nothing. -3. The value of `User.prototype` is a plain object `{}`, its prototype is `Object.prototype`. And there is `Object.prototype.constructor == Object`. So it is used. - -At the end, we have `let user2 = new Object('Pete')`. The built-in `Object` constructor ignores arguments, it always creates an empty object -- that's what we have in `user2` after all. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/article.md b/1-js/07-object-oriented-programming/04-function-prototype/article.md deleted file mode 100644 index d4b4538507..0000000000 --- a/1-js/07-object-oriented-programming/04-function-prototype/article.md +++ /dev/null @@ -1,174 +0,0 @@ -# F.prototype - -In modern JavaScript we can set a prototype using `__proto__`, as described in the previous article. But it wasn't like that all the time. - -JavaScript has had prototypal inheritance from the beginning. It was one of the core features of the language. - -But in the old times, there was another (and the only) way to set it: to use a `"prototype"` property of the constructor function. And there are still many scripts that use it. - -## The "prototype" property - -As we know already, `new F()` creates a new object. - -When a new object is created with `new F()`, the object's `[[Prototype]]` is set to `F.prototype`. - -In other words, if `F` has a `prototype` property with a value of the object type, then `new` operator uses it to set `[[Prototype]]` for the new object. - -Please note that `F.prototype` here means a regular property named `"prototype"` on `F`. It sounds something similar to the term "prototype", but here we really mean a regular property with this name. - -Here's the example: - -```js run -let animal = { - eats: true -}; - -function Rabbit(name) { - this.name = name; -} - -*!* -Rabbit.prototype = animal; -*/!* - -let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal - -alert( rabbit.eats ); // true -``` - -Setting `Rabbit.prototype = animal` literally states the following: "When a `new Rabbit` is created, assign its `[[Prototype]]` to `animal`". - -That's the resulting picture: - -![](proto-constructor-animal-rabbit.png) - -On the picture, `"prototype"` is a horizontal arrow, it's a regular property, and `[[Prototype]]` is vertical, meaning the inheritance of `rabbit` from `animal`. - - -## Default F.prototype, constructor property - -Every function has the `"prototype"` property even if we don't supply it. - -The default `"prototype"` is an object with the only property `constructor` that points back to the function itself. - -Like this: - -```js -function Rabbit() {} - -/* default prototype -Rabbit.prototype = { constructor: Rabbit }; -*/ -``` - -![](function-prototype-constructor.png) - -We can check it: - -```js run -function Rabbit() {} -// by default: -// Rabbit.prototype = { constructor: Rabbit } - -alert( Rabbit.prototype.constructor == Rabbit ); // true -``` - -Naturally, if we do nothing, the `constructor` property is available to all rabbits through `[[Prototype]]`: - -```js run -function Rabbit() {} -// by default: -// Rabbit.prototype = { constructor: Rabbit } - -let rabbit = new Rabbit(); // inherits from {constructor: Rabbit} - -alert(rabbit.constructor == Rabbit); // true (from prototype) -``` - -![](rabbit-prototype-constructor.png) - -We can use `constructor` property to create a new object using the same constructor as the existing one. - -Like here: - -```js run -function Rabbit(name) { - this.name = name; - alert(name); -} - -let rabbit = new Rabbit("White Rabbit"); - -*!* -let rabbit2 = new rabbit.constructor("Black Rabbit"); -*/!* -``` - -That's handy when we have an object, don't know which constructor was used for it (e.g. it comes from a 3rd party library), and we need to create another one of the same kind. - -But probably the most important thing about `"constructor"` is that... - -**...JavaScript itself does not ensure the right `"constructor"` value.** - -Yes, it exists in the default `"prototype"` for functions, but that's all. What happens with it later -- is totally on us. - -In particular, if we replace the default prototype as a whole, then there will be no `"constructor"` in it. - -For instance: - -```js run -function Rabbit() {} -Rabbit.prototype = { - jumps: true -}; - -let rabbit = new Rabbit(); -*!* -alert(rabbit.constructor === Rabbit); // false -*/!* -``` - -So, to keep the right `"constructor"` we can choose to add/remove properties to the default `"prototype"` instead of overwriting it as a whole: - -```js -function Rabbit() {} - -// Not overwrite Rabbit.prototype totally -// just add to it -Rabbit.prototype.jumps = true -// the default Rabbit.prototype.constructor is preserved -``` - -Or, alternatively, recreate the `constructor` property it manually: - -```js -Rabbit.prototype = { - jumps: true, -*!* - constructor: Rabbit -*/!* -}; - -// now constructor is also correct, because we added it -``` - - -## Summary - -In this chapter we briefly described the way of setting a `[[Prototype]]` for objects created via a constructor function. Later we'll see more advanced programming patterns that rely on it. - -Everything is quite simple, just few notes to make things clear: - -- The `F.prototype` property is not the same as `[[Prototype]]`. The only thing `F.prototype` does: it sets `[[Prototype]]` of new objects when `new F()` is called. -- The value of `F.prototype` should be either an object or null: other values won't work. -- The `"prototype"` property only has such a special effect when is set to a constructor function, and invoked with `new`. - -On regular objects the `prototype` is nothing special: -```js -let user = { - name: "John", - prototype: "Bla-bla" // no magic at all -}; -``` - -By default all functions have `F.prototype = { constructor: F }`, so we can get the constructor of an object by accessing its `"constructor"` property. diff --git a/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor.png b/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor.png deleted file mode 100644 index 0dbc7b8fc4..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor@2x.png deleted file mode 100644 index e38cb85bd9..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/function-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring.png deleted file mode 100644 index 83258d0614..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring@2x.png deleted file mode 100644 index d77cc8f4ec..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-array-tostring@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes.png deleted file mode 100644 index fa6a2943f6..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes@2x.png deleted file mode 100644 index 9368d7e502..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/native-prototypes-classes@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1.png deleted file mode 100644 index e7c6bdb212..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1@2x.png deleted file mode 100644 index 34c256bbfd..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype-1@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype.png deleted file mode 100644 index d97d87d2c3..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/object-prototype@2x.png deleted file mode 100644 index 76ce4c9ac5..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/object-prototype@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit.png b/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit.png deleted file mode 100644 index 3eec740fbb..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit@2x.png deleted file mode 100644 index ed28a388a0..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/proto-constructor-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object.png deleted file mode 100644 index 3254270f38..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object@2x.png deleted file mode 100644 index f794d7b84a..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-animal-object@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png deleted file mode 100644 index d3ef344852..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png deleted file mode 100644 index 45cbb6baab..0000000000 Binary files a/1-js/07-object-oriented-programming/04-function-prototype/rabbit-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/solution.md b/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/solution.md deleted file mode 100644 index e3651683fa..0000000000 --- a/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/solution.md +++ /dev/null @@ -1,17 +0,0 @@ - - -```js run -Function.prototype.defer = function(ms) { - let f = this; - return function(...args) { - setTimeout(() => f.apply(this, args), ms); - } -}; - -// check it -function f(a, b) { - alert( a + b ); -} - -f.defer(1000)(1, 2); // shows 3 after 1 sec -``` diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/article.md b/1-js/07-object-oriented-programming/05-native-prototypes/article.md deleted file mode 100644 index 4777f356f8..0000000000 --- a/1-js/07-object-oriented-programming/05-native-prototypes/article.md +++ /dev/null @@ -1,180 +0,0 @@ -# Native prototypes - -The `"prototype"` property is widely used by the core of JavaScript itself. All built-in constructor functions use it. - -We'll see how it is for plain objects first, and then for more complex ones. - -## Object.prototype - -Let's say we output an empty object: - -```js run -let obj = {}; -alert( obj ); // "[object Object]" ? -``` - -Where's the code that generates the string `"[object Object]"`? That's a built-in `toString` method, but where is it? The `obj` is empty! - -...But the short notation `obj = {}` is the same as `obj = new Object()`, where `Object` -- is a built-in object constructor function. And that function has `Object.prototype` that references a huge object with `toString` and other functions. - -Like this (all that is built-in): - -![](object-prototype.png) - -When `new Object()` is called (or a literal object `{...}` is created), the `[[Prototype]]` of it is set to `Object.prototype` by the rule that we've discussed in the previous chapter: - -![](object-prototype-1.png) - -Afterwards when `obj.toString()` is called -- the method is taken from `Object.prototype`. - -We can check it like this: - -```js run -let obj = {}; - -alert(obj.__proto__ === Object.prototype); // true -// obj.toString === obj.__proto__.toString == Object.prototype.toString -``` - -Please note that there is no additional `[[Prototype]]` in the chain above `Object.prototype`: - -```js run -alert(Object.prototype.__proto__); // null -``` - -## Other built-in prototypes - -Other built-in objects such as `Array`, `Date`, `Function` and others also keep methods in prototypes. - -For instance, when we create an array `[1, 2, 3]`, the default `new Array()` constructor is used internally. So the array data is written into the new object, and `Array.prototype` becomes its prototype and provides methods. That's very memory-efficient. - -By specification, all built-in prototypes have `Object.prototype` on the top. Sometimes people say that "everything inherits from objects". - -Here's the overall picture (for 3 built-ins to fit): - -![](native-prototypes-classes.png) - -Let's check the prototypes manually: - -```js run -let arr = [1, 2, 3]; - -// it inherits from Array.prototype? -alert( arr.__proto__ === Array.prototype ); // true - -// then from Object.prototype? -alert( arr.__proto__.__proto__ === Object.prototype ); // true - -// and null on the top. -alert( arr.__proto__.__proto__.__proto__ ); // null -``` - -Some methods in prototypes may overlap, for instance, `Array.prototype` has its own `toString` that lists comma-delimited elements: - -```js run -let arr = [1, 2, 3] -alert(arr); // 1,2,3 <-- the result of Array.prototype.toString -``` - -As we've seen before, `Object.prototype` has `toString` as well, but `Array.prototype` is closer in the chain, so the array variant is used. - - -![](native-prototypes-array-tostring.png) - - -In-browser tools like Chrome developer console also show inheritance (may need to use `console.dir` for built-in objects): - -![](console_dir_array.png) - -Other built-in objects also work the same way. Even functions. They are objects of a built-in `Function` constructor, and their methods: `call/apply` and others are taken from `Function.prototype`. Functions have their own `toString` too. - -```js run -function f() {} - -alert(f.__proto__ == Function.prototype); // true -alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects -``` - -## Primitives - -The most intricate thing happens with strings, numbers and booleans. - -As we remember, they are not objects. But if we try to access their properties, then temporary wrapper objects are created using built-in constructors `String`, `Number`, `Boolean`, they provide the methods and disappear. - -These objects are created invisibly to us and most engines optimize them out, but the specification describes it exactly this way. Methods of these objects also reside in prototypes, available as `String.prototype`, `Number.prototype` and `Boolean.prototype`. - -```warn header="Values `null` and `undefined` have no object wrappers" -Special values `null` and `undefined` stand apart. They have no object wrappers, so methods and properties are not available for them. And there are no corresponding prototypes too. -``` - -## Changing native prototypes [#native-prototype-change] - -Native prototypes can be modified. For instance, if we add a method to `String.prototype`, it becomes available to all strings: - -```js run -String.prototype.show = function() { - alert(this); -}; - -"BOOM!".show(); // BOOM! -``` - -During the process of development we may have ideas which new built-in methods we'd like to have. And there may be a slight temptation to add them to native prototypes. But that is generally a bad idea. - -Prototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them overwrites the other one. - -In modern programming, there is only one case when modifying native prototypes is approved. That's polyfills. In other words, if there's a method in JavaScript specification that is not yet supported by our JavaScript engine (or any of those that we want to support), then may implement it manually and populate the built-in prototype with it. - -For instance: - -```js run -if (!String.prototype.repeat) { // if there's no such method - // add it to the prototype - - String.prototype.repeat = function(n) { - // repeat the string n times - - // actually, the code should be more complex than that, - // throw errors for negative values of "n" - // the full algorithm is in the specification - return new Array(n + 1).join(this); - }; -} - -alert( "La".repeat(3) ); // LaLaLa -``` - -## Borrowing from prototypes - -In the chapter we talked about method borrowing: - -```js run -function showArgs() { -*!* - // borrow join from array and call in the context of arguments - alert( [].join.call(arguments, " - ") ); -*/!* -} - -showArgs("John", "Pete", "Alice"); // John - Pete - Alice -``` - -Because `join` resides in `Array.prototype`, we can call it from there directly and rewrite it as: - -```js -function showArgs() { -*!* - alert( Array.prototype.join.call(arguments, " - ") ); -*/!* -} -``` - -That's more efficient, because it avoids the creation of an extra array object `[]`. On the other hand, it is longer to write. - -## Summary - -- All built-in objects follow the same pattern: - - The methods are stored in the prototype (`Array.prototype`, `Object.prototype`, `Date.prototype` etc). - - The object itself stores only the data (array items, object properties, the date). -- Primitives also store methods in prototypes of wrapper objects: `Number.prototype`, `String.prototype`, `Boolean.prototype`. There are no wrapper objects only for `undefined` and `null`. -- Built-in prototypes can be modified or populated with new methods. But it's not recommended to change them. Probably the only allowable cause is when we add-in a new standard, but not yet supported by the engine JavaScript method. diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor.png b/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor.png deleted file mode 100644 index 0dbc7b8fc4..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor@2x.png deleted file mode 100644 index e38cb85bd9..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/function-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring.png deleted file mode 100644 index 83258d0614..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring@2x.png deleted file mode 100644 index d77cc8f4ec..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-array-tostring@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes.png deleted file mode 100644 index fa6a2943f6..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes@2x.png deleted file mode 100644 index 9368d7e502..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/native-prototypes-classes@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1.png deleted file mode 100644 index e7c6bdb212..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1@2x.png deleted file mode 100644 index 34c256bbfd..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-1@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null.png deleted file mode 100644 index 7c2e3f9c78..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null@2x.png deleted file mode 100644 index fec1facb0b..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype-null@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype.png deleted file mode 100644 index d97d87d2c3..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype@2x.png deleted file mode 100644 index 76ce4c9ac5..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/object-prototype@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit.png b/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit.png deleted file mode 100644 index 3eec740fbb..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit@2x.png deleted file mode 100644 index ed28a388a0..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/proto-constructor-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png deleted file mode 100644 index d3ef344852..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png b/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png deleted file mode 100644 index 45cbb6baab..0000000000 Binary files a/1-js/07-object-oriented-programming/05-native-prototypes/rabbit-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md b/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md deleted file mode 100644 index debaecd6a1..0000000000 --- a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/solution.md +++ /dev/null @@ -1,29 +0,0 @@ - -The method can take all enumerable keys using `Object.keys` and output their list. - -To make `toString` non-enumerable, let's define it using a property descriptor. The syntax of `Object.create` allows to provide an object with property descriptors as the second argument. - -```js run -*!* -let dictionary = Object.create(null, { - toString: { // define toString property - value() { // the value is a function - return Object.keys(this).join(); - } - } -}); -*/!* - -dictionary.apple = "Apple"; -dictionary.__proto__ = "test"; - -// apple and __proto__ is in the loop -for(let key in dictionary) { - alert(key); // "apple", then "__proto__" -} - -// comma-separated list of properties by toString -alert(dictionary); // "apple,__proto__" -``` - -When we create a property using a descriptor, its flags are `false` by default. So in the code above, `dictionary.toString` is non-enumerable. diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md b/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md deleted file mode 100644 index 92653bd8c3..0000000000 --- a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/task.md +++ /dev/null @@ -1,27 +0,0 @@ -importance: 5 - ---- - -# The difference beteeen calls - -Let's create a new `rabbit` object: - -```js -function Rabbit(name) { - this.name = name; -} -Rabbit.prototype.sayHi = function() { - alert(this.name); -}; - -let rabbit = new Rabbit("Rabbit"); -``` - -These calls do the same thing or not? - -```js -rabbit.sayHi(); -Rabbit.prototype.sayHi(); -Object.getPrototypeOf(rabbit).sayHi(); -rabbit.__proto__.sayHi(); -``` diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/article.md b/1-js/07-object-oriented-programming/06-prototype-methods/article.md deleted file mode 100644 index e253e8af76..0000000000 --- a/1-js/07-object-oriented-programming/06-prototype-methods/article.md +++ /dev/null @@ -1,253 +0,0 @@ - -# Methods for prototypes - -In this chapter we cover additional methods to work with a prototype. - -There are also other ways to get/set a prototype, besides those that we already know: - -- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` and optional property descriptors. -- [Object.getPrototypeOf(obj)](mdn:js/Object/getPrototypeOf) -- returns the `[[Prototype]]` of `obj`. -- [Object.setPrototypeOf(obj, proto)](mdn:js/Object/setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto`. - -For instance: - -```js run -let animal = { - eats: true -}; - -// create a new object with animal as a prototype -*!* -let rabbit = Object.create(animal); -*/!* - -alert(rabbit.eats); // true -*!* -alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit -*/!* - -*!* -Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {} -*/!* -``` - -`Object.create` has an optional second argument: property descriptors. We can provide additional properties to the new object there, like this: - -```js run -let animal = { - eats: true -}; - -let rabbit = Object.create(animal, { - jumps: { - value: true - } -}); - -alert(rabbit.jumps); // true -``` - -The descriptors are in the same format as described in the chapter . - -We can use `Object.create` to perform an object cloning more powerful than copying properties in `for..in`: - -```js -// fully identical shallow clone of obj -let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); -``` - -This call makes a truly exact copy of `obj`, including all properties: enumerable and non-enumerable, data properties and setters/getters -- everything, and with the right `[[Prototype]]`. - -## Brief history - -If we count all the ways to manage `[[Prototype]]`, there's a lot! Many ways to do the same! - -Why so? - -That's for historical reasons. - -- The `"prototype"` property of a constructor function works since very ancient times. -- Later in the year 2012: `Object.create` appeared in the standard. It allowed to create objects with the given prototype, but did not allow to get/set it. So browsers implemented non-standard `__proto__` accessor that allowed to get/set a prototype at any time. -- Later in the year 2015: `Object.setPrototypeOf` and `Object.getPrototypeOf` were added to the standard. The `__proto__` was de-facto implemented everywhere, so it made its way to the Annex B of the standard, that is optional for non-browser environments. - -As of now we have all these ways at our disposal. - -Technically, we can get/set `[[Prototype]]` at any time. But usually we only set it once at the object creation time, and then do not modify: `rabbit` inherits from `animal`, and that is not going to change. And JavaScript engines are highly optimized to that. Changing a prototype "on-the-fly" with `Object.setPrototypeOf` or `obj.__proto__=` is a very slow operation. But it is possible. - -## "Very plain" objects - -As we know, objects can be used as associative arrays to store key/value pairs. - -...But if we try to store *user-provided* keys in it (for instance, a user-entered dictionary), we can see an interesting glitch: all keys work fine except `"__proto__"`. - -Check out the example: - -```js run -let obj = {}; - -let key = prompt("What's the key?", "__proto__"); -obj[key] = "some value"; - -alert(obj[key]); // [object Object], not "some value"! -``` - -Here if the user types in `__proto__`, the assignment is ignored! - -That shouldn't surprise us. The `__proto__` property is special: it must be either an object or `null`, a string can not become a prototype. - -But we did not intend to implement such behavior, right? We want to store key/value pairs, and the key named `"__proto__"` was not properly saved. So that's a bug. Here the consequences are not terrible. But in other cases the prototype may indeed be changed, so the execution may go wrong in totally unexpected ways. - -What's worst -- usually developers do not think about such possibility at all. That makes such bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on server-side. - -Such thing happens only with `__proto__`. All other properties are "assignable" normally. - -How to evade the problem? - -First, we can just switch to using `Map`, then everything's fine. - -But `Object` also can serve us well here, because language creators gave a thought to that problem long ago. - -The `__proto__` is not a property of an object, but an accessor property of `Object.prototype`: - -![](object-prototype-2.png) - -So, if `obj.__proto__` is read or assigned, the corresponding getter/setter is called from its prototype, and it gets/sets `[[Prototype]]`. - -As it was said in the beginning: `__proto__` is a way to access `[[Prototype]]`, it is not `[[Prototype]]` itself. - -Now, if we want to use an object as an associative array, we can do it with a little trick: - -```js run -*!* -let obj = Object.create(null); -*/!* - -let key = prompt("What's the key?", "__proto__"); -obj[key] = "some value"; - -alert(obj[key]); // "some value" -``` - -`Object.create(null)` creates an empty object without a prototype (`[[Prototype]]` is `null`): - -![](object-prototype-null.png) - -So, there is no inherited getter/setter for `__proto__`. Now it is processed as a regular data property, so the example above works right. - -We can call such object "very plain" or "pure dictionary objects", because they are even simpler than regular plain object `{...}`. - -A downside is that such objects lack any built-in object methods, e.g. `toString`: - -```js run -*!* -let obj = Object.create(null); -*/!* - -alert(obj); // Error (no toString) -``` - -...But that's usually fine for associative arrays. - -Please note that most object-related methods are `Object.something(...)`, like `Object.keys(obj)` -- they are not in the prototype, so they will keep working on such objects: - - -```js run -let chineseDictionary = Object.create(null); -chineseDictionary.hello = "ni hao"; -chineseDictionary.bye = "zai jian"; - -alert(Object.keys(chineseDictionary)); // hello,bye -``` - -## Getting all properties - -There are many ways to get keys/values from an object. - -We already know these ones: - -- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. These methods only list *enumerable* properties, and those that have *strings as keys*. - -If we want symbolic properties: - -- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names. - -If we want non-enumerable properties: - -- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names. - -If we want *all* properties: - -- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names. - -These methods are a bit different about which properties they return, but all of them operate on the object itself. Properties from the prototype are not listed. - -The `for..in` loop is different: it loops over inherited properties too. - -For instance: - -```js run -let animal = { - eats: true -}; - -let rabbit = { - jumps: true, - __proto__: animal -}; - -*!* -// only own keys -alert(Object.keys(rabbit)); // jumps -*/!* - -*!* -// inherited keys too -for(let prop in rabbit) alert(prop); // jumps, then eats -*/!* -``` - -If we want to distinguish inherited properties, there's a built-in method [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. - -So we can filter out inherited properties (or do something else with them): - -```js run -let animal = { - eats: true -}; - -let rabbit = { - jumps: true, - __proto__: animal -}; - -for(let prop in rabbit) { - let isOwn = rabbit.hasOwnProperty(prop); - alert(`${prop}: ${isOwn}`); // jumps:true, then eats:false -} -``` -Here we have the following inheritance chain: `rabbit`, then `animal`, then `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it: - -![](rabbit-animal-object.png) - -Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited. - -...But why `hasOwnProperty` does not appear in `for..in` loop, if it lists all inherited properties? The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`. That's why they are not listed. - -## Summary - -Here's a brief list of methods we discussed in this chapter -- as a recap: - -- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` (can be `null`) and optional property descriptors. -- [Object.getPrototypeOf(obj)](mdn:js/Object.getPrototypeOf) -- returns the `[[Prototype]]` of `obj` (same as `__proto__` getter). -- [Object.setPrototypeOf(obj, proto)](mdn:js/Object.setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto` (same as `__proto__` setter). -- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. -- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names. -- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names. -- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names. -- [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. - -We also made it clear that `__proto__` is a getter/setter for `[[Prototype]]` and resides in `Object.prototype`, just as other methods. - -We can create an object without a prototype by `Object.create(null)`. Such objects are used as "pure dictionaries", they have no issues with `"__proto__"` as the key. - -All methods that return object properties (like `Object.keys` and others) -- return "own" properties. If we want inherited ones, then we can use `for..in`. diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2.png deleted file mode 100644 index ee42f6b98c..0000000000 Binary files a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2@2x.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2@2x.png deleted file mode 100644 index 1917c6631d..0000000000 Binary files a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-2@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null.png deleted file mode 100644 index 7c2e3f9c78..0000000000 Binary files a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null@2x.png b/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null@2x.png deleted file mode 100644 index fec1facb0b..0000000000 Binary files a/1-js/07-object-oriented-programming/06-prototype-methods/object-prototype-null@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object.png b/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object.png deleted file mode 100644 index 3254270f38..0000000000 Binary files a/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object@2x.png b/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object@2x.png deleted file mode 100644 index f794d7b84a..0000000000 Binary files a/1-js/07-object-oriented-programming/06-prototype-methods/rabbit-animal-object@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/solution.md b/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/solution.md deleted file mode 100644 index 55f945ca5d..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/solution.md +++ /dev/null @@ -1,46 +0,0 @@ -Here's the line with the error: - -```js -Rabbit.prototype = Animal.prototype; -``` - -Here `Rabbit.prototype` and `Animal.prototype` become the same object. So methods of both classes become mixed in that object. - -As a result, `Rabbit.prototype.walk` overwrites `Animal.prototype.walk`, so all animals start to bounce: - -```js run -function Animal(name) { - this.name = name; -} - -Animal.prototype.walk = function() { - alert(this.name + ' walks'); -}; - -function Rabbit(name) { - this.name = name; -} - -*!* -Rabbit.prototype = Animal.prototype; -*/!* - -Rabbit.prototype.walk = function() { - alert(this.name + " bounces!"); -}; - -*!* -let animal = new Animal("pig"); -animal.walk(); // pig bounces! -*/!* -``` - -The correct variant would be: - -```js -Rabbit.prototype.__proto__ = Animal.prototype; -// or like this: -Rabbit.prototype = Object.create(Animal.prototype); -``` - -That makes prototypes separate, each of them stores methods of the corresponding class, but `Rabbit.prototype` inherits from `Animal.prototype`. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/task.md b/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/task.md deleted file mode 100644 index ee486c3d67..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/1-inheritance-error-assign/task.md +++ /dev/null @@ -1,29 +0,0 @@ -importance: 5 - ---- - -# An error in the inheritance - -Find an error in the prototypal inheritance below. - -What's wrong? What are consequences going to be? - -```js -function Animal(name) { - this.name = name; -} - -Animal.prototype.walk = function() { - alert(this.name + ' walks'); -}; - -function Rabbit(name) { - this.name = name; -} - -Rabbit.prototype = Animal.prototype; - -Rabbit.prototype.walk = function() { - alert(this.name + " bounces!"); -}; -``` diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.md b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.md deleted file mode 100644 index 300b25d94c..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.md +++ /dev/null @@ -1 +0,0 @@ -Please note that properties that were internal in functional style (`template`, `timer`) and the internal method `render` are marked private with the underscore `_`. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js deleted file mode 100644 index 7a193b79d0..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock({ template }) { - this._template = template; -} - -Clock.prototype._render = function() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) min = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this._template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this._timer); -}; - -Clock.prototype.start = function() { - this._render(); - this._timer = setInterval(() => this._render(), 1000); -}; diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/index.html b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/index.html deleted file mode 100644 index fdee13d01b..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/solution.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/clock.js b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/clock.js deleted file mode 100644 index 26081a35a1..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/clock.js +++ /dev/null @@ -1,34 +0,0 @@ -function Clock({ template }) { - - let timer; - - function render() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) min = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); - } - - this.stop = function() { - clearInterval(timer); - }; - - this.start = function() { - render(); - timer = setInterval(render, 1000); - }; - -} diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/index.html b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/index.html deleted file mode 100644 index fdee13d01b..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/source.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/task.md b/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/task.md deleted file mode 100644 index 71131816be..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/2-rewrite-to-prototypes/task.md +++ /dev/null @@ -1,9 +0,0 @@ -importance: 5 - ---- - -# Rewrite to prototypes - -The `Clock` class is written in functional style. Rewrite it using prototypes. - -P.S. The clock ticks in the console, open it to see. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/article.md b/1-js/07-object-oriented-programming/08-class-patterns/article.md deleted file mode 100644 index 92b521c192..0000000000 --- a/1-js/07-object-oriented-programming/08-class-patterns/article.md +++ /dev/null @@ -1,240 +0,0 @@ - -# Class patterns - -```quote author="Wikipedia" -In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods). -``` - -There's a special syntax construct and a keyword `class` in JavaScript. But before studying it, we should consider that the term "class" comes from the theory of object-oriented programming. The definition is cited above, and it's language-independent. - -In JavaScript there are several well-known programming patterns to make classes even without using the `class` keyword. And here we'll talk about them first. - -The `class` construct will be described in the next chapter, but in JavaScript it's a "syntax sugar" and an extension of one of the patterns that we'll study here. - - -## Functional class pattern - -The constructor function below can be considered a "class" according to the definition: - -```js run -function User(name) { - this.sayHi = function() { - alert(name); - }; -} - -let user = new User("John"); -user.sayHi(); // John -``` - -It follows all parts of the definition: - -1. It is a "program-code-template" for creating objects (callable with `new`). -2. It provides initial values for the state (`name` from parameters). -3. It provides methods (`sayHi`). - -This is called *functional class pattern*. - -In the functional class pattern, local variables and nested functions inside `User`, that are not assigned to `this`, are visible from inside, but not accessible by the outer code. - -So we can easily add internal functions and variables, like `calcAge()` here: - -```js run -function User(name, birthday) { -*!* - // only visible from other methods inside User - function calcAge() { - return new Date().getFullYear() - birthday.getFullYear(); - } -*/!* - - this.sayHi = function() { - alert(`${name}, age:${calcAge()}`); - }; -} - -let user = new User("John", new Date(2000, 0, 1)); -user.sayHi(); // John, age:17 -``` - -In this code variables `name`, `birthday` and the function `calcAge()` are internal, *private* to the object. They are only visible from inside of it. - -On the other hand, `sayHi` is the external, *public* method. The external code that creates `user` can access it. - -This way we can hide internal implementation details and helper methods from the outer code. Only what's assigned to `this` becomes visible outside. - -## Factory class pattern - -We can create a class without using `new` at all. - -Like this: - -```js run -function User(name, birthday) { - // only visible from other methods inside User - function calcAge() { - return new Date().getFullYear() - birthday.getFullYear(); - } - - return { - sayHi() { - alert(`${name}, age:${calcAge()}`); - } - }; -} - -*!* -let user = User("John", new Date(2000, 0, 1)); -*/!* -user.sayHi(); // John, age:17 -``` - -As we can see, the function `User` returns an object with public properties and methods. The only benefit of this method is that we can omit `new`: write `let user = User(...)` instead of `let user = new User(...)`. In other aspects it's almost the same as the functional pattern. - -## Prototype-based classes - -Prototype-based classes are the most important and generally the best. Functional and factory class patterns are rarely used in practice. - -Soon you'll see why. - -Here's the same class rewritten using prototypes: - -```js run -function User(name, birthday) { -*!* - this._name = name; - this._birthday = birthday; -*/!* -} - -*!* -User.prototype._calcAge = function() { -*/!* - return new Date().getFullYear() - this._birthday.getFullYear(); -}; - -User.prototype.sayHi = function() { - alert(`${this._name}, age:${this._calcAge()}`); -}; - -let user = new User("John", new Date(2000, 0, 1)); -user.sayHi(); // John, age:17 -``` - -The code structure: - -- The constructor `User` only initializes the current object state. -- Methods are added to `User.prototype`. - -As we can see, methods are lexically not inside `function User`, they do not share a common lexical environment. If we declare variables inside `function User`, then they won't be visible to methods. - -So, there is a widely known agreement that internal properties and methods are prepended with an underscore `"_"`. Like `_name` or `_calcAge()`. Technically, that's just an agreement, the outer code still can access them. But most developers recognize the meaning of `"_"` and try not to touch prefixed properties and methods in the external code. - -Here are the advantages over the functional pattern: - -- In the functional pattern, each object has its own copy of every method. We assign a separate copy of `this.sayHi = function() {...}` and other methods in the constructor. -- In the prototypal pattern, all methods are in `User.prototype` that is shared between all user objects. An object itself only stores the data. - -So the prototypal pattern is more memory-efficient. - -...But not only that. Prototypes allow us to setup the inheritance in a really efficient way. Built-in JavaScript objects all use prototypes. Also there's a special syntax construct: "class" that provides nice-looking syntax for them. And there's more, so let's go on with them. - -## Prototype-based inheritance for classes - -Let's say we have two prototype-based classes. - -`Rabbit`: - -```js -function Rabbit(name) { - this.name = name; -} - -Rabbit.prototype.jump = function() { - alert(`${this.name} jumps!`); -}; - -let rabbit = new Rabbit("My rabbit"); -``` - -![](rabbit-animal-independent-1.png) - -...And `Animal`: - -```js -function Animal(name) { - this.name = name; -} - -Animal.prototype.eat = function() { - alert(`${this.name} eats.`); -}; - -let animal = new Animal("My animal"); -``` - -![](rabbit-animal-independent-2.png) - -Right now they are fully independent. - -But we'd want `Rabbit` to extend `Animal`. In other words, rabbits should be based on animals, have access to methods of `Animal` and extend them with its own methods. - -What does it mean in the language of prototypes? - -Right now methods for `rabbit` objects are in `Rabbit.prototype`. We'd like `rabbit` to use `Animal.prototype` as a "fallback", if the method is not found in `Rabbit.prototype`. - -So the prototype chain should be `rabbit` -> `Rabbit.prototype` -> `Animal.prototype`. - -Like this: - -![](class-inheritance-rabbit-animal.png) - -The code to implement that: - -```js run -// Same Animal as before -function Animal(name) { - this.name = name; -} - -// All animals can eat, right? -Animal.prototype.eat = function() { - alert(`${this.name} eats.`); -}; - -// Same Rabbit as before -function Rabbit(name) { - this.name = name; -} - -Rabbit.prototype.jump = function() { - alert(`${this.name} jumps!`); -}; - -*!* -// setup the inheritance chain -Rabbit.prototype.__proto__ = Animal.prototype; // (*) -*/!* - -let rabbit = new Rabbit("White Rabbit"); -*!* -rabbit.eat(); // rabbits can eat too -*/!* -rabbit.jump(); -``` - -The line `(*)` sets up the prototype chain. So that `rabbit` first searches methods in `Rabbit.prototype`, then `Animal.prototype`. And then, just for completeness, let's mention that if the method is not found in `Animal.prototype`, then the search continues in `Object.prototype`, because `Animal.prototype` is a regular plain object, so it inherits from it. - -So here's the full picture: - -![](class-inheritance-rabbit-animal-2.png) - -## Summary - -The term "class" comes from the object-oriented programming. In JavaScript it usually means the functional class pattern or the prototypal pattern. The prototypal pattern is more powerful and memory-efficient, so it's recommended to stick to it. - -According to the prototypal pattern: -1. Methods are stored in `Class.prototype`. -2. Prototypes inherit from each other. - -In the next chapter we'll study `class` keyword and construct. It allows to write prototypal classes shorter and provides some additional benefits. diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png deleted file mode 100644 index ad4a409322..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png deleted file mode 100644 index 199ed3ee60..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal-2@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal.png deleted file mode 100644 index 70708c2848..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal@2x.png deleted file mode 100644 index 0db1301815..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/class-inheritance-rabbit-animal@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1.png deleted file mode 100644 index e63d7d7844..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1@2x.png deleted file mode 100644 index 3d1be9ccef..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-1@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2.png deleted file mode 100644 index 435ec5f89f..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2@2x.png b/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2@2x.png deleted file mode 100644 index 5731da73a8..0000000000 Binary files a/1-js/07-object-oriented-programming/08-class-patterns/rabbit-animal-independent-2@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/clock.js b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/clock.js deleted file mode 100644 index c710b9da9b..0000000000 --- a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/clock.js +++ /dev/null @@ -1,34 +0,0 @@ -class Clock { - constructor({ template }) { - this._template = template; - } - - _render() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) min = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this._template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); - } - - stop() { - clearInterval(this._timer); - } - - start() { - this._render(); - this._timer = setInterval(() => this._render(), 1000); - } -} diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/index.html b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/index.html deleted file mode 100644 index fdee13d01b..0000000000 --- a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/solution.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/clock.js b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/clock.js deleted file mode 100644 index b1a2625079..0000000000 --- a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/clock.js +++ /dev/null @@ -1,34 +0,0 @@ - - -function Clock({ template }) { - this._template = template; -} - -Clock.prototype._render = function() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) min = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this._template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this._timer); -}; - -Clock.prototype.start = function() { - this._render(); - this._timer = setInterval(() => this._render(), 1000); -}; diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/index.html b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/index.html deleted file mode 100644 index fdee13d01b..0000000000 --- a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/source.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/task.md b/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/task.md deleted file mode 100644 index a29d347f52..0000000000 --- a/1-js/07-object-oriented-programming/09-class/1-rewrite-to-class/task.md +++ /dev/null @@ -1,9 +0,0 @@ -importance: 5 - ---- - -# Rewrite to class - -Rewrite the `Clock` class from prototypes to the modern "class" syntax. - -P.S. The clock ticks in the console, open it to see. diff --git a/1-js/07-object-oriented-programming/09-class/article.md b/1-js/07-object-oriented-programming/09-class/article.md deleted file mode 100644 index 71878ae35b..0000000000 --- a/1-js/07-object-oriented-programming/09-class/article.md +++ /dev/null @@ -1,355 +0,0 @@ - -# Classes - -The "class" construct allows to define prototype-based classes with a clean, nice-looking syntax. - -## The "class" syntax - -The `class` syntax is versatile, we'll start with a simple example first. - -Here's a prototype-based class `User`: - -```js run -function User(name) { - this.name = name; -} - -User.prototype.sayHi = function() { - alert(this.name); -} - -let user = new User("John"); -user.sayHi(); -``` - -...And that's the same using `class` syntax: - -```js run -class User { - - constructor(name) { - this.name = name; - } - - sayHi() { - alert(this.name); - } - -} - -let user = new User("John"); -user.sayHi(); -``` - -It's easy to see that the two examples are alike. Just please note that methods in a class do not have a comma between them. Novice developers sometimes forget it and put a comma between class methods, and things don't work. That's not a literal object, but a class syntax. - -So, what exactly does `class` do? We may think that it defines a new language-level entity, but that would be wrong. - -The `class User {...}` here actually does two things: - -1. Declares a variable `User` that references the function named `"constructor"`. -2. Puts methods listed in the definition into `User.prototype`. Here, it includes `sayHi` and the `constructor`. - -Here's the code to dig into the class and see that: - -```js run -class User { - constructor(name) { this.name = name; } - sayHi() { alert(this.name); } -} - -*!* -// proof: User is the "constructor" function -*/!* -alert(User === User.prototype.constructor); // true - -*!* -// proof: there are two methods in its "prototype" -*/!* -alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi -``` - -Here's the illustration of what `class User` creates: - -![](class-user.png) - - - -So `class` is a special syntax to define a constructor together with its prototype methods. - -...But not only that. There are minor tweaks here and there: - -Constructors require `new` -: Unlike a regular function, a class `constructor` can't be called without `new`: - -```js run -class User { - constructor() {} -} - -alert(typeof User); // function -User(); // Error: Class constructor User cannot be invoked without 'new' -``` - -Different string output -: If we output it like `alert(User)`, some engines show `"class User..."`, while others show `"function User..."`. - -Please don't be confused: the string representation may vary, but that's still a function, there is no separate "class" entity in JavaScript language. - -Class methods are non-enumerable -: A class definition sets `enumerable` flag to `false` for all methods in the `"prototype"`. That's good, because if we `for..in` over an object, we usually don't want its class methods. - -Classes have a default `constructor() {}` -: If there's no `constructor` in the `class` construct, then an empty function is generated, same as if we had written `constructor() {}`. - -Classes always `use strict` -: All code inside the class construct is automatically in strict mode. - -### Getters/setters - -Classes may also include getters/setters. Here's an example with `user.name` implemented using them: - -```js run -class User { - - constructor(name) { - // invokes the setter - this.name = name; - } - -*!* - get name() { -*/!* - return this._name; - } - -*!* - set name(value) { -*/!* - if (value.length < 4) { - alert("Name is too short."); - return; - } - this._name = value; - } - -} - -let user = new User("John"); -alert(user.name); // John - -user = new User(""); // Name too short. -``` - -Internally, getters and setters are also created on the `User` prototype, like this: - -```js -Object.defineProperties(User.prototype, { - name: { - get() { - return this._name - }, - set(name) { - // ... - } - } -}); -``` - -### Only methods - -Unlike object literals, no `property:value` assignments are allowed inside `class`. There may be only methods and getters/setters. There is some work going on in the specification to lift that limitation, but it's not yet there. - -If we really need to put a non-function value into the prototype, then we can alter `prototype` manually, like this: - -```js run -class User { } - -User.prototype.test = 5; - -alert( new User().test ); // 5 -``` - -So, technically that's possible, but we should know why we're doing it. Such properties will be shared among all objects of the class. - -An "in-class" alternative is to use a getter: - -```js run -class User { - get test() { - return 5; - } -} - -alert( new User().test ); // 5 -``` - -From the external code, the usage is the same. But the getter variant is a bit slower. - -## Class Expression - -Just like functions, classes can be defined inside another expression, passed around, returned etc. - -Here's a class-returning function ("class factory"): - -```js run -function makeClass(phrase) { -*!* - // declare a class and return it - return class { - sayHi() { - alert(phrase); - }; - }; -*/!* -} - -let User = makeClass("Hello"); - -new User().sayHi(); // Hello -``` - -That's quite normal if we recall that `class` is just a special form of a function-with-prototype definition. - -And, like Named Function Expressions, such classes also may have a name, that is visible inside that class only: - -```js run -// "Named Class Expression" (alas, no such term, but that's what's going on) -let User = class *!*MyClass*/!* { - sayHi() { - alert(MyClass); // MyClass is visible only inside the class - } -}; - -new User().sayHi(); // works, shows MyClass definition - -alert(MyClass); // error, MyClass not visible outside of the class -``` - -## Static methods - -We can also assign methods to the class function, not to its `"prototype"`. Such methods are called *static*. - -An example: - -```js run -class User { -*!* - static staticMethod() { -*/!* - alert(this === User); - } -} - -User.staticMethod(); // true -``` - -That actually does the same as assigning it as a function property: - -```js -function User() { } - -User.staticMethod = function() { - alert(this === User); -}; -``` - -The value of `this` inside `User.staticMethod()` is the class constructor `User` itself (the "object before dot" rule). - -Usually, static methods are used to implement functions that belong to the class, but not to any particular object of it. - -For instance, we have `Article` objects and need a function to compare them. The natural choice would be `Article.compare`, like this: - -```js run -class Article { - constructor(title, date) { - this.title = title; - this.date = date; - } - -*!* - static compare(articleA, articleB) { - return articleA.date - articleB.date; - } -*/!* -} - -// usage -let articles = [ - new Article("Mind", new Date(2016, 1, 1)), - new Article("Body", new Date(2016, 0, 1)), - new Article("JavaScript", new Date(2016, 11, 1)) -]; - -*!* -articles.sort(Article.compare); -*/!* - -alert( articles[0].title ); // Body -``` - -Here `Article.compare` stands "over" the articles, as a means to compare them. It's not a method of an article, but rather of the whole class. - -Another example would be a so-called "factory" method. Imagine, we need few ways to create an article: - -1. Create by given parameters (`title`, `date` etc). -2. Create an empty article with today's date. -3. ... - -The first way can be implemented by the constructor. And for the second one we can make a static method of the class. - -Like `Article.createTodays()` here: - -```js run -class Article { - constructor(title, date) { - this.title = title; - this.date = date; - } - -*!* - static createTodays() { - // remember, this = Article - return new this("Today's digest", new Date()); - } -*/!* -} - -let article = Article.createTodays(); - -alert( article.title ); // Todays digest -``` - -Now every time we need to create a today's digest, we can call `Article.createTodays()`. Once again, that's not a method of an article, but a method of the whole class. - -Static methods are also used in database-related classes to search/save/remove entries from the database, like this: - -```js -// assuming Article is a special class for managing articles -// static method to remove the article: -Article.remove({id: 12345}); -``` - -## Summary - -The basic class syntax looks like this: - -```js -class MyClass { - constructor(...) { - // ... - } - method1(...) {} - method2(...) {} - get something(...) {} - set something(...) {} - static staticMethod(..) {} - // ... -} -``` - -The value of `MyClass` is a function provided as `constructor`. If there's no `constructor`, then an empty function. - -In any case, methods listed in the class declaration become members of its `prototype`, with the exception of static methods that are written into the function itself and callable as `MyClass.staticMethod()`. Static methods are used when we need a function bound to a class, but not to any object of that class. - -In the next chapter we'll learn more about classes, including inheritance. diff --git a/1-js/07-object-oriented-programming/09-class/class-user.png b/1-js/07-object-oriented-programming/09-class/class-user.png deleted file mode 100644 index 5579e6bbdf..0000000000 Binary files a/1-js/07-object-oriented-programming/09-class/class-user.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/09-class/class-user@2x.png b/1-js/07-object-oriented-programming/09-class/class-user@2x.png deleted file mode 100644 index 5a85e6589d..0000000000 Binary files a/1-js/07-object-oriented-programming/09-class/class-user@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/clock.js b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/clock.js deleted file mode 100644 index c710b9da9b..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/clock.js +++ /dev/null @@ -1,34 +0,0 @@ -class Clock { - constructor({ template }) { - this._template = template; - } - - _render() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) min = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this._template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); - } - - stop() { - clearInterval(this._timer); - } - - start() { - this._render(); - this._timer = setInterval(() => this._render(), 1000); - } -} diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js deleted file mode 100644 index 4eb12381f9..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js +++ /dev/null @@ -1,12 +0,0 @@ -class ExtendedClock extends Clock { - constructor(options) { - super(options); - let { precision=1000 } = options; - this._precision = precision; - } - - start() { - this._render(); - this._timer = setInterval(() => this._render(), this._precision); - } -}; diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/index.html b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/index.html deleted file mode 100644 index 7ac1db7143..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.view/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - Console clock - - - - - - - - - - - diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/clock.js b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/clock.js deleted file mode 100644 index c710b9da9b..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/clock.js +++ /dev/null @@ -1,34 +0,0 @@ -class Clock { - constructor({ template }) { - this._template = template; - } - - _render() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) min = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this._template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); - } - - stop() { - clearInterval(this._timer); - } - - start() { - this._render(); - this._timer = setInterval(() => this._render(), 1000); - } -} diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/index.html b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/index.html deleted file mode 100644 index b48a2a0074..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/source.view/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Console clock - - - - - - - - - - - diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/task.md b/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/task.md deleted file mode 100644 index 05da453875..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/task.md +++ /dev/null @@ -1,12 +0,0 @@ -importance: 5 - ---- - -# Extended clock - -We've got a `Clock` class. As of now, it prints the time every second. - -Create a new class `ExtendedClock` that inherits from `Clock` and adds the parameter `precision` -- the number of `ms` between "ticks". Should be `1000` (1 second) by default. - -- Your code should be in the file `extended-clock.js` -- Don't modify the original `clock.js`. Extend it. diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object.png b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object.png deleted file mode 100644 index d4ff37e561..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png deleted file mode 100644 index a54a9d2f86..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/solution.md b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/solution.md deleted file mode 100644 index c1483aa365..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/solution.md +++ /dev/null @@ -1,81 +0,0 @@ -First, let's see why the latter code doesn't work. - -The reason becomes obvious if we try to run it. An inheriting class constructor must call `super()`. Otherwise `"this"` won't be "defined". - -So here's the fix: - -```js run -class Rabbit extends Object { - constructor(name) { -*!* - super(); // need to call the parent constructor when inheriting -*/!* - this.name = name; - } -} - -let rabbit = new Rabbit("Rab"); - -alert( rabbit.hasOwnProperty('name') ); // true -``` - -But that's not all yet. - -Even after the fix, there's still important difference in `"class Rabbit extends Object"` versus `class Rabbit`. - -As we know, the "extends" syntax sets up two prototypes: - -1. Between `"prototype"` of the constructor functions (for methods). -2. Between the constructor functions itself (for static methods). - -In our case, for `class Rabbit extends Object` it means: - -```js run -class Rabbit extends Object {} - -alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true -alert( Rabbit.__proto__ === Object ); // (2) true -``` - -So `Rabbit` now provides access to static methods of `Object` via `Rabbit`, like this: - -```js run -class Rabbit extends Object {} - -*!* -// normally we call Object.getOwnPropertyNames -alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b -*/!* -``` - -But if we don't have `extends Object`, then `Rabbit.__proto__` is not set to `Object`. - -Here's the demo: - -```js run -class Rabbit {} - -alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true -alert( Rabbit.__proto__ === Object ); // (2) false (!) -alert( Rabbit.__proto__ === Function.prototype ); // as any function by default - -*!* -// error, no such function in Rabbit -alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error -*/!* -``` - -So `Rabbit` doesn't provide access to static methods of `Object` in that case. - -By the way, `Function.prototype` has "generic" function methods, like `call`, `bind` etc. They are ultimately available in both cases, because for the built-in `Object` constructor, `Object.__proto__ === Function.prototype`. - -Here's the picture: - -![](rabbit-extends-object.png) - -So, to put it short, there are two differences: - -| class Rabbit | class Rabbit extends Object | -|--------------|------------------------------| -| -- | needs to call `super()` in constructor | -| `Rabbit.__proto__ === Function.prototype` | `Rabbit.__proto__ === Object` | diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/task.md b/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/task.md deleted file mode 100644 index ca6628edf1..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/3-class-extend-object/task.md +++ /dev/null @@ -1,43 +0,0 @@ -importance: 5 - ---- - -# Class extends Object? - -As we know, all objects normally inherit from `Object.prototype` and get access to "generic" object methods like `hasOwnProperty` etc. - -For instance: - -```js run -class Rabbit { - constructor(name) { - this.name = name; - } -} - -let rabbit = new Rabbit("Rab"); - -*!* -// hasOwnProperty method is from Object.prototype -// rabbit.__proto__ === Object.prototype -alert( rabbit.hasOwnProperty('name') ); // true -*/!* -``` - -But if we spell it out explicitly like `"class Rabbit extends Object"`, then the result would be different from a simple `"class Rabbit"`? - -What's the difference? - -Here's an example of such code (it doesn't work -- why? fix it?): - -```js -class Rabbit extends Object { - constructor(name) { - this.name = name; - } -} - -let rabbit = new Rabbit("Rab"); - -alert( rabbit.hasOwnProperty('name') ); // true -``` diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-extends.png b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-extends.png deleted file mode 100644 index 2db88f3663..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-extends.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-extends@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-extends@2x.png deleted file mode 100644 index 9539fe9ec6..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-extends@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static.png b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static.png deleted file mode 100644 index 998c823300..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static@2x.png deleted file mode 100644 index 98a80d38fa..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/animal-rabbit-static@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/article.md b/1-js/07-object-oriented-programming/10-class-inheritance/article.md deleted file mode 100644 index 2c44e5eadd..0000000000 --- a/1-js/07-object-oriented-programming/10-class-inheritance/article.md +++ /dev/null @@ -1,575 +0,0 @@ - -# Class inheritance, super - -Classes can extend one another. There's a nice syntax, technically based on the prototypal inheritance. - -To inherit from another class, we should specify `"extends"` and the parent class before the brackets `{..}`. - -Here `Rabbit` inherits from `Animal`: - -```js run -class Animal { - - constructor(name) { - this.speed = 0; - this.name = name; - } - - run(speed) { - this.speed += speed; - alert(`${this.name} runs with speed ${this.speed}.`); - } - - stop() { - this.speed = 0; - alert(`${this.name} stopped.`); - } - -} - -*!* -// Inherit from Animal -class Rabbit extends Animal { - hide() { - alert(`${this.name} hides!`); - } -} -*/!* - -let rabbit = new Rabbit("White Rabbit"); - -rabbit.run(5); // White Rabbit runs with speed 5. -rabbit.hide(); // White Rabbit hides! -``` - -The `extends` keyword actually adds a `[[Prototype]]` reference from `Rabbit.prototype` to `Animal.prototype`, just as you expect it to be, and as we've seen before. - -![](animal-rabbit-extends.png) - -So now `rabbit` has access both to its own methods and to methods of `Animal`. - -````smart header="Any expression is allowed after `extends`" -Class syntax allows to specify not just a class, but any expression after `extends`. - -For instance, a function call that generates the parent class: - -```js run -function f(phrase) { - return class { - sayHi() { alert(phrase) } - } -} - -*!* -class User extends f("Hello") {} -*/!* - -new User().sayHi(); // Hello -``` -Here `class User` inherits from the result of `f("Hello")`. - -That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them. -```` - -## Overriding a method - -Now let's move forward and override a method. As of now, `Rabbit` inherits the `stop` method that sets `this.speed = 0` from `Animal`. - -If we specify our own `stop` in `Rabbit`, then it will be used instead: - -```js -class Rabbit extends Animal { - stop() { - // ...this will be used for rabbit.stop() - } -} -``` - - -...But usually we don't want to totally replace a parent method, but rather to build on top of it, tweak or extend its functionality. We do something in our method, but call the parent method before/after it or in the process. - -Classes provide `"super"` keyword for that. - -- `super.method(...)` to call a parent method. -- `super(...)` to call a parent constructor (inside our constructor only). - -For instance, let our rabbit autohide when stopped: - -```js run -class Animal { - - constructor(name) { - this.speed = 0; - this.name = name; - } - - run(speed) { - this.speed += speed; - alert(`${this.name} runs with speed ${this.speed}.`); - } - - stop() { - this.speed = 0; - alert(`${this.name} stopped.`); - } - -} - -class Rabbit extends Animal { - hide() { - alert(`${this.name} hides!`); - } - -*!* - stop() { - super.stop(); // call parent stop - this.hide(); // and then hide - } -*/!* -} - -let rabbit = new Rabbit("White Rabbit"); - -rabbit.run(5); // White Rabbit runs with speed 5. -rabbit.stop(); // White Rabbit stopped. White rabbit hides! -``` - -Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the process. - -````smart header="Arrow functions have no `super`" -As was mentioned in the chapter , arrow functions do not have `super`. - -If accessed, it's taken from the outer function. For instance: -```js -class Rabbit extends Animal { - stop() { - setTimeout(() => super.stop(), 1000); // call parent stop after 1sec - } -} -``` - -The `super` in the arrow function is the same as in `stop()`, so it works as intended. If we specified a "regular" function here, there would be an error: - -```js -// Unexpected super -setTimeout(function() { super.stop() }, 1000); -``` -```` - - -## Overriding constructor - -With constructors it gets a little bit tricky. - -Till now, `Rabbit` did not have its own `constructor`. - -According to the [specification](https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation), if a class extends another class and has no `constructor`, then the following `constructor` is generated: - -```js -class Rabbit extends Animal { - // generated for extending classes without own constructors -*!* - constructor(...args) { - super(...args); - } -*/!* -} -``` - -As we can see, it basically calls the parent `constructor` passing it all the arguments. That happens if we don't write a constructor of our own. - -Now let's add a custom constructor to `Rabbit`. It will specify the `earLength` in addition to `name`: - -```js run -class Animal { - constructor(name) { - this.speed = 0; - this.name = name; - } - // ... -} - -class Rabbit extends Animal { - -*!* - constructor(name, earLength) { - this.speed = 0; - this.name = name; - this.earLength = earLength; - } -*/!* - - // ... -} - -*!* -// Doesn't work! -let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined. -*/!* -``` - -Whoops! We've got an error. Now we can't create rabbits. What went wrong? - -The short answer is: constructors in inheriting classes must call `super(...)`, and (!) do it before using `this`. - -...But why? What's going on here? Indeed, the requirement seems strange. - -Of course, there's an explanation. Let's get into details, so you'd really understand what's going on. - -In JavaScript, there's a distinction between a "constructor function of an inheriting class" and all others. In an inheriting class, the corresponding constructor function is labelled with a special internal property `[[ConstructorKind]]:"derived"`. - -The difference is: - -- When a normal constructor runs, it creates an empty object as `this` and continues with it. -- But when a derived constructor runs, it doesn't do it. It expects the parent constructor to do this job. - -So if we're making a constructor of our own, then we must call `super`, because otherwise the object with `this` reference to it won't be created. And we'll get an error. - -For `Rabbit` to work, we need to call `super()` before using `this`, like here: - -```js run -class Animal { - - constructor(name) { - this.speed = 0; - this.name = name; - } - - // ... -} - -class Rabbit extends Animal { - - constructor(name, earLength) { -*!* - super(name); -*/!* - this.earLength = earLength; - } - - // ... -} - -*!* -// now fine -let rabbit = new Rabbit("White Rabbit", 10); -alert(rabbit.name); // White Rabbit -alert(rabbit.earLength); // 10 -*/!* -``` - - -## Super: internals, [[HomeObject]] - -Let's get a little deeper under the hood of `super`. We'll see some interesting things by the way. - -First to say, from all that we've learned till now, it's impossible for `super` to work. - -Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, how to retrieve the `method`? Naturally, we need to take the `method` from the prototype of the current object. How, technically, we (or a JavaScript engine) can do it? - -Maybe we can get the method from `[[Prototype]]` of `this`, as `this.__proto__.method`? Unfortunately, that doesn't work. - -Let's try to do it. Without classes, using plain objects for the sake of simplicity. - -Here, `rabbit.eat()` should call `animal.eat()` method of the parent object: - -```js run -let animal = { - name: "Animal", - eat() { - alert(`${this.name} eats.`); - } -}; - -let rabbit = { - __proto__: animal, - name: "Rabbit", - eat() { -*!* - // that's how super.eat() could presumably work - this.__proto__.eat.call(this); // (*) -*/!* - } -}; - -rabbit.eat(); // Rabbit eats. -``` - -At the line `(*)` we take `eat` from the prototype (`animal`) and call it in the context of the current object. Please note that `.call(this)` is important here, because a simple `this.__proto__.eat()` would execute parent `eat` in the context of the prototype, not the current object. - -And in the code above it actually works as intended: we have the correct `alert`. - -Now let's add one more object to the chain. We'll see how things break: - -```js run -let animal = { - name: "Animal", - eat() { - alert(`${this.name} eats.`); - } -}; - -let rabbit = { - __proto__: animal, - eat() { - // ...bounce around rabbit-style and call parent (animal) method - this.__proto__.eat.call(this); // (*) - } -}; - -let longEar = { - __proto__: rabbit, - eat() { - // ...do something with long ears and call parent (rabbit) method - this.__proto__.eat.call(this); // (**) - } -}; - -*!* -longEar.eat(); // Error: Maximum call stack size exceeded -*/!* -``` - -The code doesn't work anymore! We can see the error trying to call `longEar.eat()`. - -It may be not that obvious, but if we trace `longEar.eat()` call, then we can see why. In both lines `(*)` and `(**)` the value of `this` is the current object (`longEar`). That's essential: all object methods get the current object as `this`, not a prototype or something. - -So, in both lines `(*)` and `(**)` the value of `this.__proto__` is exactly the same: `rabbit`. They both call `rabbit.eat` without going up the chain in the endless loop. - -Here's the picture of what happens: - -![](this-super-loop.png) - -1. Inside `longEar.eat()`, the line `(**)` calls `rabbit.eat` providing it with `this=longEar`. - ```js - // inside longEar.eat() we have this = longEar - this.__proto__.eat.call(this) // (**) - // becomes - longEar.__proto__.eat.call(this) - // that is - rabbit.eat.call(this); - ``` -2. Then in the line `(*)` of `rabbit.eat`, we'd like to pass the call even higher in the chain, but `this=longEar`, so `this.__proto__.eat` is again `rabbit.eat`! - - ```js - // inside rabbit.eat() we also have this = longEar - this.__proto__.eat.call(this) // (*) - // becomes - longEar.__proto__.eat.call(this) - // or (again) - rabbit.eat.call(this); - ``` - -3. ...So `rabbit.eat` calls itself in the endless loop, because it can't ascend any further. - -The problem can't be solved by using `this` alone. - -### `[[HomeObject]]` - -To provide the solution, JavaScript adds one more special internal property for functions: `[[HomeObject]]`. - -**When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object.** - -This actually violates the idea of "unbound" functions, because methods remember their objects. And `[[HomeObject]]` can't be changed, so this bound is forever. So that's a very important change in the language. - -But this change is safe. `[[HomeObject]]` is used only for calling parent methods in `super`, to resolve the prototype. So it doesn't break compatibility. - -Let's see how it works for `super` -- again, using plain objects: - -```js run -let animal = { - name: "Animal", - eat() { // [[HomeObject]] == animal - alert(`${this.name} eats.`); - } -}; - -let rabbit = { - __proto__: animal, - name: "Rabbit", - eat() { // [[HomeObject]] == rabbit - super.eat(); - } -}; - -let longEar = { - __proto__: rabbit, - name: "Long Ear", - eat() { // [[HomeObject]] == longEar - super.eat(); - } -}; - -*!* -longEar.eat(); // Long Ear eats. -*/!* -``` - -Every method remembers its object in the internal `[[HomeObject]]` property. Then `super` uses it to resolve the parent prototype. - -`[[HomeObject]]` is defined for methods defined both in classes and in plain objects. But for objects, methods must be specified exactly the given way: as `method()`, not as `"method: function()"`. - -In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work: - -```js run -let animal = { - eat: function() { // should be the short syntax: eat() {...} - // ... - } -}; - -let rabbit = { - __proto__: animal, - eat: function() { - super.eat(); - } -}; - -*!* -rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) -*/!* -``` - -## Static methods and inheritance - -The `class` syntax supports inheritance for static properties too. - -For instance: - -```js run -class Animal { - - constructor(name, speed) { - this.speed = speed; - this.name = name; - } - - run(speed = 0) { - this.speed += speed; - alert(`${this.name} runs with speed ${this.speed}.`); - } - - static compare(animalA, animalB) { - return animalA.speed - animalB.speed; - } - -} - -// Inherit from Animal -class Rabbit extends Animal { - hide() { - alert(`${this.name} hides!`); - } -} - -let rabbits = [ - new Rabbit("White Rabbit", 10), - new Rabbit("Black Rabbit", 5) -]; - -rabbits.sort(Rabbit.compare); - -rabbits[0].run(); // Black Rabbit runs with speed 5. -``` - -Now we can call `Rabbit.compare` assuming that the inherited `Animal.compare` will be called. - -How does it work? Again, using prototypes. As you might have already guessed, extends also gives `Rabbit` the `[[Prototype]]` reference to `Animal`. - - -![](animal-rabbit-static.png) - -So, `Rabbit` function now inherits from `Animal` function. And `Animal` function normally has `[[Prototype]]` referencing `Function.prototype`, because it doesn't `extend` anything. - -Here, let's check that: - -```js run -class Animal {} -class Rabbit extends Animal {} - -// for static propertites and methods -alert(Rabbit.__proto__ === Animal); // true - -// and the next step is Function.prototype -alert(Animal.__proto__ === Function.prototype); // true - -// that's in addition to the "normal" prototype chain for object methods -alert(Rabbit.prototype.__proto__ === Animal.prototype); -``` - -This way `Rabbit` has access to all static methods of `Animal`. - -### No static inheritance in built-ins - -Please note that built-in classes don't have such static `[[Prototype]]` reference. For instance, `Object` has `Object.defineProperty`, `Object.keys` and so on, but `Array`, `Date` etc do not inherit them. - -Here's the picture structure for `Date` and `Object`: - -![](object-date-inheritance.png) - -Note, there's no link between `Date` and `Object`. Both `Object` and `Date` exist independently. `Date.prototype` inherits from `Object.prototype`, but that's all. - -Such difference exists for historical reasons: there was no thought about class syntax and inheriting static methods at the dawn of JavaScript language. - -## Natives are extendable - -Built-in classes like Array, Map and others are extendable also. - -For instance, here `PowerArray` inherits from the native `Array`: - -```js run -// add one more method to it (can do more) -class PowerArray extends Array { - isEmpty() { - return this.length === 0; - } -} - -let arr = new PowerArray(1, 2, 5, 10, 50); -alert(arr.isEmpty()); // false - -let filteredArr = arr.filter(item => item >= 10); -alert(filteredArr); // 10, 50 -alert(filteredArr.isEmpty()); // false -``` - -Please note one very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type. They rely on the `constructor` property to do so. - -In the example above, -```js -arr.constructor === PowerArray -``` - -So when `arr.filter()` is called, it internally creates the new array of results exactly as `new PowerArray`. And we can keep using its methods further down the chain. - -Even more, we can customize that behavior. The static getter `Symbol.species`, if exists, returns the constructor to use in such cases. - -For example, here due to `Symbol.species` built-in methods like `map`, `filter` will return "normal" arrays: - -```js run -class PowerArray extends Array { - isEmpty() { - return this.length === 0; - } - -*!* - // built-in methods will use this as the constructor - static get [Symbol.species]() { - return Array; - } -*/!* -} - -let arr = new PowerArray(1, 2, 5, 10, 50); -alert(arr.isEmpty()); // false - -// filter creates new array using arr.constructor[Symbol.species] as constructor -let filteredArr = arr.filter(item => item >= 10); - -*!* -// filteredArr is not PowerArray, but Array -*/!* -alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function -``` - -We can use it in more advanced keys to strip extended functionality from resulting values if not needed. Or, maybe, to extend it even further. diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object.png deleted file mode 100644 index c5d712632f..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object@2x.png deleted file mode 100644 index edc4e841ec..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-array-object@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal.png deleted file mode 100644 index 70708c2848..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal@2x.png deleted file mode 100644 index 0db1301815..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-animal@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal.png deleted file mode 100644 index 387975a9c7..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal@2x.png deleted file mode 100644 index ca73135933..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/class-inheritance-rabbit-run-animal@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance.png b/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance.png deleted file mode 100644 index 542a0c9fa9..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance@2x.png deleted file mode 100644 index 21485062a8..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/object-date-inheritance@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop.png b/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop.png deleted file mode 100644 index 637d179398..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop@2x.png b/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop@2x.png deleted file mode 100644 index af7b443bcb..0000000000 Binary files a/1-js/07-object-oriented-programming/10-class-inheritance/this-super-loop@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/task.md b/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/task.md deleted file mode 100644 index e9481912ae..0000000000 --- a/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/task.md +++ /dev/null @@ -1,20 +0,0 @@ -importance: 5 - ---- - -# Strange instanceof - -Why `instanceof` below returns `true`? We can easily see that `a` is not created by `B()`. - -```js run -function A() {} -function B() {} - -A.prototype = B.prototype = {}; - -let a = new A(); - -*!* -alert( a instanceof B ); // true -*/!* -``` diff --git a/1-js/07-object-oriented-programming/11-instanceof/article.md b/1-js/07-object-oriented-programming/11-instanceof/article.md deleted file mode 100644 index 702c9e6b20..0000000000 --- a/1-js/07-object-oriented-programming/11-instanceof/article.md +++ /dev/null @@ -1,211 +0,0 @@ -# Class checking: "instanceof" - -The `instanceof` operator allows to check whether an object belongs to a certain class. It also takes inheritance into account. - -Such a check may be necessary in many cases, here we'll use it for building a *polymorphic* function, the one that treats arguments differently depending on their type. - -## The instanceof operator [#ref-instanceof] - -The syntax is: -```js -obj instanceof Class -``` - -It returns `true` if `obj` belongs to the `Class` (or a class inheriting from it). - -For instance: - -```js run -class Rabbit {} -let rabbit = new Rabbit(); - -// is it an object of Rabbit class? -*!* -alert( rabbit instanceof Rabbit ); // true -*/!* -``` - -It also works with constructor functions: - -```js run -*!* -// instead of class -function Rabbit() {} -*/!* - -alert( new Rabbit() instanceof Rabbit ); // true -``` - -...And with built-in classes like `Array`: - -```js run -let arr = [1, 2, 3]; -alert( arr instanceof Array ); // true -alert( arr instanceof Object ); // true -``` - -Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypally inherits from `Object`. - -The `instanceof` operator examines the prototype chain for the check, and is also fine-tunable using the static method `Symbol.hasInstance`. - -The algorithm of `obj instanceof Class` works roughly as follows: - -1. If there's a static method `Symbol.hasInstance`, then use it. Like this: - - ```js run - // assume anything that canEat is an animal - class Animal { - static [Symbol.hasInstance](obj) { - if (obj.canEat) return true; - } - } - - let obj = { canEat: true }; - alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called - ``` - -2. Most classes do not have `Symbol.hasInstance`. In that case, check if `Class.prototype` equals to one of prototypes in the `obj` prototype chain. - - In other words, compare: - ```js - obj.__proto__ === Class.prototype - obj.__proto__.__proto__ === Class.prototype - obj.__proto__.__proto__.__proto__ === Class.prototype - ... - ``` - - In the example above `Rabbit.prototype === rabbit.__proto__`, so that gives the answer immediately. - - In the case of an inheritance, `rabbit` is an instance of the parent class as well: - - ```js run - class Animal {} - class Rabbit extends Animal {} - - let rabbit = new Rabbit(); - *!* - alert(rabbit instanceof Animal); // true - */!* - // rabbit.__proto__ === Rabbit.prototype - // rabbit.__proto__.__proto__ === Animal.prototype (match!) - ``` - -Here's the illustration of what `rabbit instanceof Animal` compares with `Animal.prototype`: - -![](instanceof.png) - -By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), that returns `true` if `objA` is somewhere in the chain of prototypes for `objB`. So the test of `obj instanceof Class` can be rephrased as `Class.prototype.isPrototypeOf(obj)`. - -That's funny, but the `Class` constructor itself does not participate in the check! Only the chain of prototypes and `Class.prototype` matters. - -That can lead to interesting consequences when `prototype` is changed. - -Like here: - -```js run -function Rabbit() {} -let rabbit = new Rabbit(); - -// changed the prototype -Rabbit.prototype = {}; - -// ...not a rabbit any more! -*!* -alert( rabbit instanceof Rabbit ); // false -*/!* -``` - -That's one of the reasons to avoid changing `prototype`. Just to keep safe. - -## Bonus: Object toString for the type - -We already know that plain objects are converted to string as `[object Object]`: - -```js run -let obj = {}; - -alert(obj); // [object Object] -alert(obj.toString()); // the same -``` - -That's their implementation of `toString`. But there's a hidden feature that makes `toString` actually much more powerful than that. We can use it as an extended `typeof` and an alternative for `instanceof`. - -Sounds strange? Indeed. Let's demystify. - -By [specification](https://tc39.github.io/ecma262/#sec-object.prototype.tostring), the built-in `toString` can be extracted from the object and executed in the context of any other value. And its result depends on that value. - -- For a number, it will be `[object Number]` -- For a boolean, it will be `[object Boolean]` -- For `null`: `[object Null]` -- For `undefined`: `[object Undefined]` -- For arrays: `[object Array]` -- ...etc (customizable). - -Let's demonstrate: - -```js run -// copy toString method into a variable for convenience -let objectToString = Object.prototype.toString; - -// what type is this? -let arr = []; - -alert( objectToString.call(arr) ); // [object Array] -``` - -Here we used [call](mdn:js/function/call) as described in the chapter [](info:call-apply-decorators) to execute the function `objectToString` in the context `this=arr`. - -Internally, the `toString` algorithm examines `this` and returns the corresponding result. More examples: - -```js run -let s = Object.prototype.toString; - -alert( s.call(123) ); // [object Number] -alert( s.call(null) ); // [object Null] -alert( s.call(alert) ); // [object Function] -``` - -### Symbol.toStringTag - -The behavior of Object `toString` can be customized using a special object property `Symbol.toStringTag`. - -For instance: - -```js run -let user = { - [Symbol.toStringTag]: "User" -}; - -alert( {}.toString.call(user) ); // [object User] -``` - -For most environment-specific objects, there is such a property. Here are few browser specific examples: - -```js run -// toStringTag for the envinronment-specific object and class: -alert( window[Symbol.toStringTag]); // window -alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest - -alert( {}.toString.call(window) ); // [object Window] -alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] -``` - -As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped into `[object ...]`. - -At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized. - -It can be used instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check. - -## Summary - -Let's recap the type-checking methods that we know: - -| | works for | returns | -|---------------|-------------|---------------| -| `typeof` | primitives | string | -| `{}.toString` | primitives, built-in objects, objects with `Symbol.toStringTag` | string | -| `instanceof` | objects | true/false | - -As we can see, `{}.toString` is technically a "more advanced" `typeof`. - -And `instanceof` operator really shines when we are working with a class hierarchy and want to check for the class taking into account inheritance. diff --git a/1-js/07-object-oriented-programming/11-instanceof/instanceof.png b/1-js/07-object-oriented-programming/11-instanceof/instanceof.png deleted file mode 100644 index 85aa9a55f3..0000000000 Binary files a/1-js/07-object-oriented-programming/11-instanceof/instanceof.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/11-instanceof/instanceof@2x.png b/1-js/07-object-oriented-programming/11-instanceof/instanceof@2x.png deleted file mode 100644 index fba7712207..0000000000 Binary files a/1-js/07-object-oriented-programming/11-instanceof/instanceof@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/13-mixins/article.md b/1-js/07-object-oriented-programming/13-mixins/article.md deleted file mode 100644 index bb51395e69..0000000000 --- a/1-js/07-object-oriented-programming/13-mixins/article.md +++ /dev/null @@ -1,205 +0,0 @@ -# Mixins - -In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]` for an object. And a class may extend only one other class. - -But sometimes that feels limiting. For instance, I have a class `StreetSweeper` and a class `Bicycle`, and want to make a `StreetSweepingBicycle`. - -Or, talking about programming, we have a class `Renderer` that implements templating and a class `EventEmitter` that implements event handling, and want to merge these functionalities together with a class `Page`, to make a page that can use templates and emit events. - -There's a concept that can help here, called "mixins". - -As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class that contains methods for use by other classes without having to be the parent class of those other classes. - -In other words, a *mixin* provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes. - -## A mixin example - -The simplest way to make a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class. - -For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`: - -```js run -*!* -// mixin -*/!* -let sayHiMixin = { - sayHi() { - alert(`Hello ${this.name}`); - }, - sayBye() { - alert(`Bye ${this.name}`); - } -}; - -*!* -// usage: -*/!* -class User { - constructor(name) { - this.name = name; - } -} - -// copy the methods -Object.assign(User.prototype, sayHiMixin); - -// now User can say hi -new User("Dude").sayHi(); // Hello Dude! -``` - -There's no inheritance, but a simple method copying. So `User` may extend some other class and also include the mixin to "mix-in" the additional methods, like this: - -```js -class User extends Person { - // ... -} - -Object.assign(User.prototype, sayHiMixin); -``` - -Mixins can make use of inheritance inside themselves. - -For instance, here `sayHiMixin` inherits from `sayMixin`: - -```js run -let sayMixin = { - say(phrase) { - alert(phrase); - } -}; - -let sayHiMixin = { - __proto__: sayMixin, // (or we could use Object.create to set the prototype here) - - sayHi() { - *!* - // call parent method - */!* - super.say(`Hello ${this.name}`); - }, - sayBye() { - super.say(`Bye ${this.name}`); - } -}; - -class User { - constructor(name) { - this.name = name; - } -} - -// copy the methods -Object.assign(User.prototype, sayHiMixin); - -// now User can say hi -new User("Dude").sayHi(); // Hello Dude! -``` - -Please note that the call to the parent method `super.say()` from `sayHiMixin` looks for the method in the prototype of that mixin, not the class. - -![](mixin-inheritance.png) - -That's because methods from `sayHiMixin` have `[[HomeObject]]` set to it. So `super` actually means `sayHiMixin.__proto__`, not `User.__proto__`. - -## EventMixin - -Now let's make a mixin for real life. - -The important feature of many objects is working with events. - -That is: an object should have a method to "generate an event" when something important happens to it, and other objects should be able to "listen" to such events. - -An event must have a name and, optionally, bundle some additional data. - -For instance, an object `user` can generate an event `"login"` when the visitor logs in. And another object `calendar` may want to receive such events to load the calendar for the logged-in person. - -Or, a `menu` can generate the event `"select"` when a menu item is selected, and other objects may want to get that information and react on that event. - -Events is a way to "share information" with anyone who wants it. They can be useful in any class, so let's make a mixin for them: - -```js run -let eventMixin = { - /** - * Subscribe to event, usage: - * menu.on('select', function(item) { ... } - */ - on(eventName, handler) { - if (!this._eventHandlers) this._eventHandlers = {}; - if (!this._eventHandlers[eventName]) { - this._eventHandlers[eventName] = []; - } - this._eventHandlers[eventName].push(handler); - }, - - /** - * Cancel the subscription, usage: - * menu.off('select', handler) - */ - off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; - if (!handlers) return; - for (let i = 0; i < handlers.length; i++) { - if (handlers[i] === handler) { - handlers.splice(i--, 1); - } - } - }, - - /** - * Generate the event and attach the data to it - * this.trigger('select', data1, data2); - */ - trigger(eventName, ...args) { - if (!this._eventHandlers || !this._eventHandlers[eventName]) { - return; // no handlers for that event name - } - - // call the handlers - this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); - } -}; -``` - -There are 3 methods here: - -1. `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name happens. The handlers are stored in the `_eventHandlers` property. -2. `.off(eventName, handler)` -- removes the function from the handlers list. -3. `.trigger(eventName, ...args)` -- generates the event: all assigned handlers are called and `args` are passed as arguments to them. - - -Usage: - -```js run -// Make a class -class Menu { - choose(value) { - this.trigger("select", value); - } -} -// Add the mixin -Object.assign(Menu.prototype, eventMixin); - -let menu = new Menu(); - -// call the handler on selection: -*!* -menu.on("select", value => alert(`Value selected: ${value}`)); -*/!* - -// triggers the event => shows Value selected: 123 -menu.choose("123"); // value selected -``` - -Now if we have the code interested to react on user selection, we can bind it with `menu.on(...)`. - -And the `eventMixin` can add such behavior to as many classes as we'd like, without interfering with the inheritance chain. - -## Summary - -*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes. - -Some other languages like e.g. python allow to create mixins using multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying them into the prototype. - -We can use mixins as a way to augment a class by multiple behaviors, like event-handling as we have seen above. - -Mixins may become a point of conflict if they occasionally overwrite native class methods. So generally one should think well about the naming for a mixin, to minimize such possibility. diff --git a/1-js/07-object-oriented-programming/13-mixins/head.html b/1-js/07-object-oriented-programming/13-mixins/head.html deleted file mode 100644 index 77ea38b204..0000000000 --- a/1-js/07-object-oriented-programming/13-mixins/head.html +++ /dev/null @@ -1,43 +0,0 @@ - diff --git a/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance.png b/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance.png deleted file mode 100644 index 7cc655036f..0000000000 Binary files a/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance@2x.png b/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance@2x.png deleted file mode 100644 index f53ecf68fd..0000000000 Binary files a/1-js/07-object-oriented-programming/13-mixins/mixin-inheritance@2x.png and /dev/null differ diff --git a/1-js/07-object-oriented-programming/index.md b/1-js/07-object-oriented-programming/index.md deleted file mode 100644 index 7053ada816..0000000000 --- a/1-js/07-object-oriented-programming/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# Objects, classes, inheritance - -In this section we return to objects and learn them even more in-depth. diff --git a/1-js/07-object-properties/01-property-descriptors/article.md b/1-js/07-object-properties/01-property-descriptors/article.md new file mode 100644 index 0000000000..0a945b377b --- /dev/null +++ b/1-js/07-object-properties/01-property-descriptors/article.md @@ -0,0 +1,349 @@ + +# Property flags and descriptors + +As we know, objects can store properties. + +Until now, a property was a simple "key-value" pair to us. But an object property is actually a more flexible and powerful thing. + +In this chapter we'll study additional configuration options, and in the next we'll see how to invisibly turn them into getter/setter functions. + +## Property flags + +Object properties, besides a **`value`**, have three special attributes (so-called "flags"): + +- **`writable`** -- if `true`, the value can be changed, otherwise it's read-only. +- **`enumerable`** -- if `true`, then listed in loops, otherwise not listed. +- **`configurable`** -- if `true`, the property can be deleted and these attributes can be modified, otherwise not. + +We didn't see them yet, because generally they do not show up. When we create a property "the usual way", all of them are `true`. But we also can change them anytime. + +First, let's see how to get those flags. + +The method [Object.getOwnPropertyDescriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor) allows to query the *full* information about a property. + +The syntax is: +```js +let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName); +``` + +`obj` +: The object to get information from. + +`propertyName` +: The name of the property. + +The returned value is a so-called "property descriptor" object: it contains the value and all the flags. + +For instance: + +```js run +let user = { + name: "John" +}; + +let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); + +alert( JSON.stringify(descriptor, null, 2 ) ); +/* property descriptor: +{ + "value": "John", + "writable": true, + "enumerable": true, + "configurable": true +} +*/ +``` + +To change the flags, we can use [Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). + +The syntax is: + +```js +Object.defineProperty(obj, propertyName, descriptor) +``` + +`obj`, `propertyName` +: The object and its property to apply the descriptor. + +`descriptor` +: Property descriptor object to apply. + +If the property exists, `defineProperty` updates its flags. Otherwise, it creates the property with the given value and flags; in that case, if a flag is not supplied, it is assumed `false`. + +For instance, here a property `name` is created with all falsy flags: + +```js run +let user = {}; + +*!* +Object.defineProperty(user, "name", { + value: "John" +}); +*/!* + +let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); + +alert( JSON.stringify(descriptor, null, 2 ) ); +/* +{ + "value": "John", +*!* + "writable": false, + "enumerable": false, + "configurable": false +*/!* +} + */ +``` + +Compare it with "normally created" `user.name` above: now all flags are falsy. If that's not what we want then we'd better set them to `true` in `descriptor`. + +Now let's see effects of the flags by example. + +## Non-writable + +Let's make `user.name` non-writable (can't be reassigned) by changing `writable` flag: + +```js run +let user = { + name: "John" +}; + +Object.defineProperty(user, "name", { +*!* + writable: false +*/!* +}); + +*!* +user.name = "Pete"; // Error: Cannot assign to read only property 'name' +*/!* +``` + +Now no one can change the name of our user, unless they apply their own `defineProperty` to override ours. + +```smart header="Errors appear only in strict mode" +In non-strict mode, no errors occur when writing to non-writable properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict. +``` + +Here's the same example, but the property is created from scratch: + +```js run +let user = { }; + +Object.defineProperty(user, "name", { +*!* + value: "John", + // for new properties we need to explicitly list what's true + enumerable: true, + configurable: true +*/!* +}); + +alert(user.name); // John +user.name = "Pete"; // Error +``` + +## Non-enumerable + +Now let's add a custom `toString` to `user`. + +Normally, a built-in `toString` for objects is non-enumerable, it does not show up in `for..in`. But if we add a `toString` of our own, then by default it shows up in `for..in`, like this: + +```js run +let user = { + name: "John", + toString() { + return this.name; + } +}; + +// By default, both our properties are listed: +for (let key in user) alert(key); // name, toString +``` + +If we don't like it, then we can set `enumerable:false`. Then it won't appear in a `for..in` loop, just like the built-in one: + +```js run +let user = { + name: "John", + toString() { + return this.name; + } +}; + +Object.defineProperty(user, "toString", { +*!* + enumerable: false +*/!* +}); + +*!* +// Now our toString disappears: +*/!* +for (let key in user) alert(key); // name +``` + +Non-enumerable properties are also excluded from `Object.keys`: + +```js +alert(Object.keys(user)); // name +``` + +## Non-configurable + +The non-configurable flag (`configurable:false`) is sometimes preset for built-in objects and properties. + +A non-configurable property can't be deleted, its attributes can't be modified. + +For instance, `Math.PI` is non-writable, non-enumerable and non-configurable: + +```js run +let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI'); + +alert( JSON.stringify(descriptor, null, 2 ) ); +/* +{ + "value": 3.141592653589793, + "writable": false, + "enumerable": false, + "configurable": false +} +*/ +``` +So, a programmer is unable to change the value of `Math.PI` or overwrite it. + +```js run +Math.PI = 3; // Error, because it has writable: false + +// delete Math.PI won't work either +``` + +We also can't change `Math.PI` to be `writable` again: + +```js run +// Error, because of configurable: false +Object.defineProperty(Math, "PI", { writable: true }); +``` + +There's absolutely nothing we can do with `Math.PI`. + +Making a property non-configurable is a one-way road. We cannot change it back with `defineProperty`. + +**Please note: `configurable: false` prevents changes of property flags and its deletion, while allowing to change its value.** + +Here `user.name` is non-configurable, but we can still change it (as it's writable): + +```js run +let user = { + name: "John" +}; + +Object.defineProperty(user, "name", { + configurable: false +}); + +user.name = "Pete"; // works fine +delete user.name; // Error +``` + +And here we make `user.name` a "forever sealed" constant, just like the built-in `Math.PI`: + +```js run +let user = { + name: "John" +}; + +Object.defineProperty(user, "name", { + writable: false, + configurable: false +}); + +// won't be able to change user.name or its flags +// all this won't work: +user.name = "Pete"; +delete user.name; +Object.defineProperty(user, "name", { value: "Pete" }); +``` + +```smart header="The only attribute change possible: writable true -> false" +There's a minor exception about changing flags. + +We can change `writable: true` to `false` for a non-configurable property, thus preventing its value modification (to add another layer of protection). Not the other way around though. +``` + +## Object.defineProperties + +There's a method [Object.defineProperties(obj, descriptors)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties) that allows to define many properties at once. + +The syntax is: + +```js +Object.defineProperties(obj, { + prop1: descriptor1, + prop2: descriptor2 + // ... +}); +``` + +For instance: + +```js +Object.defineProperties(user, { + name: { value: "John", writable: false }, + surname: { value: "Smith", writable: false }, + // ... +}); +``` + +So, we can set many properties at once. + +## Object.getOwnPropertyDescriptors + +To get all property descriptors at once, we can use the method [Object.getOwnPropertyDescriptors(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors). + +Together with `Object.defineProperties` it can be used as a "flags-aware" way of cloning an object: + +```js +let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj)); +``` + +Normally when we clone an object, we use an assignment to copy properties, like this: + +```js +for (let key in user) { + clone[key] = user[key] +} +``` + +...But that does not copy flags. So if we want a "better" clone then `Object.defineProperties` is preferred. + +Another difference is that `for..in` ignores symbolic and non-enumerable properties, but `Object.getOwnPropertyDescriptors` returns *all* property descriptors including symbolic and non-enumerable ones. + +## Sealing an object globally + +Property descriptors work at the level of individual properties. + +There are also methods that limit access to the *whole* object: + +[Object.preventExtensions(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions) +: Forbids the addition of new properties to the object. + +[Object.seal(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) +: Forbids adding/removing of properties. Sets `configurable: false` for all existing properties. + +[Object.freeze(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) +: Forbids adding/removing/changing of properties. Sets `configurable: false, writable: false` for all existing properties. + +And also there are tests for them: + +[Object.isExtensible(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible) +: Returns `false` if adding properties is forbidden, otherwise `true`. + +[Object.isSealed(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed) +: Returns `true` if adding/removing properties is forbidden, and all existing properties have `configurable: false`. + +[Object.isFrozen(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen) +: Returns `true` if adding/removing/changing properties is forbidden, and all current properties are `configurable: false, writable: false`. + +These methods are rarely used in practice. diff --git a/1-js/07-object-properties/02-property-accessors/article.md b/1-js/07-object-properties/02-property-accessors/article.md new file mode 100644 index 0000000000..c2aa35d53a --- /dev/null +++ b/1-js/07-object-properties/02-property-accessors/article.md @@ -0,0 +1,244 @@ + +# Property getters and setters + +There are two kinds of object properties. + +The first kind is *data properties*. We already know how to work with them. All properties that we've been using until now were data properties. + +The second type of property is something new. It's an *accessor property*. They are essentially functions that execute on getting and setting a value, but look like regular properties to an external code. + +## Getters and setters + +Accessor properties are represented by "getter" and "setter" methods. In an object literal they are denoted by `get` and `set`: + +```js +let obj = { + *!*get propName()*/!* { + // getter, the code executed on getting obj.propName + }, + + *!*set propName(value)*/!* { + // setter, the code executed on setting obj.propName = value + } +}; +``` + +The getter works when `obj.propName` is read, the setter -- when it is assigned. + +For instance, we have a `user` object with `name` and `surname`: + +```js +let user = { + name: "John", + surname: "Smith" +}; +``` + +Now we want to add a `fullName` property, that should be `"John Smith"`. Of course, we don't want to copy-paste existing information, so we can implement it as an accessor: + +```js run +let user = { + name: "John", + surname: "Smith", + +*!* + get fullName() { + return `${this.name} ${this.surname}`; + } +*/!* +}; + +*!* +alert(user.fullName); // John Smith +*/!* +``` + +From the outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't *call* `user.fullName` as a function, we *read* it normally: the getter runs behind the scenes. + +As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error: + +```js run +let user = { + get fullName() { + return `...`; + } +}; + +*!* +user.fullName = "Test"; // Error (property has only a getter) +*/!* +``` + +Let's fix it by adding a setter for `user.fullName`: + +```js run +let user = { + name: "John", + surname: "Smith", + + get fullName() { + return `${this.name} ${this.surname}`; + }, + +*!* + set fullName(value) { + [this.name, this.surname] = value.split(" "); + } +*/!* +}; + +// set fullName is executed with the given value. +user.fullName = "Alice Cooper"; + +alert(user.name); // Alice +alert(user.surname); // Cooper +``` + +As the result, we have a "virtual" property `fullName`. It is readable and writable. + +## Accessor descriptors + +Descriptors for accessor properties are different from those for data properties. + +For accessor properties, there is no `value` or `writable`, but instead there are `get` and `set` functions. + +That is, an accessor descriptor may have: + +- **`get`** -- a function without arguments, that works when a property is read, +- **`set`** -- a function with one argument, that is called when the property is set, +- **`enumerable`** -- same as for data properties, +- **`configurable`** -- same as for data properties. + +For instance, to create an accessor `fullName` with `defineProperty`, we can pass a descriptor with `get` and `set`: + +```js run +let user = { + name: "John", + surname: "Smith" +}; + +*!* +Object.defineProperty(user, 'fullName', { + get() { + return `${this.name} ${this.surname}`; + }, + + set(value) { + [this.name, this.surname] = value.split(" "); + } +*/!* +}); + +alert(user.fullName); // John Smith + +for(let key in user) alert(key); // name, surname +``` + +Please note that a property can be either an accessor (has `get/set` methods) or a data property (has a `value`), not both. + +If we try to supply both `get` and `value` in the same descriptor, there will be an error: + +```js run +*!* +// Error: Invalid property descriptor. +*/!* +Object.defineProperty({}, 'prop', { + get() { + return 1 + }, + + value: 2 +}); +``` + +## Smarter getters/setters + +Getters/setters can be used as wrappers over "real" property values to gain more control over operations with them. + +For instance, if we want to forbid too short names for `user`, we can have a setter `name` and keep the value in a separate property `_name`: + +```js run +let user = { + get name() { + return this._name; + }, + + set name(value) { + if (value.length < 4) { + alert("Name is too short, need at least 4 characters"); + return; + } + this._name = value; + } +}; + +user.name = "Pete"; +alert(user.name); // Pete + +user.name = ""; // Name is too short... +``` + +So, the name is stored in `_name` property, and the access is done via getter and setter. + +Technically, external code is able to access the name directly by using `user._name`. But there is a widely known convention that properties starting with an underscore `"_"` are internal and should not be touched from outside the object. + + +## Using for compatibility + +One of the great uses of accessors is that they allow to take control over a "regular" data property at any moment by replacing it with a getter and a setter and tweak its behavior. + +Imagine we started implementing user objects using data properties `name` and `age`: + +```js +function User(name, age) { + this.name = name; + this.age = age; +} + +let john = new User("John", 25); + +alert( john.age ); // 25 +``` + +...But sooner or later, things may change. Instead of `age` we may decide to store `birthday`, because it's more precise and convenient: + +```js +function User(name, birthday) { + this.name = name; + this.birthday = birthday; +} + +let john = new User("John", new Date(1992, 6, 1)); +``` + +Now what to do with the old code that still uses `age` property? + +We can try to find all such places and fix them, but that takes time and can be hard to do if that code is used by many other people. And besides, `age` is a nice thing to have in `user`, right? + +Let's keep it. + +Adding a getter for `age` solves the problem: + +```js run no-beautify +function User(name, birthday) { + this.name = name; + this.birthday = birthday; + +*!* + // age is calculated from the current date and birthday + Object.defineProperty(this, "age", { + get() { + let todayYear = new Date().getFullYear(); + return todayYear - this.birthday.getFullYear(); + } + }); +*/!* +} + +let john = new User("John", new Date(1992, 6, 1)); + +alert( john.birthday ); // birthday is available +alert( john.age ); // ...as well as the age +``` + +Now the old code works too and we've got a nice additional property. diff --git a/1-js/07-object-properties/index.md b/1-js/07-object-properties/index.md new file mode 100644 index 0000000000..67fcccaffc --- /dev/null +++ b/1-js/07-object-properties/index.md @@ -0,0 +1,3 @@ +# Object properties configuration + +In this section we return to objects and study their properties even more in-depth. diff --git a/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/solution.md b/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/solution.md deleted file mode 100644 index 05ba72e008..0000000000 --- a/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/solution.md +++ /dev/null @@ -1,47 +0,0 @@ -The difference becomes obvious when we look at the code inside a function. - -The behavior is different if there's a "jump out" of `try..catch`. - -For instance, when there's a `return` inside `try..catch`. The `finally` clause works in case of *any* exit from `try..catch`, even via the `return` statement: right after `try..catch` is done, but before the calling code gets the control. - -```js run -function f() { - try { - alert('start'); -*!* - return "result"; -*/!* - } catch (e) { - /// ... - } finally { - alert('cleanup!'); - } -} - -f(); // cleanup! -``` - -...Or when there's a `throw`, like here: - -```js run -function f() { - try { - alert('start'); - throw new Error("an error"); - } catch (e) { - // ... - if("can't handle the error") { -*!* - throw e; -*/!* - } - - } finally { - alert('cleanup!') - } -} - -f(); // cleanup! -``` - -It's `finally` that guarantees the cleanup here. If we just put the code at the end of `f`, it wouldn't run. diff --git a/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/task.md b/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/task.md deleted file mode 100644 index e846873438..0000000000 --- a/1-js/08-error-handling/1-try-catch/1-finally-or-code-after/task.md +++ /dev/null @@ -1,38 +0,0 @@ -importance: 5 - ---- - -# Finally or just the code? - -Compare the two code fragments. - -1. The first one uses `finally` to execute the code after `try..catch`: - - ```js - try { - work work - } catch (e) { - handle errors - } finally { - *!* - cleanup the working space - */!* - } - ``` -2. The second fragment puts the cleaning right after `try..catch`: - - ```js - try { - work work - } catch (e) { - handle errors - } - - *!* - cleanup the working space - */!* - ``` - -We definitely need the cleanup after the work has started, doesn't matter if there was an error or not. - -Is there an advantage here in using `finally` or both code fragments are equal? If there is such an advantage, then give an example when it matters. diff --git a/1-js/08-error-handling/1-try-catch/article.md b/1-js/08-error-handling/1-try-catch/article.md deleted file mode 100644 index eabfb7423f..0000000000 --- a/1-js/08-error-handling/1-try-catch/article.md +++ /dev/null @@ -1,660 +0,0 @@ -# Error handling, "try..catch" - -No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response and for a thousand of other reasons. - -Usually, a script "dies" (immediately stops) in case of an error, printing it to console. - -But there's a syntax construct `try..catch` that allows to "catch" errors and, instead of dying, do something more reasonable. - -## The "try..catch" syntax - -The `try..catch` construct has two main blocks: `try`, and then `catch`: - -```js -try { - - // code... - -} catch (err) { - - // error handling - -} -``` - -It works like this: - -1. First, the code in `try {...}` is executed. -2. If there were no errors, then `catch(err)` is ignored: the execution reaches the end of `try` and then jumps over `catch`. -3. If an error occurs, then `try` execution is stopped, and the control flows to the beginning of `catch(err)`. The `err` variable (can use any name for it) contains an error object with details about what's happened. - -![](try-catch-flow.png) - -So, an error inside the `try {…}` block does not kill the script: we have a chance to handle it in `catch`. - -Let's see more examples. - -- An errorless example: shows `alert` `(1)` and `(2)`: - - ```js run - try { - - alert('Start of try runs'); // *!*(1) <--*/!* - - // ...no errors here - - alert('End of try runs'); // *!*(2) <--*/!* - - } catch(err) { - - alert('Catch is ignored, because there are no errors'); // (3) - - } - - alert("...Then the execution continues"); - ``` -- An example with an error: shows `(1)` and `(3)`: - - ```js run - try { - - alert('Start of try runs'); // *!*(1) <--*/!* - - *!* - lalala; // error, variable is not defined! - */!* - - alert('End of try (never reached)'); // (2) - - } catch(err) { - - alert(`Error has occured!`); // *!*(3) <--*/!* - - } - - alert("...Then the execution continues"); - ``` - - -````warn header="`try..catch` only works for runtime errors" -For `try..catch` to work, the code must be runnable. In other words, it should be valid JavaScript. - -It won't work if the code is syntactically wrong, for instance it has unmatched curly braces: - -```js run -try { - {{{{{{{{{{{{ -} catch(e) { - alert("The engine can't understand this code, it's invalid"); -} -``` - -The JavaScript engine first reads the code, and then runs it. The errors that occur on the reading phrase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code. - -So, `try..catch` can only handle errors that occur in the valid code. Such errors are called "runtime errors" or, sometimes, "exceptions". -```` - - -````warn header="`try..catch` works synchronously" -If an exception happens in "scheduled" code, like in `setTimeout`, then `try..catch` won't catch it: - -```js run -try { - setTimeout(function() { - noSuchVariable; // script will die here - }, 1000); -} catch (e) { - alert( "won't work" ); -} -``` - -That's because `try..catch` actually wraps the `setTimeout` call that schedules the function. But the function itself is executed later, when the engine has already left the `try..catch` construct. - -To catch an exception inside a scheduled function, `try..catch` must be inside that function: -```js run -setTimeout(function() { - try { - noSuchVariable; // try..catch handles the error! - } catch (e) { - alert( "error is caught here!" ); - } -}, 1000); -``` -```` - -## Error object - -When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to `catch`: - -```js -try { - // ... -} catch(err) { // <-- the "error object", could use another word instead of err - // ... -} -``` - -For all built-in errors, the error object inside `catch` block has two main properties: - -`name` -: Error name. For an undefined variable that's `"ReferenceError"`. - -`message` -: Textual message about error details. - -There are other non-standard properties available in most environments. One of most widely used and supported is: - -`stack` -: Current call stack: a string with information about the sequence of nested calls that led to the error. Used for debugging purposes. - -For instance: - -```js run untrusted -try { -*!* - lalala; // error, variable is not defined! -*/!* -} catch(err) { - alert(err.name); // ReferenceError - alert(err.message); // lalala is not defined - alert(err.stack); // ReferenceError: lalala is not defined at ... - - // Can also show an error as a whole - // The error is converted to string as "name: message" - alert(err); // ReferenceError: lalala is not defined -} -``` - - -## Using "try..catch" - -Let's explore a real-life use case of `try..catch`. - -As we already know, JavaScript supports the [JSON.parse(str)](mdn:js/JSON/parse) method to read JSON-encoded values. - -Usually it's used to decode data received over the network, from the server or another source. - -We receive it and call `JSON.parse`, like this: - -```js run -let json = '{"name":"John", "age": 30}'; // data from the server - -*!* -let user = JSON.parse(json); // convert the text representation to JS object -*/!* - -// now user is an object with properties from the string -alert( user.name ); // John -alert( user.age ); // 30 -``` - -You can find more detailed information about JSON in the chapter. - -**If `json` is malformed, `JSON.parse` generates an error, so the script "dies".** - -Should we be satisfied with that? Of course, not! - -This way, if something's wrong with the data, the visitor will never know that (unless they open the developer console). And people really don't like when something "just dies" without any error message. - -Let's use `try..catch` to handle the error: - -```js run -let json = "{ bad json }"; - -try { - -*!* - let user = JSON.parse(json); // <-- when an error occurs... -*/!* - alert( user.name ); // doesn't work - -} catch (e) { -*!* - // ...the execution jumps here - alert( "Our apologies, the data has errors, we'll try to request it one more time." ); - alert( e.name ); - alert( e.message ); -*/!* -} -``` - -Here we use the `catch` block only to show the message, but we can do much more: send a new network request, suggest an alternative to the visitor, send information about the error to a logging facility, ... . All much better than just dying. - -## Throwing our own errors - -What if `json` is syntactically correct, but doesn't have a required `name` property? - -Like this: - -```js run -let json = '{ "age": 30 }'; // incomplete data - -try { - - let user = JSON.parse(json); // <-- no errors -*!* - alert( user.name ); // no name! -*/!* - -} catch (e) { - alert( "doesn't execute" ); -} -``` - -Here `JSON.parse` runs normally, but the absence of `name` is actually an error for us. - -To unify error handling, we'll use the `throw` operator. - -### "Throw" operator - -The `throw` operator generates an error. - -The syntax is: - -```js -throw -``` - -Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it's better to use objects, preferrably with `name` and `message` properties (to stay somewhat compatible with built-in errors). - -JavaScript has many built-in constructors for standard errors: `Error`, `SyntaxError`, `ReferenceError`, `TypeError` and others. We can use them to create error objects as well. - -Their syntax is: - -```js -let error = new Error(message); -// or -let error = new SyntaxError(message); -let error = new ReferenceError(message); -// ... -``` - -For built-in errors (not for any objects, just for errors), the `name` property is exactly the name of the constructor. And `message` is taken from the argument. - -For instance: - -```js run -let error = new Error("Things happen o_O"); - -alert(error.name); // Error -alert(error.message); // Things happen o_O -``` - -Let's see what kind of error `JSON.parse` generates: - -```js run -try { - JSON.parse("{ bad json o_O }"); -} catch(e) { -*!* - alert(e.name); // SyntaxError -*/!* - alert(e.message); // Unexpected token o in JSON at position 0 -} -``` - -As we can see, that's a `SyntaxError`. - -And in our case, the absence of `name` could be treated as a syntax error also, assuming that users must have a `name`. - -So let's throw it: - -```js run -let json = '{ "age": 30 }'; // incomplete data - -try { - - let user = JSON.parse(json); // <-- no errors - - if (!user.name) { -*!* - throw new SyntaxError("Incomplete data: no name"); // (*) -*/!* - } - - alert( user.name ); - -} catch(e) { - alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name -} -``` - -In the line `(*)`, the `throw` operator generates a `SyntaxError` with the given `message`, the same way as JavaScript would generate it itself. The execution of `try` immediately stops and the control flow jumps into `catch`. - -Now `catch` became a single place for all error handling: both for `JSON.parse` and other cases. - -## Rethrowing - -In the example above we use `try..catch` to handle incorrect data. But is it possible that *another unexpected error* occurs within the `try {...}` block? Like a variable is undefined or something else, not just that "incorrect data" thing. - -Like this: - -```js run -let json = '{ "age": 30 }'; // incomplete data - -try { - user = JSON.parse(json); // <-- forgot to put "let" before user - - // ... -} catch(err) { - alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined - // (no JSON Error actually) -} -``` - -Of course, everything's possible! Programmers do make mistakes. Even in open-source utilities used by millions for decades -- suddenly a crazy bug may be discovered that leads to terrible hacks (like it happened with the `ssh` tool). - -In our case, `try..catch` is meant to catch "incorrect data" errors. But by its nature, `catch` gets *all* errors from `try`. Here it gets an unexpected error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug. - -Fortunately, we can find out which error we get, for instance from its `name`: - -```js run -try { - user = { /*...*/ }; -} catch(e) { -*!* - alert(e.name); // "ReferenceError" for accessing an undefined variable -*/!* -} -``` - -The rule is simple: - -**Catch should only process errors that it knows and "rethrow" all others.** - -The "rethrowing" technique can be explained in more detail as: - -1. Catch gets all errors. -2. In `catch(err) {...}` block we analyze the error object `err`. -2. If we don't know how to handle it, then we do `throw err`. - -In the code below, we use rethrowing so that `catch` only handles `SyntaxError`: - -```js run -let json = '{ "age": 30 }'; // incomplete data -try { - - let user = JSON.parse(json); - - if (!user.name) { - throw new SyntaxError("Incomplete data: no name"); - } - -*!* - blabla(); // unexpected error -*/!* - - alert( user.name ); - -} catch(e) { - -*!* - if (e.name == "SyntaxError") { - alert( "JSON Error: " + e.message ); - } else { - throw e; // rethrow (*) - } -*/!* - -} -``` - -The error throwing on line `(*)` from inside `catch` block "falls out" of `try..catch` and can be either caught by an outer `try..catch` construct (if it exists), or it kills the script. - -So the `catch` block actually handles only errors that it knows how to deal with and "skips" all others. - -The example below demonstrates how such errors can be caught by one more level of `try..catch`: - -```js run -function readData() { - let json = '{ "age": 30 }'; - - try { - // ... -*!* - blabla(); // error! -*/!* - } catch (e) { - // ... - if (e.name != 'SyntaxError') { -*!* - throw e; // rethrow (don't know how to deal with it) -*/!* - } - } -} - -try { - readData(); -} catch (e) { -*!* - alert( "External catch got: " + e ); // caught it! -*/!* -} -``` - -Here `readData` only knows how to handle `SyntaxError`, while the outer `try..catch` knows how to handle everything. - -## try..catch..finally - -Wait, that's not all. - -The `try..catch` construct may have one more code clause: `finally`. - -If it exists, it runs in all cases: - -- after `try`, if there were no errors, -- after `catch`, if there were errors. - -The extended syntax looks like this: - -```js -*!*try*/!* { - ... try to execute the code ... -} *!*catch*/!*(e) { - ... handle errors ... -} *!*finally*/!* { - ... execute always ... -} -``` - -Try running this code: - -```js run -try { - alert( 'try' ); - if (confirm('Make an error?')) BAD_CODE(); -} catch (e) { - alert( 'catch' ); -} finally { - alert( 'finally' ); -} -``` - -The code has two ways of execution: - -1. If you answer "Yes" to "Make an error?", then `try -> catch -> finally`. -2. If you say "No", then `try -> finally`. - -The `finally` clause is often used when we start doing something before `try..catch` and want to finalize it in any case of outcome. - -For instance, we want to measure the time that a Fibonacci numbers function `fib(n)` takes. Naturally, we can start measuring before it runs and finish afterwards. But what if there's an error during the function call? In particular, the implementation of `fib(n)` in the code below returns an error for negative or non-integer numbers. - -The `finally` clause is a great place to finish the measurements no matter what. - -Here `finally` guarantees that the time will be measured correctly in both situations -- in case of a successful execution of `fib` and in case of an error in it: - -```js run -let num = +prompt("Enter a positive integer number?", 35) - -let diff, result; - -function fib(n) { - if (n < 0 || Math.trunc(n) != n) { - throw new Error("Must not be negative, and also an integer."); - } - return n <= 1 ? n : fib(n - 1) + fib(n - 2); -} - -let start = Date.now(); - -try { - result = fib(num); -} catch (e) { - result = 0; -*!* -} finally { - diff = Date.now() - start; -} -*/!* - -alert(result || "error occured"); - -alert( `execution took ${diff}ms` ); -``` - -You can check by running the code with entering `35` into `prompt` -- it executes normally, `finally` after `try`. And then enter `-1` -- there will be an immediate error, an the execution will take `0ms`. Both measurements are done correctly. - -In other words, there may be two ways to exit a function: either a `return` or `throw`. The `finally` clause handles them both. - - -```smart header="Variables are local inside `try..catch..finally`" -Please note that `result` and `diff` variables in the code above are declared *before* `try..catch`. - -Otherwise, if `let` were made inside the `{...}` block, it would only be visible inside of it. -``` - -````smart header="`finally` and `return`" -The `finally` clause works for *any* exit from `try..catch`. That includes an explicit `return`. - -In the example below, there's a `return` in `try`. In this case, `finally` is executed just before the control returns to the outer code. - -```js run -function func() { - - try { -*!* - return 1; -*/!* - - } catch (e) { - /* ... */ - } finally { -*!* - alert( 'finally' ); -*/!* - } -} - -alert( func() ); // first works alert from finally, and then this one -``` -```` - -````smart header="`try..finally`" - -The `try..finally` construct, without `catch` clause, is also useful. We apply it when we don't want to handle errors right here, but want to be sure that processes that we started are finalized. - -```js -function func() { - // start doing something that needs completion (like measurements) - try { - // ... - } finally { - // complete that thing even if all dies - } -} -``` -In the code above, an error inside `try` always falls out, because there's no `catch`. But `finally` works before the execution flow jumps outside. -```` - -## Global catch - -```warn header="Environment-specific" -The information from this section is not a part of the core JavaScript. -``` - -Let's imagine we've got a fatal error outside of `try..catch`, and the script died. Like a programming error or something else terrible. - -Is there a way to react on such occurrences? We may want to log the error, show something to the user (normally they don't see error messages) etc. - -There is none in the specification, but environments usually provide it, because it's really useful. For instance, Node.JS has [process.on('uncaughtException')](https://nodejs.org/api/process.html#process_event_uncaughtexception) for that. And in the browser we can assign a function to special [window.onerror](mdn:api/GlobalEventHandlers/onerror) property. It will run in case of an uncaught error. - -The syntax: - -```js -window.onerror = function(message, url, line, col, error) { - // ... -}; -``` - -`message` -: Error message. - -`url` -: URL of the script where error happened. - -`line`, `col` -: Line and column numbers where error happened. - -`error` -: Error object. - -For instance: - -```html run untrusted refresh height=1 - -``` - -The role of the global handler `window.onerror` is usually not to recover the script execution -- that's probably impossible in case of programming errors, but to send the error message to developers. - -There are also web-services that provide error-logging for such cases, like or . - -They work like this: - -1. We register at the service and get a piece of JS (or a script URL) from them to insert on pages. -2. That JS script has a custom `window.onerror` function. -3. When an error occurs, it sends a network request about it to the service. -4. We can log in to the service web interface and see errors. - -## Summary - -The `try..catch` construct allows to handle runtime errors. It literally allows to try running the code and catch errors that may occur in it. - -The syntax is: - -```js -try { - // run this code -} catch(err) { - // if an error happened, then jump here - // err is the error object -} finally { - // do in any case after try/catch -} -``` - -There may be no `catch` section or no `finally`, so `try..catch` and `try..finally` are also valid. - -Error objects have following properties: - -- `message` -- the human-readable error message. -- `name` -- the string with error name (error constructor name). -- `stack` (non-standard) -- the stack at the moment of error creation. - -We can also generate our own errors using the `throw` operator. Technically, the argument of `throw` can be anything, but usually it's an error object inheriting from the built-in `Error` class. More on extending errors in the next chapter. - -Rethrowing is a basic pattern of error handling: a `catch` block usually expects and knows how to handle the particular error type, so it should rethrow errors it doesn't know. - -Even if we don't have `try..catch`, most environments allow to setup a "global" error handler to catch errors that "fall out". In-browser that's `window.onerror`. diff --git a/1-js/08-error-handling/1-try-catch/try-catch-flow.png b/1-js/08-error-handling/1-try-catch/try-catch-flow.png deleted file mode 100644 index 6a91b63295..0000000000 Binary files a/1-js/08-error-handling/1-try-catch/try-catch-flow.png and /dev/null differ diff --git a/1-js/08-error-handling/1-try-catch/try-catch-flow@2x.png b/1-js/08-error-handling/1-try-catch/try-catch-flow@2x.png deleted file mode 100644 index 8bf9680fd6..0000000000 Binary files a/1-js/08-error-handling/1-try-catch/try-catch-flow@2x.png and /dev/null differ diff --git a/1-js/08-error-handling/2-custom-errors/1-format-error/solution.md b/1-js/08-error-handling/2-custom-errors/1-format-error/solution.md deleted file mode 100644 index bb6b74cfaf..0000000000 --- a/1-js/08-error-handling/2-custom-errors/1-format-error/solution.md +++ /dev/null @@ -1,16 +0,0 @@ -```js run untrusted -class FormatError extends SyntaxError { - constructor(message) { - super(message); - this.name = "FormatError"; - } -} - -let err = new FormatError("formatting error"); - -alert( err.message ); // formatting error -alert( err.name ); // FormatError -alert( err.stack ); // stack - -alert( err instanceof SyntaxError ); // true -``` diff --git a/1-js/08-error-handling/2-custom-errors/article.md b/1-js/08-error-handling/2-custom-errors/article.md deleted file mode 100644 index d7d61166dd..0000000000 --- a/1-js/08-error-handling/2-custom-errors/article.md +++ /dev/null @@ -1,307 +0,0 @@ -# Custom errors, extending Error - -When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need `HttpError`, for database operations `DbError`, for searching operations `NotFoundError` and so on. - -Our errors should support basic error properties like `message`, `name` and, preferably, `stack`. But they also may have other properties of their own, e.g. `HttpError` objects may have `statusCode` property with a value like `404` or `403` or `500`. - -JavaScript allows to use `throw` with any argument, so technically our custom error classes don't need to inherit from `Error`. But if we inherit, then it becomes possible to use `obj instanceof Error` to identify error objects. So it's better to inherit from it. - -As we build our application, our own errors naturally form a hierarchy, for instance `HttpTimeoutError` may inherit from `HttpError`, and so on. - -## Extending Error - -As an example, let's consider a function `readUser(json)` that should read JSON with user data. - -Here's an example of how a valid `json` may look: -```js -let json = `{ "name": "John", "age": 30 }`; -``` - -Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it throws `SyntaxError`. - -But even if `json` is syntactically correct, that doesn't mean that it's a valid user, right? It may miss the necessary data. For instance, if may not have `name` and `age` properties that are essential for our users. - -Our function `readUser(json)` will not only read JSON, but check ("validate") the data. If there are no required fields, or the format is wrong, then that's an error. And that's not a `SyntaxError`, because the data is syntactically correct, but another kind of error. We'll call it `ValidationError` and create a class for it. An error of that kind should also carry the information about the offending field. - -Our `ValidationError` class should inherit from the built-in `Error` class. - -That class is built-in, but we should have its approximate code before our eyes, to understand what we're extending. - -So here you are: - -```js -// The "pseudocode" for the built-in Error class defined by JavaScript itself -class Error { - constructor(message) { - this.message = message; - this.name = "Error"; // (different names for different built-in error classes) - this.stack = ; // non-standard, but most environments support it - } -} -``` - -Now let's go on and inherit `ValidationError` from it: - -```js run untrusted -*!* -class ValidationError extends Error { -*/!* - constructor(message) { - super(message); // (1) - this.name = "ValidationError"; // (2) - } -} - -function test() { - throw new ValidationError("Whoops!"); -} - -try { - test(); -} catch(err) { - alert(err.message); // Whoops! - alert(err.name); // ValidationError - alert(err.stack); // a list of nested calls with line numbers for each -} -``` - -Please take a look at the constructor: - -1. In the line `(1)` we call the parent constructor. JavaScript requires us to call `super` in the child constructor, so that's obligatory. The parent constructor sets the `message` property. -2. The parent constructor also sets the `name` property to `"Error"`, so in the line `(2)` we reset it to the right value. - -Let's try to use it in `readUser(json)`: - -```js run -class ValidationError extends Error { - constructor(message) { - super(message); - this.name = "ValidationError"; - } -} - -// Usage -function readUser(json) { - let user = JSON.parse(json); - - if (!user.age) { - throw new ValidationError("No field: age"); - } - if (!user.name) { - throw new ValidationError("No field: name"); - } - - return user; -} - -// Working example with try..catch - -try { - let user = readUser('{ "age": 25 }'); -} catch (err) { - if (err instanceof ValidationError) { -*!* - alert("Invalid data: " + err.message); // Invalid data: No field: name -*/!* - } else if (err instanceof SyntaxError) { // (*) - alert("JSON Syntax Error: " + err.message); - } else { - throw err; // unknown error, rethrow it (**) - } -} -``` - -The `try..catch` block in the code above handles both our `ValidationError` and the built-in `SyntaxError` from `JSON.parse`. - -Please take a look at how we use `instanceof` to check for the specific error type in the line `(*)`. - -We could also look at `err.name`, like this: - -```js -// ... -// instead of (err instanceof SyntaxError) -} else if (err.name == "SyntaxError") { // (*) -// ... -``` - -The `instanceof` version is much better, because in the future we are going to extend `ValidationError`, make subtypes of it, like `PropertyRequiredError`. And `instanceof` check will continue to work for new inheriting classes. So that's future-proof. - -Also it's important that if `catch` meets an unknown error, then it rethrows it in the line `(**)`. The `catch` only knows how to handle validation and syntax errors, other kinds (due to a typo in the code or such) should fall through. - -## Further inheritance - -The `ValidationError` class is very generic. Many things may go wrong. The property may be absent or it may be in a wrong format (like a string value for `age`). Let's make a more concrete class `PropertyRequiredError`, exactly for absent properties. It will carry additional information about the property that's missing. - -```js run -class ValidationError extends Error { - constructor(message) { - super(message); - this.name = "ValidationError"; - } -} - -*!* -class PropertyRequiredError extends ValidationError { - constructor(property) { - super("No property: " + property); - this.name = "PropertyRequiredError"; - this.property = property; - } -} -*/!* - -// Usage -function readUser(json) { - let user = JSON.parse(json); - - if (!user.age) { - throw new PropertyRequiredError("age"); - } - if (!user.name) { - throw new PropertyRequiredError("name"); - } - - return user; -} - -// Working example with try..catch - -try { - let user = readUser('{ "age": 25 }'); -} catch (err) { - if (err instanceof ValidationError) { -*!* - alert("Invalid data: " + err.message); // Invalid data: No property: name - alert(err.name); // PropertyRequiredError - alert(err.property); // name -*/!* - } else if (err instanceof SyntaxError) { - alert("JSON Syntax Error: " + err.message); - } else { - throw err; // unknown error, rethrow it - } -} -``` - -The new class `PropertyRequiredError` is easy to use: we only need to pass the property name: `new PropertyRequiredError(property)`. The human-readable `message` is generated by the constructor. - -Please note that `this.name` in `PropertyRequiredError` constructor is again assigned manually. That may become a bit tedius -- to assign `this.name = ` when creating each custom error. But there's a way out. We can make our own "basic error" class that removes this burden from our shoulders by using `this.constructor.name` for `this.name` in the constructor. And then inherit from it. - -Let's call it `MyError`. - -Here's the code with `MyError` and other custom error classes, simplified: - -```js run -class MyError extends Error { - constructor(message) { - super(message); -*!* - this.name = this.constructor.name; -*/!* - } -} - -class ValidationError extends MyError { } - -class PropertyRequiredError extends ValidationError { - constructor(property) { - super("No property: " + property); - this.property = property; - } -} - -// name is correct -alert( new PropertyRequiredError("field").name ); // PropertyRequiredError -``` - -Now custom errors are much shorter, especially `ValidationError`, as we got rid of the `"this.name = ..."` line in the constructor. - -## Wrapping exceptions - -The purpose of the function `readUser` in the code above is "to read the user data", right? There may occur different kinds of errors in the process. Right now we have `SyntaxError` and `ValidationError`, but in the future `readUser` function may grow: the new code will probably generate other kinds of errors. - -The code which calls `readUser` should handle these errors. Right now it uses multiple `if` in the `catch` block to check for different error types and rethrow the unknown ones. But if `readUser` function generates several kinds of errors -- then we should ask ourselves: do we really want to check for all error types one-by-one in every code that calls `readUser`? - -Often the answer is "No": the outer code wants to be "one level above all that". It wants to have some kind of "data reading error". Why exactly it happened -- is often irrelevant (the error message describes it). Or, even better if there is a way to get error details, but only if we need to. - -So let's make a new class `ReadError` to represent such errors. If an error occurs inside `readUser`, we'll catch it there and generate `ReadError`. We'll also keep the reference to the original error in its `cause` property. Then the outer code will only have to check for `ReadError`. - -Here's the code that defines `ReadError` and demonstrates its use in `readUser` and `try..catch`: - -```js run -class ReadError extends Error { - constructor(message, cause) { - super(message); - this.cause = cause; - this.name = 'ReadError'; - } -} - -class ValidationError extends Error { /*...*/ } -class PropertyRequiredError extends ValidationError { /* ... */ } - -function validateUser(user) { - if (!user.age) { - throw new PropertyRequiredError("age"); - } - - if (!user.name) { - throw new PropertyRequiredError("name"); - } -} - -function readUser(json) { - let user; - - try { - user = JSON.parse(json); - } catch (err) { -*!* - if (err instanceof SyntaxError) { - throw new ReadError("Syntax Error", err); - } else { - throw err; - } -*/!* - } - - try { - validateUser(user); - } catch (err) { -*!* - if (err instanceof ValidationError) { - throw new ReadError("Validation Error", err); - } else { - throw err; - } -*/!* - } - -} - -try { - readUser('{bad json}'); -} catch (e) { - if (e instanceof ReadError) { -*!* - alert(e); - // Original error: SyntaxError: Unexpected token b in JSON at position 1 - alert("Original error: " + e.cause); -*/!* - } else { - throw e; - } -} -``` - -In the code above, `readUser` works exactly as described -- catches syntax and validation errors and throws `ReadError` errors instead (unknown errors are rethrown as usual). - -So the outer code checks `instanceof ReadError` and that's it. No need to list possible all error types. - -The approach is called "wrapping exceptions", because we take "low level exceptions" and "wrap" them into `ReadError` that is more abstract and more convenient to use for the calling code. It is widely used in object-oriented programming. - -## Summary - -- We can inherit from `Error` and other built-in error classes normally, just need to take care of `name` property and don't forget to call `super`. -- Most of the time, we should use `instanceof` to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from the 3rd-party library and there's no easy way to get the class. Then `name` property can be used for such checks. -- Wrapping exceptions is a widespread technique when a function handles low-level exceptions and makes a higher-level object to report about the errors. Low-level exceptions sometimes become properties of that object like `err.cause` in the examples above, but that's not strictly required. diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/solution.md b/1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/solution.md rename to 1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/solution.md diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/task.md b/1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/task.md similarity index 100% rename from 1-js/07-object-oriented-programming/03-prototype-inheritance/1-property-after-delete/task.md rename to 1-js/08-prototypes/01-prototype-inheritance/1-property-after-delete/task.md diff --git a/1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/solution.md b/1-js/08-prototypes/01-prototype-inheritance/2-search-algorithm/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/03-prototype-inheritance/2-search-algorithm/solution.md rename to 1-js/08-prototypes/01-prototype-inheritance/2-search-algorithm/solution.md diff --git a/1-js/08-prototypes/01-prototype-inheritance/2-search-algorithm/task.md b/1-js/08-prototypes/01-prototype-inheritance/2-search-algorithm/task.md new file mode 100644 index 0000000000..bc2db47fed --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/2-search-algorithm/task.md @@ -0,0 +1,31 @@ +importance: 5 + +--- + +# Searching algorithm + +The task has two parts. + +Given the following objects: + +```js +let head = { + glasses: 1 +}; + +let table = { + pen: 3 +}; + +let bed = { + sheet: 1, + pillow: 2 +}; + +let pockets = { + money: 2000 +}; +``` + +1. Use `__proto__` to assign prototypes in a way that any property lookup will follow the path: `pockets` -> `bed` -> `table` -> `head`. For instance, `pockets.pen` should be `3` (found in `table`), and `bed.glasses` should be `1` (found in `head`). +2. Answer the question: is it faster to get `glasses` as `pockets.glasses` or `head.glasses`? Benchmark if needed. diff --git a/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md b/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md new file mode 100644 index 0000000000..4d6ea2653c --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md @@ -0,0 +1,7 @@ +**The answer: `rabbit`.** + +That's because `this` is an object before the dot, so `rabbit.eat()` modifies `rabbit`. + +Property lookup and execution are two different things. + +The method `rabbit.eat` is first found in the prototype, then executed with `this=rabbit`. diff --git a/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/task.md b/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/task.md new file mode 100644 index 0000000000..ed8482c072 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Where does it write? + +We have `rabbit` inheriting from `animal`. + +If we call `rabbit.eat()`, which object receives the `full` property: `animal` or `rabbit`? + +```js +let animal = { + eat() { + this.full = true; + } +}; + +let rabbit = { + __proto__: animal +}; + +rabbit.eat(); +``` diff --git a/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/solution.md b/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/solution.md new file mode 100644 index 0000000000..c141b2ecdc --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/solution.md @@ -0,0 +1,80 @@ +Let's look carefully at what's going on in the call `speedy.eat("apple")`. + +1. The method `speedy.eat` is found in the prototype (`=hamster`), then executed with `this=speedy` (the object before the dot). + +2. Then `this.stomach.push()` needs to find `stomach` property and call `push` on it. It looks for `stomach` in `this` (`=speedy`), but nothing found. + +3. Then it follows the prototype chain and finds `stomach` in `hamster`. + +4. Then it calls `push` on it, adding the food into *the stomach of the prototype*. + +So all hamsters share a single stomach! + +Both for `lazy.stomach.push(...)` and `speedy.stomach.push()`, the property `stomach` is found in the prototype (as it's not in the object itself), then the new data is pushed into it. + +Please note that such thing doesn't happen in case of a simple assignment `this.stomach=`: + +```js run +let hamster = { + stomach: [], + + eat(food) { +*!* + // assign to this.stomach instead of this.stomach.push + this.stomach = [food]; +*/!* + } +}; + +let speedy = { + __proto__: hamster +}; + +let lazy = { + __proto__: hamster +}; + +// Speedy one found the food +speedy.eat("apple"); +alert( speedy.stomach ); // apple + +// Lazy one's stomach is empty +alert( lazy.stomach ); // +``` + +Now all works fine, because `this.stomach=` does not perform a lookup of `stomach`. The value is written directly into `this` object. + +Also we can totally avoid the problem by making sure that each hamster has their own stomach: + +```js run +let hamster = { + stomach: [], + + eat(food) { + this.stomach.push(food); + } +}; + +let speedy = { + __proto__: hamster, +*!* + stomach: [] +*/!* +}; + +let lazy = { + __proto__: hamster, +*!* + stomach: [] +*/!* +}; + +// Speedy one found the food +speedy.eat("apple"); +alert( speedy.stomach ); // apple + +// Lazy one's stomach is empty +alert( lazy.stomach ); // +``` + +As a common solution, all properties that describe the state of a particular object, like `stomach` above, should be written into that object. That prevents such problems. diff --git a/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/task.md b/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/task.md new file mode 100644 index 0000000000..50171123d4 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/task.md @@ -0,0 +1,35 @@ +importance: 5 + +--- + +# Why are both hamsters full? + +We have two hamsters: `speedy` and `lazy` inheriting from the general `hamster` object. + +When we feed one of them, the other one is also full. Why? How can we fix it? + +```js run +let hamster = { + stomach: [], + + eat(food) { + this.stomach.push(food); + } +}; + +let speedy = { + __proto__: hamster +}; + +let lazy = { + __proto__: hamster +}; + +// This one found the food +speedy.eat("apple"); +alert( speedy.stomach ); // apple + +// This one also has it, why? fix please. +alert( lazy.stomach ); // apple +``` + diff --git a/1-js/08-prototypes/01-prototype-inheritance/article.md b/1-js/08-prototypes/01-prototype-inheritance/article.md new file mode 100644 index 0000000000..ef6c7ffebd --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/article.md @@ -0,0 +1,338 @@ +# Prototypal inheritance + +In programming, we often want to take something and extend it. + +For instance, we have a `user` object with its properties and methods, and want to make `admin` and `guest` as slightly modified variants of it. We'd like to reuse what we have in `user`, not copy/reimplement its methods, just build a new object on top of it. + +*Prototypal inheritance* is a language feature that helps in that. + +## [[Prototype]] + +In JavaScript, objects have a special hidden property `[[Prototype]]` (as named in the specification), that is either `null` or references another object. That object is called "a prototype": + +![prototype](object-prototype-empty.svg) + +When we read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, this is called "prototypal inheritance". And soon we'll study many examples of such inheritance, as well as cooler language features built upon it. + +The property `[[Prototype]]` is internal and hidden, but there are many ways to set it. + +One of them is to use the special name `__proto__`, like this: + +```js run +let animal = { + eats: true +}; +let rabbit = { + jumps: true +}; + +*!* +rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal +*/!* +``` + +Now if we read a property from `rabbit`, and it's missing, JavaScript will automatically take it from `animal`. + +For instance: + +```js +let animal = { + eats: true +}; +let rabbit = { + jumps: true +}; + +*!* +rabbit.__proto__ = animal; // (*) +*/!* + +// we can find both properties in rabbit now: +*!* +alert( rabbit.eats ); // true (**) +*/!* +alert( rabbit.jumps ); // true +``` + +Here the line `(*)` sets `animal` to be the prototype of `rabbit`. + +Then, when `alert` tries to read property `rabbit.eats` `(**)`, it's not in `rabbit`, so JavaScript follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up): + +![](proto-animal-rabbit.svg) + +Here we can say that "`animal` is the prototype of `rabbit`" or "`rabbit` prototypically inherits from `animal`". + +So if `animal` has a lot of useful properties and methods, then they become automatically available in `rabbit`. Such properties are called "inherited". + +If we have a method in `animal`, it can be called on `rabbit`: + +```js run +let animal = { + eats: true, +*!* + walk() { + alert("Animal walk"); + } +*/!* +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +// walk is taken from the prototype +*!* +rabbit.walk(); // Animal walk +*/!* +``` + +The method is automatically taken from the prototype, like this: + +![](proto-animal-rabbit-walk.svg) + +The prototype chain can be longer: + +```js run +let animal = { + eats: true, + walk() { + alert("Animal walk"); + } +}; + +let rabbit = { + jumps: true, +*!* + __proto__: animal +*/!* +}; + +let longEar = { + earLength: 10, +*!* + __proto__: rabbit +*/!* +}; + +// walk is taken from the prototype chain +longEar.walk(); // Animal walk +alert(longEar.jumps); // true (from rabbit) +``` + +![](proto-animal-rabbit-chain.svg) + +Now if we read something from `longEar`, and it's missing, JavaScript will look for it in `rabbit`, and then in `animal`. + +There are only two limitations: + +1. The references can't go in circles. JavaScript will throw an error if we try to assign `__proto__` in a circle. +2. The value of `__proto__` can be either an object or `null`. Other types are ignored. + +Also it may be obvious, but still: there can be only one `[[Prototype]]`. An object may not inherit from two others. + +```smart header="`__proto__` is a historical getter/setter for `[[Prototype]]`" +It's a common mistake of novice developers not to know the difference between these two. + +Please note that `__proto__` is *not the same* as the internal `[[Prototype]]` property. It's a getter/setter for `[[Prototype]]`. Later we'll see situations where it matters, for now let's just keep it in mind, as we build our understanding of JavaScript language. + +The `__proto__` property is a bit outdated. It exists for historical reasons, modern JavaScript suggests that we should use `Object.getPrototypeOf/Object.setPrototypeOf` functions instead that get/set the prototype. We'll also cover these functions later. + +By the specification, `__proto__` must only be supported by browsers. In fact though, all environments including server-side support `__proto__`, so we're quite safe using it. + +As the `__proto__` notation is a bit more intuitively obvious, we use it in the examples. +``` + +## Writing doesn't use prototype + +The prototype is only used for reading properties. + +Write/delete operations work directly with the object. + +In the example below, we assign its own `walk` method to `rabbit`: + +```js run +let animal = { + eats: true, + walk() { + /* this method won't be used by rabbit */ + } +}; + +let rabbit = { + __proto__: animal +}; + +*!* +rabbit.walk = function() { + alert("Rabbit! Bounce-bounce!"); +}; +*/!* + +rabbit.walk(); // Rabbit! Bounce-bounce! +``` + +From now on, `rabbit.walk()` call finds the method immediately in the object and executes it, without using the prototype: + +![](proto-animal-rabbit-walk-2.svg) + +Accessor properties are an exception, as assignment is handled by a setter function. So writing to such a property is actually the same as calling a function. + +For that reason `admin.fullName` works correctly in the code below: + +```js run +let user = { + name: "John", + surname: "Smith", + + set fullName(value) { + [this.name, this.surname] = value.split(" "); + }, + + get fullName() { + return `${this.name} ${this.surname}`; + } +}; + +let admin = { + __proto__: user, + isAdmin: true +}; + +alert(admin.fullName); // John Smith (*) + +// setter triggers! +admin.fullName = "Alice Cooper"; // (**) + +alert(admin.fullName); // Alice Cooper, state of admin modified +alert(user.fullName); // John Smith, state of user protected +``` + +Here in the line `(*)` the property `admin.fullName` has a getter in the prototype `user`, so it is called. And in the line `(**)` the property has a setter in the prototype, so it is called. + +## The value of "this" + +An interesting question may arise in the example above: what's the value of `this` inside `set fullName(value)`? Where are the properties `this.name` and `this.surname` written: into `user` or `admin`? + +The answer is simple: `this` is not affected by prototypes at all. + +**No matter where the method is found: in an object or its prototype. In a method call, `this` is always the object before the dot.** + +So, the setter call `admin.fullName=` uses `admin` as `this`, not `user`. + +That is actually a super-important thing, because we may have a big object with many methods, and have objects that inherit from it. And when the inheriting objects run the inherited methods, they will modify only their own states, not the state of the big object. + +For instance, here `animal` represents a "method storage", and `rabbit` makes use of it. + +The call `rabbit.sleep()` sets `this.isSleeping` on the `rabbit` object: + +```js run +// animal has methods +let animal = { + walk() { + if (!this.isSleeping) { + alert(`I walk`); + } + }, + sleep() { + this.isSleeping = true; + } +}; + +let rabbit = { + name: "White Rabbit", + __proto__: animal +}; + +// modifies rabbit.isSleeping +rabbit.sleep(); + +alert(rabbit.isSleeping); // true +alert(animal.isSleeping); // undefined (no such property in the prototype) +``` + +The resulting picture: + +![](proto-animal-rabbit-walk-3.svg) + +If we had other objects, like `bird`, `snake`, etc., inheriting from `animal`, they would also gain access to methods of `animal`. But `this` in each method call would be the corresponding object, evaluated at the call-time (before dot), not `animal`. So when we write data into `this`, it is stored into these objects. + +As a result, methods are shared, but the object state is not. + +## for..in loop + +The `for..in` loop iterates over inherited properties too. + +For instance: + +```js run +let animal = { + eats: true +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +*!* +// Object.keys only returns own keys +alert(Object.keys(rabbit)); // jumps +*/!* + +*!* +// for..in loops over both own and inherited keys +for(let prop in rabbit) alert(prop); // jumps, then eats +*/!* +``` + +If that's not what we want, and we'd like to exclude inherited properties, there's a built-in method [obj.hasOwnProperty(key)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. + +So we can filter out inherited properties (or do something else with them): + +```js run +let animal = { + eats: true +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +for(let prop in rabbit) { + let isOwn = rabbit.hasOwnProperty(prop); + + if (isOwn) { + alert(`Our: ${prop}`); // Our: jumps + } else { + alert(`Inherited: ${prop}`); // Inherited: eats + } +} +``` + +Here we have the following inheritance chain: `rabbit` inherits from `animal`, that inherits from `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it: + +![](rabbit-animal-object.svg) + +Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? We did not define it. Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited. + +...But why does `hasOwnProperty` not appear in the `for..in` loop like `eats` and `jumps` do, if `for..in` lists inherited properties? + +The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`, it has `enumerable:false` flag. And `for..in` only lists enumerable properties. That's why it and the rest of the `Object.prototype` properties are not listed. + +```smart header="Almost all other key/value-getting methods ignore inherited properties" +Almost all other key/value-getting methods, such as `Object.keys`, `Object.values` and so on ignore inherited properties. + +They only operate on the object itself. Properties from the prototype are *not* taken into account. +``` + +## Summary + +- In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`. +- We can use `obj.__proto__` to access it (a historical getter/setter, there are other ways, to be covered soon). +- The object referenced by `[[Prototype]]` is called a "prototype". +- If we want to read a property of `obj` or call a method, and it doesn't exist, then JavaScript tries to find it in the prototype. +- Write/delete operations act directly on the object, they don't use the prototype (assuming it's a data property, not a setter). +- If we call `obj.method()`, and the `method` is taken from the prototype, `this` still references `obj`. So methods always work with the current object even if they are inherited. +- The `for..in` loop iterates over both its own and its inherited properties. All other key/value-getting methods only operate on the object itself. diff --git a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg new file mode 100644 index 0000000000..eb79c19ffd --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg @@ -0,0 +1 @@ +prototype objectobject[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg new file mode 100644 index 0000000000..4bf580ae77 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg @@ -0,0 +1 @@ +eats: true walk: functionanimaljumps: truerabbit[[Prototype]]earLength: 10longEar[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg new file mode 100644 index 0000000000..838c78395b --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg @@ -0,0 +1 @@ +eats: true walk: functionanimalwalk: functionrabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg new file mode 100644 index 0000000000..d791e5390d --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg @@ -0,0 +1 @@ +walk: function sleep: functionanimalrabbit[[Prototype]]name: "White Rabbit" isSleeping: true \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg new file mode 100644 index 0000000000..b324710286 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg @@ -0,0 +1 @@ +eats: true walk: functionanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg new file mode 100644 index 0000000000..4f3c1bc0ec --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg @@ -0,0 +1 @@ +eats: trueanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg new file mode 100644 index 0000000000..bf0baf013a --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg @@ -0,0 +1 @@ +name: "John" surname: "Smith" set fullName: functionisAdmin: true name: "Alice" surname: "Cooper"useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg new file mode 100644 index 0000000000..32a9858f83 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg @@ -0,0 +1 @@ +toString: function hasOwnProperty: function ...Object.prototypeanimal[[Prototype]][[Prototype]][[Prototype]]nulleats: truerabbitjumps: true \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md new file mode 100644 index 0000000000..ebbdf3a7c1 --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md @@ -0,0 +1,20 @@ + +Answers: + +1. `true`. + + The assignment to `Rabbit.prototype` sets up `[[Prototype]]` for new objects, but it does not affect the existing ones. + +2. `false`. + + Objects are assigned by reference. The object from `Rabbit.prototype` is not duplicated, it's still a single object referenced both by `Rabbit.prototype` and by the `[[Prototype]]` of `rabbit`. + + So when we change its content through one reference, it is visible through the other one. + +3. `true`. + + All `delete` operations are applied directly to the object. Here `delete rabbit.eats` tries to remove `eats` property from `rabbit`, but it doesn't have it. So the operation won't have any effect. + +4. `undefined`. + + The property `eats` is deleted from the prototype, it doesn't exist any more. diff --git a/1-js/08-prototypes/02-function-prototype/1-changing-prototype/task.md b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/task.md new file mode 100644 index 0000000000..2838c125ad --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/task.md @@ -0,0 +1,89 @@ +importance: 5 + +--- + +# Changing "prototype" + +In the code below we create `new Rabbit`, and then try to modify its prototype. + +In the start, we have this code: + +```js run +function Rabbit() {} +Rabbit.prototype = { + eats: true +}; + +let rabbit = new Rabbit(); + +alert( rabbit.eats ); // true +``` + + +1. We added one more string (emphasized). What will `alert` show now? + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + Rabbit.prototype = {}; + */!* + + alert( rabbit.eats ); // ? + ``` + +2. ...And if the code is like this (replaced one line)? + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + Rabbit.prototype.eats = false; + */!* + + alert( rabbit.eats ); // ? + ``` + +3. And like this (replaced one line)? + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + delete rabbit.eats; + */!* + + alert( rabbit.eats ); // ? + ``` + +4. The last variant: + + ```js + function Rabbit() {} + Rabbit.prototype = { + eats: true + }; + + let rabbit = new Rabbit(); + + *!* + delete Rabbit.prototype.eats; + */!* + + alert( rabbit.eats ); // ? + ``` 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 new file mode 100644 index 0000000000..372d50dd6d --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md @@ -0,0 +1,49 @@ +We can use such approach if we are sure that `"constructor"` property has the correct value. + +For instance, if we don't touch the default `"prototype"`, then this code works for sure: + +```js run +function User(name) { + this.name = name; +} + +let user = new User('John'); +let user2 = new user.constructor('Pete'); + +alert( user2.name ); // Pete (worked!) +``` + +It worked, because `User.prototype.constructor == User`. + +..But if someone, so to speak, overwrites `User.prototype` and forgets to recreate `constructor` to reference `User`, then it would fail. + +For instance: + +```js run +function User(name) { + this.name = name; +} +*!* +User.prototype = {}; // (*) +*/!* + +let user = new User('John'); +let user2 = new user.constructor('Pete'); + +alert( user2.name ); // undefined +``` + +Why `user2.name` is `undefined`? + +Here's how `new user.constructor('Pete')` works: + +1. First, it looks for `constructor` in `user`. Nothing. +2. Then it follows the prototype chain. The prototype of `user` is `User.prototype`, and it also has no `constructor` (because we "forgot" to set it right!). +3. Going further up the chain, `User.prototype` is a plain object, its prototype is the built-in `Object.prototype`. +4. Finally, for the built-in `Object.prototype`, there's a built-in `Object.prototype.constructor == Object`. So it is used. + +Finally, at the end, we have `let user2 = new Object('Pete')`. + +Probably, that's not what we want. We'd like to create `new User`, not `new Object`. That's the outcome of the missing `constructor`. + +(Just in case you're curious, the `new Object(...)` call converts its argument to an object. That's a theoretical thing, in practice no one calls `new Object` with a value, and generally we don't use `new Object` to make objects at all). \ No newline at end of file diff --git a/1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/task.md b/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/task.md similarity index 100% rename from 1-js/07-object-oriented-programming/04-function-prototype/4-new-object-same-constructor/task.md rename to 1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/task.md diff --git a/1-js/08-prototypes/02-function-prototype/article.md b/1-js/08-prototypes/02-function-prototype/article.md new file mode 100644 index 0000000000..b1ef518266 --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/article.md @@ -0,0 +1,175 @@ +# F.prototype + +Remember, new objects can be created with a constructor function, like `new F()`. + +If `F.prototype` is an object, then the `new` operator uses it to set `[[Prototype]]` for the new object. + +```smart +JavaScript had prototypal inheritance from the beginning. It was one of the core features of the language. + +But in the old times, there was no direct access to it. The only thing that worked reliably was a `"prototype"` property of the constructor function, described in this chapter. So there are many scripts that still use it. +``` + +Please note that `F.prototype` here means a regular property named `"prototype"` on `F`. It sounds something similar to the term "prototype", but here we really mean a regular property with this name. + +Here's the example: + +```js run +let animal = { + eats: true +}; + +function Rabbit(name) { + this.name = name; +} + +*!* +Rabbit.prototype = animal; +*/!* + +let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal + +alert( rabbit.eats ); // true +``` + +Setting `Rabbit.prototype = animal` literally states the following: "When a `new Rabbit` is created, assign its `[[Prototype]]` to `animal`". + +That's the resulting picture: + +![](proto-constructor-animal-rabbit.svg) + +On the picture, `"prototype"` is a horizontal arrow, meaning a regular property, and `[[Prototype]]` is vertical, meaning the inheritance of `rabbit` from `animal`. + +```smart header="`F.prototype` only used at `new F` time" +`F.prototype` property is only used when `new F` is called, it assigns `[[Prototype]]` of the new object. + +If, after the creation, `F.prototype` property changes (`F.prototype = `), then new objects created by `new F` will have another object as `[[Prototype]]`, but already existing objects keep the old one. +``` + +## Default F.prototype, constructor property + +Every function has the `"prototype"` property even if we don't supply it. + +The default `"prototype"` is an object with the only property `constructor` that points back to the function itself. + +Like this: + +```js +function Rabbit() {} + +/* default prototype +Rabbit.prototype = { constructor: Rabbit }; +*/ +``` + +![](function-prototype-constructor.svg) + +We can check it: + +```js run +function Rabbit() {} +// by default: +// Rabbit.prototype = { constructor: Rabbit } + +alert( Rabbit.prototype.constructor == Rabbit ); // true +``` + +Naturally, if we do nothing, the `constructor` property is available to all rabbits through `[[Prototype]]`: + +```js run +function Rabbit() {} +// by default: +// Rabbit.prototype = { constructor: Rabbit } + +let rabbit = new Rabbit(); // inherits from {constructor: Rabbit} + +alert(rabbit.constructor == Rabbit); // true (from prototype) +``` + +![](rabbit-prototype-constructor.svg) + +We can use `constructor` property to create a new object using the same constructor as the existing one. + +Like here: + +```js run +function Rabbit(name) { + this.name = name; + alert(name); +} + +let rabbit = new Rabbit("White Rabbit"); + +*!* +let rabbit2 = new rabbit.constructor("Black Rabbit"); +*/!* +``` + +That's handy when we have an object, don't know which constructor was used for it (e.g. it comes from a 3rd party library), and we need to create another one of the same kind. + +But probably the most important thing about `"constructor"` is that... + +**...JavaScript itself does not ensure the right `"constructor"` value.** + +Yes, it exists in the default `"prototype"` for functions, but that's all. What happens with it later -- is totally on us. + +In particular, if we replace the default prototype as a whole, then there will be no `"constructor"` in it. + +For instance: + +```js run +function Rabbit() {} +Rabbit.prototype = { + jumps: true +}; + +let rabbit = new Rabbit(); +*!* +alert(rabbit.constructor === Rabbit); // false +*/!* +``` + +So, to keep the right `"constructor"` we can choose to add/remove properties to the default `"prototype"` instead of overwriting it as a whole: + +```js +function Rabbit() {} + +// Not overwrite Rabbit.prototype totally +// just add to it +Rabbit.prototype.jumps = true +// the default Rabbit.prototype.constructor is preserved +``` + +Or, alternatively, recreate the `constructor` property manually: + +```js +Rabbit.prototype = { + jumps: true, +*!* + constructor: Rabbit +*/!* +}; + +// now constructor is also correct, because we added it +``` + + +## Summary + +In this chapter we briefly described the way of setting a `[[Prototype]]` for objects created via a constructor function. Later we'll see more advanced programming patterns that rely on it. + +Everything is quite simple, just a few notes to make things clear: + +- The `F.prototype` property (don't mistake it for `[[Prototype]]`) sets `[[Prototype]]` of new objects when `new F()` is called. +- The value of `F.prototype` should be either an object or `null`: other values won't work. +- The `"prototype"` property only has such a special effect when set on a constructor function, and invoked with `new`. + +On regular objects the `prototype` is nothing special: +```js +let user = { + name: "John", + prototype: "Bla-bla" // no magic at all +}; +``` + +By default all functions have `F.prototype = { constructor: F }`, so we can get the constructor of an object by accessing its `"constructor"` property. diff --git a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg new file mode 100644 index 0000000000..59d60b397a --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg @@ -0,0 +1 @@ +Rabbitprototypeconstructordefault "prototype" \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg new file mode 100644 index 0000000000..ede4e1227e --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg @@ -0,0 +1 @@ +eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg new file mode 100644 index 0000000000..54b3d79804 --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg @@ -0,0 +1 @@ +default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/solution.md b/1-js/08-prototypes/03-native-prototypes/1-defer-to-prototype/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/solution.md rename to 1-js/08-prototypes/03-native-prototypes/1-defer-to-prototype/solution.md diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/task.md b/1-js/08-prototypes/03-native-prototypes/1-defer-to-prototype/task.md similarity index 100% rename from 1-js/07-object-oriented-programming/05-native-prototypes/1-defer-to-prototype/task.md rename to 1-js/08-prototypes/03-native-prototypes/1-defer-to-prototype/task.md diff --git a/1-js/08-prototypes/03-native-prototypes/2-defer-to-prototype-extended/solution.md b/1-js/08-prototypes/03-native-prototypes/2-defer-to-prototype-extended/solution.md new file mode 100644 index 0000000000..99c358c9b0 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/2-defer-to-prototype-extended/solution.md @@ -0,0 +1,41 @@ + + +```js run +Function.prototype.defer = function(ms) { + let f = this; + return function(...args) { + setTimeout(() => f.apply(this, args), ms); + } +}; + +// check it +function f(a, b) { + alert( a + b ); +} + +f.defer(1000)(1, 2); // shows 3 after 1 sec +``` + +Please note: we use `this` in `f.apply` to make our decoration work for object methods. + +So if the wrapper function is called as an object method, then `this` is passed to the original method `f`. + +```js run +Function.prototype.defer = function(ms) { + let f = this; + return function(...args) { + setTimeout(() => f.apply(this, args), ms); + } +}; + +let user = { + name: "John", + sayHi() { + alert(this.name); + } +} + +user.sayHi = user.sayHi.defer(1000); + +user.sayHi(); +``` diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/task.md b/1-js/08-prototypes/03-native-prototypes/2-defer-to-prototype-extended/task.md similarity index 100% rename from 1-js/07-object-oriented-programming/05-native-prototypes/2-defer-to-prototype-extended/task.md rename to 1-js/08-prototypes/03-native-prototypes/2-defer-to-prototype-extended/task.md diff --git a/1-js/08-prototypes/03-native-prototypes/article.md b/1-js/08-prototypes/03-native-prototypes/article.md new file mode 100644 index 0000000000..bdfc86dd8d --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/article.md @@ -0,0 +1,198 @@ +# Native prototypes + +The `"prototype"` property is widely used by the core of JavaScript itself. All built-in constructor functions use it. + +First we'll look at the details, and then how to use it for adding new capabilities to built-in objects. + +## Object.prototype + +Let's say we output an empty object: + +```js run +let obj = {}; +alert( obj ); // "[object Object]" ? +``` + +Where's the code that generates the string `"[object Object]"`? That's a built-in `toString` method, but where is it? The `obj` is empty! + +...But the short notation `obj = {}` is the same as `obj = new Object()`, where `Object` is a built-in object constructor function, with its own `prototype` referencing a huge object with `toString` and other methods. + +Here's what's going on: + +![](object-prototype.svg) + +When `new Object()` is called (or a literal object `{...}` is created), the `[[Prototype]]` of it is set to `Object.prototype` according to the rule that we discussed in the previous chapter: + +![](object-prototype-1.svg) + +So then when `obj.toString()` is called the method is taken from `Object.prototype`. + +We can check it like this: + +```js run +let obj = {}; + +alert(obj.__proto__ === Object.prototype); // true + +alert(obj.toString === obj.__proto__.toString); //true +alert(obj.toString === Object.prototype.toString); //true +``` + +Please note that there is no more `[[Prototype]]` in the chain above `Object.prototype`: + +```js run +alert(Object.prototype.__proto__); // null +``` + +## Other built-in prototypes + +Other built-in objects such as `Array`, `Date`, `Function` and others also keep methods in prototypes. + +For instance, when we create an array `[1, 2, 3]`, the default `new Array()` constructor is used internally. So `Array.prototype` becomes its prototype and provides methods. That's very memory-efficient. + +By specification, all of the built-in prototypes have `Object.prototype` on the top. That's why some people say that "everything inherits from objects". + +Here's the overall picture (for 3 built-ins to fit): + +![](native-prototypes-classes.svg) + +Let's check the prototypes manually: + +```js run +let arr = [1, 2, 3]; + +// it inherits from Array.prototype? +alert( arr.__proto__ === Array.prototype ); // true + +// then from Object.prototype? +alert( arr.__proto__.__proto__ === Object.prototype ); // true + +// and null on the top. +alert( arr.__proto__.__proto__.__proto__ ); // null +``` + +Some methods in prototypes may overlap, for instance, `Array.prototype` has its own `toString` that lists comma-delimited elements: + +```js run +let arr = [1, 2, 3] +alert(arr); // 1,2,3 <-- the result of Array.prototype.toString +``` + +As we've seen before, `Object.prototype` has `toString` as well, but `Array.prototype` is closer in the chain, so the array variant is used. + + +![](native-prototypes-array-tostring.svg) + + +In-browser tools like Chrome developer console also show inheritance (`console.dir` may need to be used for built-in objects): + +![](console_dir_array.png) + +Other built-in objects also work the same way. Even functions -- they are objects of a built-in `Function` constructor, and their methods (`call`/`apply` and others) are taken from `Function.prototype`. Functions have their own `toString` too. + +```js run +function f() {} + +alert(f.__proto__ == Function.prototype); // true +alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects +``` + +## Primitives + +The most intricate thing happens with strings, numbers and booleans. + +As we remember, they are not objects. But if we try to access their properties, temporary wrapper objects are created using built-in constructors `String`, `Number` and `Boolean`. They provide the methods and disappear. + +These objects are created invisibly to us and most engines optimize them out, but the specification describes it exactly this way. Methods of these objects also reside in prototypes, available as `String.prototype`, `Number.prototype` and `Boolean.prototype`. + +```warn header="Values `null` and `undefined` have no object wrappers" +Special values `null` and `undefined` stand apart. They have no object wrappers, so methods and properties are not available for them. And there are no corresponding prototypes either. +``` + +## Changing native prototypes [#native-prototype-change] + +Native prototypes can be modified. For instance, if we add a method to `String.prototype`, it becomes available to all strings: + +```js run +String.prototype.show = function() { + alert(this); +}; + +"BOOM!".show(); // BOOM! +``` + +During the process of development, we may have ideas for new built-in methods we'd like to have, and we may be tempted to add them to native prototypes. But that is generally a bad idea. + +```warn +Prototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them will be overwriting the method of the other. + +So, generally, modifying a native prototype is considered a bad idea. +``` + +**In modern programming, there is only one case where modifying native prototypes is approved. That's polyfilling.** + +Polyfilling is a term for making a substitute for a method that exists in the JavaScript specification, but is not yet supported by a particular JavaScript engine. + +We may then implement it manually and populate the built-in prototype with it. + +For instance: + +```js run +if (!String.prototype.repeat) { // if there's no such method + // add it to the prototype + + String.prototype.repeat = function(n) { + // repeat the string n times + + // actually, the code should be a little bit more complex than that + // (the full algorithm is in the specification) + // but even an imperfect polyfill is often considered good enough + return new Array(n + 1).join(this); + }; +} + +alert( "La".repeat(3) ); // LaLaLa +``` + + +## Borrowing from prototypes + +In the chapter we talked about method borrowing. + +That's when we take a method from one object and copy it into another. + +Some methods of native prototypes are often borrowed. + +For instance, if we're making an array-like object, we may want to copy some `Array` methods to it. + +E.g. + +```js run +let obj = { + 0: "Hello", + 1: "world!", + length: 2, +}; + +*!* +obj.join = Array.prototype.join; +*/!* + +alert( obj.join(',') ); // Hello,world! +``` + +It works because the internal algorithm of the built-in `join` method only cares about the correct indexes and the `length` property. It doesn't check if the object is indeed an array. Many built-in methods are like that. + +Another possibility is to inherit by setting `obj.__proto__` to `Array.prototype`, so all `Array` methods are automatically available in `obj`. + +But that's impossible if `obj` already inherits from another object. Remember, we only can inherit from one object at a time. + +Borrowing methods is flexible, it allows to mix functionalities from different objects if needed. + +## Summary + +- All built-in objects follow the same pattern: + - The methods are stored in the prototype (`Array.prototype`, `Object.prototype`, `Date.prototype`, etc.) + - The object itself stores only the data (array items, object properties, the date) +- Primitives also store methods in prototypes of wrapper objects: `Number.prototype`, `String.prototype` and `Boolean.prototype`. Only `undefined` and `null` do not have wrapper objects +- Built-in prototypes can be modified or populated with new methods. But it's not recommended to change them. The only allowable case is probably when we add-in a new standard, but it's not yet supported by the JavaScript engine diff --git a/1-js/07-object-oriented-programming/05-native-prototypes/console_dir_array.png b/1-js/08-prototypes/03-native-prototypes/console_dir_array.png similarity index 100% rename from 1-js/07-object-oriented-programming/05-native-prototypes/console_dir_array.png rename to 1-js/08-prototypes/03-native-prototypes/console_dir_array.png diff --git a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg new file mode 100644 index 0000000000..59d60b397a --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg @@ -0,0 +1 @@ +Rabbitprototypeconstructordefault "prototype" \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg new file mode 100644 index 0000000000..ebb4f32051 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg @@ -0,0 +1 @@ +toString: function ...Array.prototypetoString: function ...Object.prototype[[Prototype]][[Prototype]][1, 2, 3] \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg new file mode 100644 index 0000000000..4d6129e0a0 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg @@ -0,0 +1 @@ +toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg new file mode 100644 index 0000000000..9630e68e27 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg @@ -0,0 +1 @@ +constructor: Object toString: function ...Object.prototypeObjectobj = new Object()[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg new file mode 100644 index 0000000000..9ccb342299 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg @@ -0,0 +1 @@ +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg new file mode 100644 index 0000000000..024dd30213 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg @@ -0,0 +1 @@ +constructor: Object toString: function ...Object.prototypeObjectprototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg new file mode 100644 index 0000000000..54b3d79804 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg @@ -0,0 +1 @@ +default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/2-dictionary-tostring/solution.md b/1-js/08-prototypes/04-prototype-methods/2-dictionary-tostring/solution.md new file mode 100644 index 0000000000..f3c9cf0e52 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/2-dictionary-tostring/solution.md @@ -0,0 +1,31 @@ + +The method can take all enumerable keys using `Object.keys` and output their list. + +To make `toString` non-enumerable, let's define it using a property descriptor. The syntax of `Object.create` allows us to provide an object with property descriptors as the second argument. + +```js run +*!* +let dictionary = Object.create(null, { + toString: { // define toString property + value() { // the value is a function + return Object.keys(this).join(); + } + } +}); +*/!* + +dictionary.apple = "Apple"; +dictionary.__proto__ = "test"; + +// apple and __proto__ is in the loop +for(let key in dictionary) { + alert(key); // "apple", then "__proto__" +} + +// comma-separated list of properties by toString +alert(dictionary); // "apple,__proto__" +``` + +When we create a property using a descriptor, its flags are `false` by default. So in the code above, `dictionary.toString` is non-enumerable. + +See the chapter [](info:property-descriptors) for review. diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md b/1-js/08-prototypes/04-prototype-methods/2-dictionary-tostring/task.md similarity index 100% rename from 1-js/07-object-oriented-programming/06-prototype-methods/2-dictionary-tostring/task.md rename to 1-js/08-prototypes/04-prototype-methods/2-dictionary-tostring/task.md diff --git a/1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/solution.md b/1-js/08-prototypes/04-prototype-methods/3-compare-calls/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/06-prototype-methods/3-compare-calls/solution.md rename to 1-js/08-prototypes/04-prototype-methods/3-compare-calls/solution.md 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 new file mode 100644 index 0000000000..09bb7f1ed8 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/3-compare-calls/task.md @@ -0,0 +1,27 @@ +importance: 5 + +--- + +# The difference between calls + +Let's create a new `rabbit` object: + +```js +function Rabbit(name) { + this.name = name; +} +Rabbit.prototype.sayHi = function() { + alert(this.name); +}; + +let rabbit = new Rabbit("Rabbit"); +``` + +These calls do the same thing or not? + +```js +rabbit.sayHi(); +Rabbit.prototype.sayHi(); +Object.getPrototypeOf(rabbit).sayHi(); +rabbit.__proto__.sayHi(); +``` diff --git a/1-js/08-prototypes/04-prototype-methods/article.md b/1-js/08-prototypes/04-prototype-methods/article.md new file mode 100644 index 0000000000..34b977e9f1 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/article.md @@ -0,0 +1,223 @@ + +# Prototype methods, objects without __proto__ + +In the first chapter of this section, we mentioned that there are modern methods to setup a prototype. + +Setting or reading the prototype with `obj.__proto__` is considered outdated and somewhat deprecated (moved to the so-called "Annex B" of the JavaScript standard, meant for browsers only). + +The modern methods to get/set a prototype are: + +- [Object.getPrototypeOf(obj)](mdn:js/Object/getPrototypeOf) -- returns the `[[Prototype]]` of `obj`. +- [Object.setPrototypeOf(obj, proto)](mdn:js/Object/setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto`. + +The only usage of `__proto__`, that's not frowned upon, is as a property when creating a new object: `{ __proto__: ... }`. + +Although, there's a special method for this too: + +- [Object.create(proto[, descriptors])](mdn:js/Object/create) -- creates an empty object with given `proto` as `[[Prototype]]` and optional property descriptors. + +For instance: + +```js run +let animal = { + eats: true +}; + +// create a new object with animal as a prototype +*!* +let rabbit = Object.create(animal); // same as {__proto__: animal} +*/!* + +alert(rabbit.eats); // true + +*!* +alert(Object.getPrototypeOf(rabbit) === animal); // true +*/!* + +*!* +Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {} +*/!* +``` + +The `Object.create` method is a bit more powerful, as it has an optional second argument: property descriptors. + +We can provide additional properties to the new object there, like this: + +```js run +let animal = { + eats: true +}; + +let rabbit = Object.create(animal, { + jumps: { + value: true + } +}); + +alert(rabbit.jumps); // true +``` + +The descriptors are in the same format as described in the chapter . + +We can use `Object.create` to perform an object cloning more powerful than copying properties in `for..in`: + +```js +let clone = Object.create( + Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) +); +``` + +This call makes a truly exact copy of `obj`, including all properties: enumerable and non-enumerable, data properties and setters/getters -- everything, and with the right `[[Prototype]]`. + + +## Brief history + +There're so many ways to manage `[[Prototype]]`. How did that happen? Why? + +That's for historical reasons. + +The prototypal inheritance was in the language since its dawn, but the ways to manage it evolved over time. + +- The `prototype` property of a constructor function has worked since very ancient times. It's the oldest way to create objects with a given prototype. +- Later, in the year 2012, `Object.create` appeared in the standard. It gave the ability to create objects with a given prototype, but did not provide the ability to get/set it. Some browsers implemented the non-standard `__proto__` accessor that allowed the user to get/set a prototype at any time, to give more flexibility to developers. +- Later, in the year 2015, `Object.setPrototypeOf` and `Object.getPrototypeOf` were added to the standard, to perform the same functionality as `__proto__`. As `__proto__` was de-facto implemented everywhere, it was kind-of deprecated and made its way to the Annex B of the standard, that is: optional for non-browser environments. +- Later, in the year 2022, it was officially allowed to use `__proto__` in object literals `{...}` (moved out of Annex B), but not as a getter/setter `obj.__proto__` (still in Annex B). + +Why was `__proto__` replaced by the functions `getPrototypeOf/setPrototypeOf`? + +Why was `__proto__` partially rehabilitated and its usage allowed in `{...}`, but not as a getter/setter? + +That's an interesting question, requiring us to understand why `__proto__` is bad. + +And soon we'll get the answer. + +```warn header="Don't change `[[Prototype]]` on existing objects if speed matters" +Technically, we can get/set `[[Prototype]]` at any time. But usually we only set it once at the object creation time and don't modify it anymore: `rabbit` inherits from `animal`, and that is not going to change. + +And JavaScript engines are highly optimized for this. Changing a prototype "on-the-fly" with `Object.setPrototypeOf` or `obj.__proto__=` is a very slow operation as it breaks internal optimizations for object property access operations. So avoid it unless you know what you're doing, or JavaScript speed totally doesn't matter for you. +``` + +## "Very plain" objects [#very-plain] + +As we know, objects can be used as associative arrays to store key/value pairs. + +...But if we try to store *user-provided* keys in it (for instance, a user-entered dictionary), we can see an interesting glitch: all keys work fine except `"__proto__"`. + +Check out the example: + +```js run +let obj = {}; + +let key = prompt("What's the key?", "__proto__"); +obj[key] = "some value"; + +alert(obj[key]); // [object Object], not "some value"! +``` + +Here, if the user types in `__proto__`, the assignment in line 4 is ignored! + +That could surely be surprising for a non-developer, but pretty understandable for us. The `__proto__` property is special: it must be either an object or `null`. A string can not become a prototype. That's why an assignment a string to `__proto__` is ignored. + +But we didn't *intend* to implement such behavior, right? We want to store key/value pairs, and the key named `"__proto__"` was not properly saved. So that's a bug! + +Here the consequences are not terrible. But in other cases we may be storing objects instead of strings in `obj`, and then the prototype will indeed be changed. As a result, the execution will go wrong in totally unexpected ways. + +What's worse -- usually developers do not think about such possibility at all. That makes such bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on server-side. + +Unexpected things also may happen when assigning to `obj.toString`, as it's a built-in object method. + +How can we avoid this problem? + +First, we can just switch to using `Map` for storage instead of plain objects, then everything's fine: + +```js run +let map = new Map(); + +let key = prompt("What's the key?", "__proto__"); +map.set(key, "some value"); + +alert(map.get(key)); // "some value" (as intended) +``` + +...But `Object` syntax is often more appealing, as it's more concise. + +Fortunately, we *can* use objects, because language creators gave thought to that problem long ago. + +As we know, `__proto__` is not a property of an object, but an accessor property of `Object.prototype`: + +![](object-prototype-2.svg) + +So, if `obj.__proto__` is read or set, the corresponding getter/setter is called from its prototype, and it gets/sets `[[Prototype]]`. + +As it was said in the beginning of this tutorial section: `__proto__` is a way to access `[[Prototype]]`, it is not `[[Prototype]]` itself. + +Now, if we intend to use an object as an associative array and be free of such problems, we can do it with a little trick: + +```js run +*!* +let obj = Object.create(null); +// or: obj = { __proto__: null } +*/!* + +let key = prompt("What's the key?", "__proto__"); +obj[key] = "some value"; + +alert(obj[key]); // "some value" +``` + +`Object.create(null)` creates an empty object without a prototype (`[[Prototype]]` is `null`): + +![](object-prototype-null.svg) + +So, there is no inherited getter/setter for `__proto__`. Now it is processed as a regular data property, so the example above works right. + +We can call such objects "very plain" or "pure dictionary" objects, because they are even simpler than the regular plain object `{...}`. + +A downside is that such objects lack any built-in object methods, e.g. `toString`: + +```js run +*!* +let obj = Object.create(null); +*/!* + +alert(obj); // Error (no toString) +``` + +...But that's usually fine for associative arrays. + +Note that most object-related methods are `Object.something(...)`, like `Object.keys(obj)` -- they are not in the prototype, so they will keep working on such objects: + + +```js run +let chineseDictionary = Object.create(null); +chineseDictionary.hello = "你好"; +chineseDictionary.bye = "再见"; + +alert(Object.keys(chineseDictionary)); // hello,bye +``` + +## Summary + +- To create an object with the given prototype, use: + + - literal syntax: `{ __proto__: ... }`, allows to specify multiple properties + - or [Object.create(proto[, descriptors])](mdn:js/Object/create), allows to specify property descriptors. + + The `Object.create` provides an easy way to shallow-copy an object with all descriptors: + + ```js + let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); + ``` + +- Modern methods to get/set the prototype are: + + - [Object.getPrototypeOf(obj)](mdn:js/Object/getPrototypeOf) -- returns the `[[Prototype]]` of `obj` (same as `__proto__` getter). + - [Object.setPrototypeOf(obj, proto)](mdn:js/Object/setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto` (same as `__proto__` setter). + +- Getting/setting the prototype using the built-in `__proto__` getter/setter isn't recommended, it's now in the Annex B of the specification. + +- We also covered prototype-less objects, created with `Object.create(null)` or `{__proto__: null}`. + + These objects are used as dictionaries, to store any (possibly user-generated) keys. + + Normally, objects inherit built-in methods and `__proto__` getter/setter from `Object.prototype`, making corresponding keys "occupied" and potentially causing side effects. With `null` prototype, objects are truly empty. diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg new file mode 100644 index 0000000000..cf4d3023f8 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg @@ -0,0 +1 @@ +... get __proto__: function set __proto__: functionObject.prototypeObjectobj[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg new file mode 100644 index 0000000000..9ccb342299 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg @@ -0,0 +1 @@ +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/08-prototypes/index.md b/1-js/08-prototypes/index.md new file mode 100644 index 0000000000..8554a0e30b --- /dev/null +++ b/1-js/08-prototypes/index.md @@ -0,0 +1 @@ +# Prototypes, inheritance diff --git a/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/solution.js b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/solution.js new file mode 100644 index 0000000000..0b31cf334e --- /dev/null +++ b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/solution.js @@ -0,0 +1,38 @@ +class Clock { + constructor({ template }) { + this.template = template; + } + + render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) mins = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this.template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + stop() { + clearInterval(this.timer); + } + + start() { + this.render(); + this.timer = setInterval(() => this.render(), 1000); + } +} + + +let clock = new Clock({template: 'h:m:s'}); +clock.start(); diff --git a/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/source.js b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/source.js new file mode 100644 index 0000000000..f1749c8ba5 --- /dev/null +++ b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/source.js @@ -0,0 +1,37 @@ +function Clock({ template }) { + + let timer; + + function render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) mins = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + this.stop = function() { + clearInterval(timer); + }; + + this.start = function() { + render(); + timer = setInterval(render, 1000); + }; + +} + +let clock = new Clock({template: 'h:m:s'}); +clock.start(); diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.md b/1-js/09-classes/01-class/1-rewrite-to-class/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/10-class-inheritance/2-clock-class-extended/solution.md rename to 1-js/09-classes/01-class/1-rewrite-to-class/solution.md diff --git a/1-js/09-classes/01-class/1-rewrite-to-class/task.md b/1-js/09-classes/01-class/1-rewrite-to-class/task.md new file mode 100644 index 0000000000..4477de6799 --- /dev/null +++ b/1-js/09-classes/01-class/1-rewrite-to-class/task.md @@ -0,0 +1,9 @@ +importance: 5 + +--- + +# Rewrite to class + +The `Clock` class (see the sandbox) is written in functional style. Rewrite it in the "class" syntax. + +P.S. The clock ticks in the console, open it to see. diff --git a/1-js/09-classes/01-class/article.md b/1-js/09-classes/01-class/article.md new file mode 100644 index 0000000000..135d24929b --- /dev/null +++ b/1-js/09-classes/01-class/article.md @@ -0,0 +1,428 @@ + +# Class basic syntax + +```quote author="Wikipedia" +In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods). +``` + +In practice, we often need to create many objects of the same kind, like users, or goods or whatever. + +As we already know from the chapter , `new function` can help with that. + +But in the modern JavaScript, there's a more advanced "class" construct, that introduces great new features which are useful for object-oriented programming. + +## The "class" syntax + +The basic syntax is: +```js +class MyClass { + // class methods + constructor() { ... } + method1() { ... } + method2() { ... } + method3() { ... } + ... +} +``` + +Then use `new MyClass()` to create a new object with all the listed methods. + +The `constructor()` method is called automatically by `new`, so we can initialize the object there. + +For example: + +```js run +class User { + + constructor(name) { + this.name = name; + } + + sayHi() { + alert(this.name); + } + +} + +// Usage: +let user = new User("John"); +user.sayHi(); +``` + +When `new User("John")` is called: +1. A new object is created. +2. The `constructor` runs with the given argument and assigns it to `this.name`. + +...Then we can call object methods, such as `user.sayHi()`. + + +```warn header="No comma between class methods" +A common pitfall for novice developers is to put a comma between class methods, which would result in a syntax error. + +The notation here is not to be confused with object literals. Within the class, no commas are required. +``` + +## What is a class? + +So, what exactly is a `class`? That's not an entirely new language-level entity, as one might think. + +Let's unveil any magic and see what a class really is. That'll help in understanding many complex aspects. + +In JavaScript, a class is a kind of function. + +Here, take a look: + +```js run +class User { + constructor(name) { this.name = name; } + sayHi() { alert(this.name); } +} + +// proof: User is a function +*!* +alert(typeof User); // function +*/!* +``` + +What `class User {...}` construct really does is: + +1. Creates a function named `User`, that becomes the result of the class declaration. The function code is taken from the `constructor` method (assumed empty if we don't write such method). +2. Stores class methods, such as `sayHi`, in `User.prototype`. + +After `new User` object is created, when we call its method, it's taken from the prototype, just as described in the chapter . So the object has access to class methods. + +We can illustrate the result of `class User` declaration as: + +![](class-user.svg) + +Here's the code to introspect it: + +```js run +class User { + constructor(name) { this.name = name; } + sayHi() { alert(this.name); } +} + +// class is a function +alert(typeof User); // function + +// ...or, more precisely, the constructor method +alert(User === User.prototype.constructor); // true + +// The methods are in User.prototype, e.g: +alert(User.prototype.sayHi); // the code of the sayHi method + +// there are exactly two methods in the prototype +alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi +``` + +## Not just a syntactic sugar + +Sometimes people say that `class` is a "syntactic sugar" (syntax that is designed to make things easier to read, but doesn't introduce anything new), because we could actually declare the same thing without using the `class` keyword at all: + +```js run +// rewriting class User in pure functions + +// 1. Create constructor function +function User(name) { + this.name = name; +} +// a function prototype has "constructor" property by default, +// so we don't need to create it + +// 2. Add the method to prototype +User.prototype.sayHi = function() { + alert(this.name); +}; + +// Usage: +let user = new User("John"); +user.sayHi(); +``` + +The result of this definition is about the same. So, there are indeed reasons why `class` can be considered a syntactic sugar to define a constructor together with its prototype methods. + +Still, there are important differences. + +1. First, a function created by `class` is labelled by a special internal property `[[IsClassConstructor]]: true`. So it's not entirely the same as creating it manually. + + The language checks for that property in a variety of places. For example, unlike a regular function, it must be called with `new`: + + ```js run + class User { + constructor() {} + } + + alert(typeof User); // function + User(); // Error: Class constructor User cannot be invoked without 'new' + ``` + + Also, a string representation of a class constructor in most JavaScript engines starts with the "class..." + + ```js run + class User { + constructor() {} + } + + alert(User); // class User { ... } + ``` + There are other differences, we'll see them soon. + +2. Class methods are non-enumerable. + A class definition sets `enumerable` flag to `false` for all methods in the `"prototype"`. + + That's good, because if we `for..in` over an object, we usually don't want its class methods. + +3. Classes always `use strict`. + All code inside the class construct is automatically in strict mode. + +Besides, `class` syntax brings many other features that we'll explore later. + +## Class Expression + +Just like functions, classes can be defined inside another expression, passed around, returned, assigned, etc. + +Here's an example of a class expression: + +```js +let User = class { + sayHi() { + alert("Hello"); + } +}; +``` + +Similar to Named Function Expressions, class expressions may have a name. + +If a class expression has a name, it's visible inside the class only: + +```js run +// "Named Class Expression" +// (no such term in the spec, but that's similar to Named Function Expression) +let User = class *!*MyClass*/!* { + sayHi() { + alert(MyClass); // MyClass name is visible only inside the class + } +}; + +new User().sayHi(); // works, shows MyClass definition + +alert(MyClass); // error, MyClass name isn't visible outside of the class +``` + +We can even make classes dynamically "on-demand", like this: + +```js run +function makeClass(phrase) { + // declare a class and return it + return class { + sayHi() { + alert(phrase); + } + }; +} + +// Create a new class +let User = makeClass("Hello"); + +new User().sayHi(); // Hello +``` + + +## Getters/setters + +Just like literal objects, classes may include getters/setters, computed properties etc. + +Here's an example for `user.name` implemented using `get/set`: + +```js run +class User { + + constructor(name) { + // invokes the setter + this.name = name; + } + +*!* + get name() { +*/!* + return this._name; + } + +*!* + set name(value) { +*/!* + if (value.length < 4) { + alert("Name is too short."); + return; + } + this._name = value; + } + +} + +let user = new User("John"); +alert(user.name); // John + +user = new User(""); // Name is too short. +``` + +Technically, such class declaration works by creating getters and setters in `User.prototype`. + +## Computed names [...] + +Here's an example with a computed method name using brackets `[...]`: + +```js run +class User { + +*!* + ['say' + 'Hi']() { +*/!* + alert("Hello"); + } + +} + +new User().sayHi(); +``` + +Such features are easy to remember, as they resemble that of literal objects. + +## Class fields + +```warn header="Old browsers may need a polyfill" +Class fields are a recent addition to the language. +``` + +Previously, our classes only had methods. + +"Class fields" is a syntax that allows to add any properties. + +For instance, let's add `name` property to `class User`: + +```js run +class User { +*!* + name = "John"; +*/!* + + sayHi() { + alert(`Hello, ${this.name}!`); + } +} + +new User().sayHi(); // Hello, John! +``` + +So, we just write " = " in the declaration, and that's it. + +The important difference of class fields is that they are set on individual objects, not `User.prototype`: + +```js run +class User { +*!* + name = "John"; +*/!* +} + +let user = new User(); +alert(user.name); // John +alert(User.prototype.name); // undefined +``` + +We can also assign values using more complex expressions and function calls: + +```js run +class User { +*!* + name = prompt("Name, please?", "John"); +*/!* +} + +let user = new User(); +alert(user.name); // John +``` + + +### Making bound methods with class fields + +As demonstrated in the chapter functions in JavaScript have a dynamic `this`. It depends on the context of the call. + +So if an object method is passed around and called in another context, `this` won't be a reference to its object any more. + +For instance, this code will show `undefined`: + +```js run +class Button { + constructor(value) { + this.value = value; + } + + click() { + alert(this.value); + } +} + +let button = new Button("hello"); + +*!* +setTimeout(button.click, 1000); // undefined +*/!* +``` + +The problem is called "losing `this`". + +There are two approaches to fixing it, as discussed in the chapter : + +1. Pass a wrapper-function, such as `setTimeout(() => button.click(), 1000)`. +2. Bind the method to object, e.g. in the constructor. + +Class fields provide another, quite elegant syntax: + +```js run +class Button { + constructor(value) { + this.value = value; + } +*!* + click = () => { + alert(this.value); + } +*/!* +} + +let button = new Button("hello"); + +setTimeout(button.click, 1000); // hello +``` + +The class field `click = () => {...}` is created on a per-object basis, there's a separate function for each `Button` object, with `this` inside it referencing that object. We can pass `button.click` around anywhere, and the value of `this` will always be correct. + +That's especially useful in browser environment, for event listeners. + +## Summary + +The basic class syntax looks like this: + +```js +class MyClass { + prop = value; // property + + constructor(...) { // constructor + // ... + } + + method(...) {} // method + + get something(...) {} // getter method + set something(...) {} // setter method + + [Symbol.iterator]() {} // method with computed name (symbol here) + // ... +} +``` + +`MyClass` is technically a function (the one that we provide as `constructor`), while methods, getters and setters are written to `MyClass.prototype`. + +In the next chapters we'll learn more about classes, including inheritance and other features. diff --git a/1-js/09-classes/01-class/class-user.svg b/1-js/09-classes/01-class/class-user.svg new file mode 100644 index 0000000000..418d71d187 --- /dev/null +++ b/1-js/09-classes/01-class/class-user.svg @@ -0,0 +1 @@ +sayHi: functionUserUser.prototypeprototypeconstructor: User \ No newline at end of file diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/solution.md b/1-js/09-classes/02-class-inheritance/1-class-constructor-error/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/solution.md rename to 1-js/09-classes/02-class-inheritance/1-class-constructor-error/solution.md diff --git a/1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/task.md b/1-js/09-classes/02-class-inheritance/1-class-constructor-error/task.md similarity index 100% rename from 1-js/07-object-oriented-programming/10-class-inheritance/1-class-constructor-error/task.md rename to 1-js/09-classes/02-class-inheritance/1-class-constructor-error/task.md diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.md b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.md new file mode 100644 index 0000000000..dcb4ffe59a --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.md @@ -0,0 +1 @@ +[js src="solution.view/extended-clock.js"] diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/clock.js new file mode 100644 index 0000000000..d701c0caeb --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/clock.js @@ -0,0 +1,34 @@ +class Clock { + constructor({ template }) { + this.template = template; + } + + render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) mins = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this.template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + stop() { + clearInterval(this.timer); + } + + start() { + this.render(); + this.timer = setInterval(() => this.render(), 1000); + } +} 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 new file mode 100644 index 0000000000..be2053cfcf --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js @@ -0,0 +1,12 @@ +class ExtendedClock extends Clock { + constructor(options) { + super(options); + let { precision = 1000 } = options; + this.precision = precision; + } + + start() { + this.render(); + this.timer = setInterval(() => this.render(), this.precision); + } +}; diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/index.html b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/index.html new file mode 100644 index 0000000000..f76a436238 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/index.html @@ -0,0 +1,12 @@ + + + + + diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/clock.js new file mode 100644 index 0000000000..d701c0caeb --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/clock.js @@ -0,0 +1,34 @@ +class Clock { + constructor({ template }) { + this.template = template; + } + + render() { + let date = new Date(); + + let hours = date.getHours(); + if (hours < 10) hours = '0' + hours; + + let mins = date.getMinutes(); + if (mins < 10) mins = '0' + mins; + + let secs = date.getSeconds(); + if (secs < 10) secs = '0' + secs; + + let output = this.template + .replace('h', hours) + .replace('m', mins) + .replace('s', secs); + + console.log(output); + } + + stop() { + clearInterval(this.timer); + } + + start() { + this.render(); + this.timer = setInterval(() => this.render(), 1000); + } +} diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/index.html b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/index.html new file mode 100644 index 0000000000..c0609858b8 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/index.html @@ -0,0 +1,21 @@ + + + diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/task.md b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/task.md new file mode 100644 index 0000000000..bbc2c6a43c --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/task.md @@ -0,0 +1,15 @@ +importance: 5 + +--- + +# Extended clock + +We've got a `Clock` class. As of now, it prints the time every second. + + +[js src="source.view/clock.js"] + +Create a new class `ExtendedClock` that inherits from `Clock` and adds the parameter `precision` -- the number of `ms` between "ticks". Should be `1000` (1 second) by default. + +- Your code should be in the file `extended-clock.js` +- Don't modify the original `clock.js`. Extend it. diff --git a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg new file mode 100644 index 0000000000..63b5a18a19 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg @@ -0,0 +1 @@ +constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "White Rabbit"constructorconstructorextends \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/article.md b/1-js/09-classes/02-class-inheritance/article.md new file mode 100644 index 0000000000..464042d823 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/article.md @@ -0,0 +1,627 @@ + +# Class inheritance + +Class inheritance is a way for one class to extend another class. + +So we can create new functionality on top of the existing. + +## The "extends" keyword + +Let's say we have class `Animal`: + +```js +class Animal { + constructor(name) { + this.speed = 0; + this.name = name; + } + run(speed) { + this.speed = speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + stop() { + this.speed = 0; + alert(`${this.name} stands still.`); + } +} + +let animal = new Animal("My animal"); +``` + +Here's how we can represent `animal` object and `Animal` class graphically: + +![](rabbit-animal-independent-animal.svg) + +...And we would like to create another `class Rabbit`. + +As rabbits are animals, `Rabbit` class should be based on `Animal`, have access to animal methods, so that rabbits can do what "generic" animals can do. + +The syntax to extend another class is: `class Child extends Parent`. + +Let's create `class Rabbit` that inherits from `Animal`: + +```js +*!* +class Rabbit extends Animal { +*/!* + hide() { + alert(`${this.name} hides!`); + } +} + +let rabbit = new Rabbit("White Rabbit"); + +rabbit.run(5); // White Rabbit runs with speed 5. +rabbit.hide(); // White Rabbit hides! +``` + +Object of `Rabbit` class have access both to `Rabbit` methods, such as `rabbit.hide()`, and also to `Animal` methods, such as `rabbit.run()`. + +Internally, `extends` keyword works using the good old prototype mechanics. It sets `Rabbit.prototype.[[Prototype]]` to `Animal.prototype`. So, if a method is not found in `Rabbit.prototype`, JavaScript takes it from `Animal.prototype`. + +![](animal-rabbit-extends.svg) + +For instance, to find `rabbit.run` method, the engine checks (bottom-up on the picture): +1. The `rabbit` object (has no `run`). +2. Its prototype, that is `Rabbit.prototype` (has `hide`, but not `run`). +3. Its prototype, that is (due to `extends`) `Animal.prototype`, that finally has the `run` method. + +As we can recall from the chapter , JavaScript itself uses prototypal inheritance for built-in objects. E.g. `Date.prototype.[[Prototype]]` is `Object.prototype`. That's why dates have access to generic object methods. + +````smart header="Any expression is allowed after `extends`" +Class syntax allows to specify not just a class, but any expression after `extends`. + +For instance, a function call that generates the parent class: + +```js run +function f(phrase) { + return class { + sayHi() { alert(phrase); } + }; +} + +*!* +class User extends f("Hello") {} +*/!* + +new User().sayHi(); // Hello +``` +Here `class User` inherits from the result of `f("Hello")`. + +That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them. +```` + +## Overriding a method + +Now let's move forward and override a method. By default, all methods that are not specified in `class Rabbit` are taken directly "as is" from `class Animal`. + +But if we specify our own method in `Rabbit`, such as `stop()` then it will be used instead: + +```js +class Rabbit extends Animal { + stop() { + // ...now this will be used for rabbit.stop() + // instead of stop() from class Animal + } +} +``` + +Usually, however, we don't want to totally replace a parent method, but rather to build on top of it to tweak or extend its functionality. We do something in our method, but call the parent method before/after it or in the process. + +Classes provide `"super"` keyword for that. + +- `super.method(...)` to call a parent method. +- `super(...)` to call a parent constructor (inside our constructor only). + +For instance, let our rabbit autohide when stopped: + +```js run +class Animal { + + constructor(name) { + this.speed = 0; + this.name = name; + } + + run(speed) { + this.speed = speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + + stop() { + this.speed = 0; + alert(`${this.name} stands still.`); + } + +} + +class Rabbit extends Animal { + hide() { + alert(`${this.name} hides!`); + } + +*!* + stop() { + super.stop(); // call parent stop + this.hide(); // and then hide + } +*/!* +} + +let rabbit = new Rabbit("White Rabbit"); + +rabbit.run(5); // White Rabbit runs with speed 5. +rabbit.stop(); // White Rabbit stands still. White Rabbit hides! +``` + +Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the process. + +````smart header="Arrow functions have no `super`" +As was mentioned in the chapter , arrow functions do not have `super`. + +If accessed, it's taken from the outer function. For instance: + +```js +class Rabbit extends Animal { + stop() { + setTimeout(() => super.stop(), 1000); // call parent stop after 1sec + } +} +``` + +The `super` in the arrow function is the same as in `stop()`, so it works as intended. If we specified a "regular" function here, there would be an error: + +```js +// Unexpected super +setTimeout(function() { super.stop() }, 1000); +``` +```` + +## Overriding constructor + +With constructors it gets a little bit tricky. + +Until now, `Rabbit` did not have its own `constructor`. + +According to the [specification](https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation), if a class extends another class and has no `constructor`, then the following "empty" `constructor` is generated: + +```js +class Rabbit extends Animal { + // generated for extending classes without own constructors +*!* + constructor(...args) { + super(...args); + } +*/!* +} +``` + +As we can see, it basically calls the parent `constructor` passing it all the arguments. That happens if we don't write a constructor of our own. + +Now let's add a custom constructor to `Rabbit`. It will specify the `earLength` in addition to `name`: + +```js run +class Animal { + constructor(name) { + this.speed = 0; + this.name = name; + } + // ... +} + +class Rabbit extends Animal { + +*!* + constructor(name, earLength) { + this.speed = 0; + this.name = name; + this.earLength = earLength; + } +*/!* + + // ... +} + +*!* +// Doesn't work! +let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined. +*/!* +``` + +Whoops! We've got an error. Now we can't create rabbits. What went wrong? + +The short answer is: + +- **Constructors in inheriting classes must call `super(...)`, and (!) do it before using `this`.** + +...But why? What's going on here? Indeed, the requirement seems strange. + +Of course, there's an explanation. Let's get into details, so you'll really understand what's going on. + +In JavaScript, there's a distinction between a constructor function of an inheriting class (so-called "derived constructor") and other functions. A derived constructor has a special internal property `[[ConstructorKind]]:"derived"`. That's a special internal label. + +That label affects its behavior with `new`. + +- When a regular function is executed with `new`, it creates an empty object and assigns it to `this`. +- But when a derived constructor runs, it doesn't do this. It expects the parent constructor to do this job. + +So a derived constructor must call `super` in order to execute its parent (base) constructor, otherwise the object for `this` won't be created. And we'll get an error. + +For the `Rabbit` constructor to work, it needs to call `super()` before using `this`, like here: + +```js run +class Animal { + + constructor(name) { + this.speed = 0; + this.name = name; + } + + // ... +} + +class Rabbit extends Animal { + + constructor(name, earLength) { +*!* + super(name); +*/!* + this.earLength = earLength; + } + + // ... +} + +*!* +// now fine +let rabbit = new Rabbit("White Rabbit", 10); +alert(rabbit.name); // White Rabbit +alert(rabbit.earLength); // 10 +*/!* +``` + +### Overriding class fields: a tricky note + +```warn header="Advanced note" +This note assumes you have a certain experience with classes, maybe in other programming languages. + +It provides better insight into the language and also explains the behavior that might be a source of bugs (but not very often). + +If you find it difficult to understand, just go on, continue reading, then return to it some time later. +``` + +We can override not only methods, but also class fields. + +Although, there's a tricky behavior when we access an overridden field in parent constructor, quite different from most other programming languages. + +Consider this example: + +```js run +class Animal { + name = 'animal'; + + constructor() { + alert(this.name); // (*) + } +} + +class Rabbit extends Animal { + name = 'rabbit'; +} + +new Animal(); // animal +*!* +new Rabbit(); // animal +*/!* +``` + +Here, class `Rabbit` extends `Animal` and overrides the `name` field with its own value. + +There's no own constructor in `Rabbit`, so `Animal` constructor is called. + +What's interesting is that in both cases: `new Animal()` and `new Rabbit()`, the `alert` in the line `(*)` shows `animal`. + +**In other words, the parent constructor always uses its own field value, not the overridden one.** + +What's odd about it? + +If it's not clear yet, please compare with methods. + +Here's the same code, but instead of `this.name` field we call `this.showName()` method: + +```js run +class Animal { + showName() { // instead of this.name = 'animal' + alert('animal'); + } + + constructor() { + this.showName(); // instead of alert(this.name); + } +} + +class Rabbit extends Animal { + showName() { + alert('rabbit'); + } +} + +new Animal(); // animal +*!* +new Rabbit(); // rabbit +*/!* +``` + +Please note: now the output is different. + +And that's what we naturally expect. When the parent constructor is called in the derived class, it uses the overridden method. + +...But for class fields it's not so. As said, the parent constructor always uses the parent field. + +Why is there a difference? + +Well, the reason is the field initialization order. The class field is initialized: +- Before constructor for the base class (that doesn't extend anything), +- Immediately after `super()` for the derived class. + +In our case, `Rabbit` is the derived class. There's no `constructor()` in it. As said previously, that's the same as if there was an empty constructor with only `super(...args)`. + +So, `new Rabbit()` calls `super()`, thus executing the parent constructor, and (per the rule for derived classes) only after that its class fields are initialized. At the time of the parent constructor execution, there are no `Rabbit` class fields yet, that's why `Animal` fields are used. + +This subtle difference between fields and methods is specific to JavaScript. + +Luckily, this behavior only reveals itself if an overridden field is used in the parent constructor. Then it may be difficult to understand what's going on, so we're explaining it here. + +If it becomes a problem, one can fix it by using methods or getters/setters instead of fields. + +## Super: internals, [[HomeObject]] + +```warn header="Advanced information" +If you're reading the tutorial for the first time - this section may be skipped. + +It's about the internal mechanisms behind inheritance and `super`. +``` + +Let's get a little deeper under the hood of `super`. We'll see some interesting things along the way. + +First to say, from all that we've learned till now, it's impossible for `super` to work at all! + +Yeah, indeed, let's ask ourselves, how it should technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, the engine needs to get the `method` from the prototype of the current object. But how? + +The task may seem simple, but it isn't. The engine knows the current object `this`, so it could get the parent `method` as `this.__proto__.method`. Unfortunately, such a "naive" solution won't work. + +Let's demonstrate the problem. Without classes, using plain objects for the sake of simplicity. + +You may skip this part and go below to the `[[HomeObject]]` subsection if you don't want to know the details. That won't harm. Or read on if you're interested in understanding things in-depth. + +In the example below, `rabbit.__proto__ = animal`. Now let's try: in `rabbit.eat()` we'll call `animal.eat()`, using `this.__proto__`: + +```js run +let animal = { + name: "Animal", + eat() { + alert(`${this.name} eats.`); + } +}; + +let rabbit = { + __proto__: animal, + name: "Rabbit", + eat() { +*!* + // that's how super.eat() could presumably work + this.__proto__.eat.call(this); // (*) +*/!* + } +}; + +rabbit.eat(); // Rabbit eats. +``` + +At the line `(*)` we take `eat` from the prototype (`animal`) and call it in the context of the current object. Please note that `.call(this)` is important here, because a simple `this.__proto__.eat()` would execute parent `eat` in the context of the prototype, not the current object. + +And in the code above it actually works as intended: we have the correct `alert`. + +Now let's add one more object to the chain. We'll see how things break: + +```js run +let animal = { + name: "Animal", + eat() { + alert(`${this.name} eats.`); + } +}; + +let rabbit = { + __proto__: animal, + eat() { + // ...bounce around rabbit-style and call parent (animal) method + this.__proto__.eat.call(this); // (*) + } +}; + +let longEar = { + __proto__: rabbit, + eat() { + // ...do something with long ears and call parent (rabbit) method + this.__proto__.eat.call(this); // (**) + } +}; + +*!* +longEar.eat(); // Error: Maximum call stack size exceeded +*/!* +``` + +The code doesn't work anymore! We can see the error trying to call `longEar.eat()`. + +It may be not that obvious, but if we trace `longEar.eat()` call, then we can see why. In both lines `(*)` and `(**)` the value of `this` is the current object (`longEar`). That's essential: all object methods get the current object as `this`, not a prototype or something. + +So, in both lines `(*)` and `(**)` the value of `this.__proto__` is exactly the same: `rabbit`. They both call `rabbit.eat` without going up the chain in the endless loop. + +Here's the picture of what happens: + +![](this-super-loop.svg) + +1. Inside `longEar.eat()`, the line `(**)` calls `rabbit.eat` providing it with `this=longEar`. + ```js + // inside longEar.eat() we have this = longEar + this.__proto__.eat.call(this) // (**) + // becomes + longEar.__proto__.eat.call(this) + // that is + rabbit.eat.call(this); + ``` +2. Then in the line `(*)` of `rabbit.eat`, we'd like to pass the call even higher in the chain, but `this=longEar`, so `this.__proto__.eat` is again `rabbit.eat`! + + ```js + // inside rabbit.eat() we also have this = longEar + this.__proto__.eat.call(this) // (*) + // becomes + longEar.__proto__.eat.call(this) + // or (again) + rabbit.eat.call(this); + ``` + +3. ...So `rabbit.eat` calls itself in the endless loop, because it can't ascend any further. + +The problem can't be solved by using `this` alone. + +### `[[HomeObject]]` + +To provide the solution, JavaScript adds one more special internal property for functions: `[[HomeObject]]`. + +When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object. + +Then `super` uses it to resolve the parent prototype and its methods. + +Let's see how it works, first with plain objects: + +```js run +let animal = { + name: "Animal", + eat() { // animal.eat.[[HomeObject]] == animal + alert(`${this.name} eats.`); + } +}; + +let rabbit = { + __proto__: animal, + name: "Rabbit", + eat() { // rabbit.eat.[[HomeObject]] == rabbit + super.eat(); + } +}; + +let longEar = { + __proto__: rabbit, + name: "Long Ear", + eat() { // longEar.eat.[[HomeObject]] == longEar + super.eat(); + } +}; + +*!* +// works correctly +longEar.eat(); // Long Ear eats. +*/!* +``` + +It works as intended, due to `[[HomeObject]]` mechanics. A method, such as `longEar.eat`, knows its `[[HomeObject]]` and takes the parent method from its prototype. Without any use of `this`. + +### Methods are not "free" + +As we've known before, generally functions are "free", not bound to objects in JavaScript. So they can be copied between objects and called with another `this`. + +The very existence of `[[HomeObject]]` violates that principle, because methods remember their objects. `[[HomeObject]]` can't be changed, so this bond is forever. + +The only place in the language where `[[HomeObject]]` is used -- is `super`. So, if a method does not use `super`, then we can still consider it free and copy between objects. But with `super` things may go wrong. + +Here's the demo of a wrong `super` result after copying: + +```js run +let animal = { + sayHi() { + alert(`I'm an animal`); + } +}; + +// rabbit inherits from animal +let rabbit = { + __proto__: animal, + sayHi() { + super.sayHi(); + } +}; + +let plant = { + sayHi() { + alert("I'm a plant"); + } +}; + +// tree inherits from plant +let tree = { + __proto__: plant, +*!* + sayHi: rabbit.sayHi // (*) +*/!* +}; + +*!* +tree.sayHi(); // I'm an animal (?!?) +*/!* +``` + +A call to `tree.sayHi()` shows "I'm an animal". Definitely wrong. + +The reason is simple: +- In the line `(*)`, the method `tree.sayHi` was copied from `rabbit`. Maybe we just wanted to avoid code duplication? +- Its `[[HomeObject]]` is `rabbit`, as it was created in `rabbit`. There's no way to change `[[HomeObject]]`. +- The code of `tree.sayHi()` has `super.sayHi()` inside. It goes up from `rabbit` and takes the method from `animal`. + +Here's the diagram of what happens: + +![](super-homeobject-wrong.svg) + +### Methods, not function properties + +`[[HomeObject]]` is defined for methods both in classes and in plain objects. But for objects, methods must be specified exactly as `method()`, not as `"method: function()"`. + +The difference may be non-essential for us, but it's important for JavaScript. + +In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work: + +```js run +let animal = { + eat: function() { // intentionally writing like this instead of eat() {... + // ... + } +}; + +let rabbit = { + __proto__: animal, + eat: function() { + super.eat(); + } +}; + +*!* +rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) +*/!* +``` + +## Summary + +1. To extend a class: `class Child extends Parent`: + - That means `Child.prototype.__proto__` will be `Parent.prototype`, so methods are inherited. +2. When overriding a constructor: + - We must call parent constructor as `super()` in `Child` constructor before using `this`. +3. When overriding another method: + - We can use `super.method()` in a `Child` method to call `Parent` method. +4. Internals: + - Methods remember their class/object in the internal `[[HomeObject]]` property. That's how `super` resolves parent methods. + - So it's not safe to copy a method with `super` from one object to another. + +Also: +- Arrow functions don't have their own `this` or `super`, so they transparently fit into the surrounding context. diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg new file mode 100644 index 0000000000..5ea9bf29ea --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg @@ -0,0 +1 @@ +slice: function ...Array.prototypearrhasOwnProperty: function ...Object.prototype[1, 2, 3][[Prototype]][[Prototype]] \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg new file mode 100644 index 0000000000..72e47e34c2 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg @@ -0,0 +1 @@ +jump: functionRabbit.prototyperabbiteat: functionAnimal.prototypename: "White Rabbit"[[Prototype]][[Prototype]]Rabbit.prototype.__proto__ = Animal.prototype sets thistoString: function hasOwnProperty: function ...Object.prototype[[Prototype]][[Prototype]]null \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg new file mode 100644 index 0000000000..bced3d355e --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg @@ -0,0 +1 @@ +methods of RabbitRabbit.prototyperabbitmethods of AnimalAnimal.prototype[[Prototype]][[Prototype]]properties of rabbit \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg new file mode 100644 index 0000000000..f53fc92dee --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg @@ -0,0 +1 @@ + constructor: Animal run: function stop: functionAnimal.prototypeAnimalnew Animal[[Prototype]]prototypename: "My animal" \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg new file mode 100644 index 0000000000..2f30a3a901 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg @@ -0,0 +1 @@ + constructor: Rabbit hide: functionRabbit.prototypeRabbitnew Rabbit[[Prototype]]prototypename: "My rabbit" \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg new file mode 100644 index 0000000000..f6450ddc49 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg @@ -0,0 +1 @@ +sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/this-super-loop.svg b/1-js/09-classes/02-class-inheritance/this-super-loop.svg new file mode 100644 index 0000000000..4f5f45034a --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/this-super-loop.svg @@ -0,0 +1 @@ +rabbitlongEarrabbitlongEar \ No newline at end of file diff --git a/1-js/09-classes/03-static-properties-methods/3-class-extend-object/rabbit-extends-object.svg b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/rabbit-extends-object.svg new file mode 100644 index 0000000000..915ab9aa64 --- /dev/null +++ b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/rabbit-extends-object.svg @@ -0,0 +1 @@ +call: function bind: function ...Function.prototypeconstructorObjectRabbit[[Prototype]][[Prototype]]constructorcall: function bind: function ...Function.prototypeRabbit[[Prototype]]constructorclass Rabbitclass Rabbit extends Object \ No newline at end of file diff --git a/1-js/09-classes/03-static-properties-methods/3-class-extend-object/solution.md b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/solution.md new file mode 100644 index 0000000000..cb9829ce05 --- /dev/null +++ b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/solution.md @@ -0,0 +1,81 @@ +First, let's see why the latter code doesn't work. + +The reason becomes obvious if we try to run it. An inheriting class constructor must call `super()`. Otherwise `"this"` won't be "defined". + +So here's the fix: + +```js run +class Rabbit extends Object { + constructor(name) { +*!* + super(); // need to call the parent constructor when inheriting +*/!* + this.name = name; + } +} + +let rabbit = new Rabbit("Rab"); + +alert( rabbit.hasOwnProperty('name') ); // true +``` + +But that's not all yet. + +Even after the fix, there's still an important difference between `"class Rabbit extends Object"` and `class Rabbit`. + +As we know, the "extends" syntax sets up two prototypes: + +1. Between `"prototype"` of the constructor functions (for methods). +2. Between the constructor functions themselves (for static methods). + +In the case of `class Rabbit extends Object` it means: + +```js run +class Rabbit extends Object {} + +alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true +alert( Rabbit.__proto__ === Object ); // (2) true +``` + +So `Rabbit` now provides access to the static methods of `Object` via `Rabbit`, like this: + +```js run +class Rabbit extends Object {} + +*!* +// normally we call Object.getOwnPropertyNames +alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b +*/!* +``` + +But if we don't have `extends Object`, then `Rabbit.__proto__` is not set to `Object`. + +Here's the demo: + +```js run +class Rabbit {} + +alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true +alert( Rabbit.__proto__ === Object ); // (2) false (!) +alert( Rabbit.__proto__ === Function.prototype ); // as any function by default + +*!* +// error, no such function in Rabbit +alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error +*/!* +``` + +So `Rabbit` doesn't provide access to static methods of `Object` in that case. + +By the way, `Function.prototype` also has "generic" function methods, like `call`, `bind` etc. They are ultimately available in both cases, because for the built-in `Object` constructor, `Object.__proto__ === Function.prototype`. + +Here's the picture: + +![](rabbit-extends-object.svg) + +So, to put it short, there are two differences: + +| class Rabbit | class Rabbit extends Object | +|--------------|------------------------------| +| -- | needs to call `super()` in constructor | +| `Rabbit.__proto__ === Function.prototype` | `Rabbit.__proto__ === Object` | diff --git a/1-js/09-classes/03-static-properties-methods/3-class-extend-object/task.md b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/task.md new file mode 100644 index 0000000000..1d0f98a74e --- /dev/null +++ b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/task.md @@ -0,0 +1,42 @@ +importance: 3 + +--- + +# Class extends Object? + +As we know, all objects normally inherit from `Object.prototype` and get access to "generic" object methods like `hasOwnProperty` etc. + +For instance: + +```js run +class Rabbit { + constructor(name) { + this.name = name; + } +} + +let rabbit = new Rabbit("Rab"); + +*!* +// hasOwnProperty method is from Object.prototype +alert( rabbit.hasOwnProperty('name') ); // true +*/!* +``` + +But if we spell it out explicitly like `"class Rabbit extends Object"`, then the result would be different from a simple `"class Rabbit"`? + +What's the difference? + +Here's an example of such code (it doesn't work -- why? fix it?): + +```js +class Rabbit extends Object { + constructor(name) { + this.name = name; + } +} + +let rabbit = new Rabbit("Rab"); + +alert( rabbit.hasOwnProperty('name') ); // Error +``` diff --git a/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg new file mode 100644 index 0000000000..3e354b895d --- /dev/null +++ b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg @@ -0,0 +1 @@ +constructor: Animal run: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitrabbit[[Prototype]][[Prototype]][[Prototype]]prototypeprototypecomparename: "White Rabbit" \ No newline at end of file diff --git a/1-js/09-classes/03-static-properties-methods/article.md b/1-js/09-classes/03-static-properties-methods/article.md new file mode 100644 index 0000000000..4b493a5e8b --- /dev/null +++ b/1-js/09-classes/03-static-properties-methods/article.md @@ -0,0 +1,249 @@ + +# Static properties and methods + +We can also assign a method to the class as a whole. Such methods are called *static*. + +In a class declaration, they are prepended by `static` keyword, like this: + +```js run +class User { +*!* + static staticMethod() { +*/!* + alert(this === User); + } +} + +User.staticMethod(); // true +``` + +That actually does the same as assigning it as a property directly: + +```js run +class User { } + +User.staticMethod = function() { + alert(this === User); +}; + +User.staticMethod(); // true +``` + +The value of `this` in `User.staticMethod()` call is the class constructor `User` itself (the "object before dot" rule). + +Usually, static methods are used to implement functions that belong to the class as a whole, but not to any particular object of it. + +For instance, we have `Article` objects and need a function to compare them. + +A natural solution would be to add `Article.compare` static method: + +```js run +class Article { + constructor(title, date) { + this.title = title; + this.date = date; + } + +*!* + static compare(articleA, articleB) { + return articleA.date - articleB.date; + } +*/!* +} + +// usage +let articles = [ + new Article("HTML", new Date(2019, 1, 1)), + new Article("CSS", new Date(2019, 0, 1)), + new Article("JavaScript", new Date(2019, 11, 1)) +]; + +*!* +articles.sort(Article.compare); +*/!* + +alert( articles[0].title ); // CSS +``` + +Here `Article.compare` method stands "above" articles, as a means to compare them. It's not a method of an article, but rather of the whole class. + +Another example would be a so-called "factory" method. + +Let's say, we need multiple ways to create an article: + +1. Create by given parameters (`title`, `date` etc). +2. Create an empty article with today's date. +3. ...or else somehow. + +The first way can be implemented by the constructor. And for the second one we can make a static method of the class. + +Such as `Article.createTodays()` here: + +```js run +class Article { + constructor(title, date) { + this.title = title; + this.date = date; + } + +*!* + static createTodays() { + // remember, this = Article + return new this("Today's digest", new Date()); + } +*/!* +} + +let article = Article.createTodays(); + +alert( article.title ); // Today's digest +``` + +Now every time we need to create a today's digest, we can call `Article.createTodays()`. Once again, that's not a method of an article, but a method of the whole class. + +Static methods are also used in database-related classes to search/save/remove entries from the database, like this: + +```js +// assuming Article is a special class for managing articles +// static method to remove the article by id: +Article.remove({id: 12345}); +``` + +````warn header="Static methods aren't available for individual objects" +Static methods are callable on classes, not on individual objects. + +E.g. such code won't work: + +```js +// ... +article.createTodays(); /// Error: article.createTodays is not a function +``` +```` + +## Static properties + +[recent browser=Chrome] + +Static properties are also possible, they look like regular class properties, but prepended by `static`: + +```js run +class Article { + static publisher = "Ilya Kantor"; +} + +alert( Article.publisher ); // Ilya Kantor +``` + +That is the same as a direct assignment to `Article`: + +```js +Article.publisher = "Ilya Kantor"; +``` + +## Inheritance of static properties and methods [#statics-and-inheritance] + +Static properties and methods are inherited. + +For instance, `Animal.compare` and `Animal.planet` in the code below are inherited and accessible as `Rabbit.compare` and `Rabbit.planet`: + +```js run +class Animal { + static planet = "Earth"; + + constructor(name, speed) { + this.speed = speed; + this.name = name; + } + + run(speed = 0) { + this.speed += speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + +*!* + static compare(animalA, animalB) { + return animalA.speed - animalB.speed; + } +*/!* + +} + +// Inherit from Animal +class Rabbit extends Animal { + hide() { + alert(`${this.name} hides!`); + } +} + +let rabbits = [ + new Rabbit("White Rabbit", 10), + new Rabbit("Black Rabbit", 5) +]; + +*!* +rabbits.sort(Rabbit.compare); +*/!* + +rabbits[0].run(); // Black Rabbit runs with speed 5. + +alert(Rabbit.planet); // Earth +``` + +Now when we call `Rabbit.compare`, the inherited `Animal.compare` will be called. + +How does it work? Again, using prototypes. As you might have already guessed, `extends` gives `Rabbit` the `[[Prototype]]` reference to `Animal`. + +![](animal-rabbit-static.svg) + +So, `Rabbit extends Animal` creates two `[[Prototype]]` references: + +1. `Rabbit` function prototypally inherits from `Animal` function. +2. `Rabbit.prototype` prototypally inherits from `Animal.prototype`. + +As a result, inheritance works both for regular and static methods. + +Here, let's check that by code: + +```js run +class Animal {} +class Rabbit extends Animal {} + +// for statics +alert(Rabbit.__proto__ === Animal); // true + +// for regular methods +alert(Rabbit.prototype.__proto__ === Animal.prototype); // true +``` + +## Summary + +Static methods are used for the functionality that belongs to the class "as a whole". It doesn't relate to a concrete class instance. + +For example, a method for comparison `Article.compare(article1, article2)` or a factory method `Article.createTodays()`. + +They are labeled by the word `static` in class declaration. + +Static properties are used when we'd like to store class-level data, also not bound to an instance. + +The syntax is: + +```js +class MyClass { + static property = ...; + + static method() { + ... + } +} +``` + +Technically, static declaration is the same as assigning to the class itself: + +```js +MyClass.property = ... +MyClass.method = ... +``` + +Static properties and methods are inherited. + +For `class B extends A` the prototype of the class `B` itself points to `A`: `B.[[Prototype]] = A`. So if a field is not found in `B`, the search continues in `A`. 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 new file mode 100644 index 0000000000..91efb89eeb --- /dev/null +++ b/1-js/09-classes/04-private-protected-properties-methods/article.md @@ -0,0 +1,322 @@ + +# Private and protected properties and methods + +One of the most important principles of object oriented programming -- delimiting internal interface from the external one. + +That is "a must" practice in developing anything more complex than a "hello world" app. + +To understand this, let's break away from development and turn our eyes into the real world. + +Usually, devices that we're using are quite complex. But delimiting the internal interface from the external one allows to use them without problems. + +## A real-life example + +For instance, a coffee machine. Simple from outside: a button, a display, a few holes...And, surely, the result -- great coffee! :) + +![](coffee.jpg) + +But inside... (a picture from the repair manual) + +![](coffee-inside.jpg) + +A lot of details. But we can use it without knowing anything. + +Coffee machines are quite reliable, aren't they? We can use one for years, and only if something goes wrong -- bring it for repairs. + +The secret of reliability and simplicity of a coffee machine -- all details are well-tuned and *hidden* inside. + +If we remove the protective cover from the coffee machine, then using it will be much more complex (where to press?), and dangerous (it can electrocute). + +As we'll see, in programming objects are like coffee machines. + +But in order to hide inner details, we'll use not a protective cover, but rather special syntax of the language and conventions. + +## Internal and external interface + +In object-oriented programming, properties and methods are split into two groups: + +- *Internal interface* -- methods and properties, accessible from other methods of the class, but not from the outside. +- *External interface* -- methods and properties, accessible also from outside the class. + +If we continue the analogy with the coffee machine -- what's hidden inside: a boiler tube, heating element, and so on -- is its internal interface. + +An internal interface is used for the object to work, its details use each other. For instance, a boiler tube is attached to the heating element. + +But from the outside a coffee machine is closed by the protective cover, so that no one can reach those. Details are hidden and inaccessible. We can use its features via the external interface. + +So, all we need to use an object is to know its external interface. We may be completely unaware how it works inside, and that's great. + +That was a general introduction. + +In JavaScript, there are two types of object fields (properties and methods): + +- Public: accessible from anywhere. They comprise the external interface. Until now we were only using public properties and methods. +- Private: accessible only from inside the class. These are for the internal interface. + +In many other languages there also exist "protected" fields: accessible only from inside the class and those extending it (like private, but plus access from inheriting classes). They are also useful for the internal interface. They are in a sense more widespread than private ones, because we usually want inheriting classes to gain access to them. + +Protected fields are not implemented in JavaScript on the language level, but in practice they are very convenient, so they are emulated. + +Now we'll make a coffee machine in JavaScript with all these types of properties. A coffee machine has a lot of details, we won't model them to stay simple (though we could). + +## Protecting "waterAmount" + +Let's make a simple coffee machine class first: + +```js run +class CoffeeMachine { + waterAmount = 0; // the amount of water inside + + constructor(power) { + this.power = power; + alert( `Created a coffee-machine, power: ${power}` ); + } + +} + +// create the coffee machine +let coffeeMachine = new CoffeeMachine(100); + +// add water +coffeeMachine.waterAmount = 200; +``` + +Right now the properties `waterAmount` and `power` are public. We can easily get/set them from the outside to any value. + +Let's change `waterAmount` property to protected to have more control over it. For instance, we don't want anyone to set it below zero. + +**Protected properties are usually prefixed with an underscore `_`.** + +That is not enforced on the language level, but there's a well-known convention between programmers that such properties and methods should not be accessed from the outside. + +So our property will be called `_waterAmount`: + +```js run +class CoffeeMachine { + _waterAmount = 0; + + set waterAmount(value) { + if (value < 0) { + value = 0; + } + this._waterAmount = value; + } + + get waterAmount() { + return this._waterAmount; + } + + constructor(power) { + this._power = power; + } + +} + +// create the coffee machine +let coffeeMachine = new CoffeeMachine(100); + +// add water +coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10 +``` + +Now the access is under control, so setting the water amount below zero becomes impossible. + +## Read-only "power" + +For `power` property, let's make it read-only. It sometimes happens that a property must be set at creation time only, and then never modified. + +That's exactly the case for a coffee machine: power never changes. + +To do so, we only need to make getter, but not the setter: + +```js run +class CoffeeMachine { + // ... + + constructor(power) { + this._power = power; + } + + get power() { + return this._power; + } + +} + +// create the coffee machine +let coffeeMachine = new CoffeeMachine(100); + +alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W + +coffeeMachine.power = 25; // Error (no setter) +``` + +````smart header="Getter/setter functions" +Here we used getter/setter syntax. + +But most of the time `get.../set...` functions are preferred, like this: + +```js +class CoffeeMachine { + _waterAmount = 0; + + *!*setWaterAmount(value)*/!* { + if (value < 0) value = 0; + this._waterAmount = value; + } + + *!*getWaterAmount()*/!* { + return this._waterAmount; + } +} + +new CoffeeMachine().setWaterAmount(100); +``` + +That looks a bit longer, but functions are more flexible. They can accept multiple arguments (even if we don't need them right now). + +On the other hand, get/set syntax is shorter, so ultimately there's no strict rule, it's up to you to decide. +```` + +```smart header="Protected fields are inherited" +If we inherit `class MegaMachine extends CoffeeMachine`, then nothing prevents us from accessing `this._waterAmount` or `this._power` from the methods of the new class. + +So protected fields are naturally inheritable. Unlike private ones that we'll see below. +``` + +## Private "#waterLimit" + +[recent browser=none] + +There's a finished JavaScript proposal, almost in the standard, that provides language-level support for private properties and methods. + +Privates should start with `#`. They are only accessible from inside the class. + +For instance, here's a private `#waterLimit` property and the water-checking private method `#fixWaterAmount`: + +```js run +class CoffeeMachine { +*!* + #waterLimit = 200; +*/!* + +*!* + #fixWaterAmount(value) { + if (value < 0) return 0; + if (value > this.#waterLimit) return this.#waterLimit; + } +*/!* + + setWaterAmount(value) { + this.#waterLimit = this.#fixWaterAmount(value); + } + +} + +let coffeeMachine = new CoffeeMachine(); + +*!* +// can't access privates from outside of the class +coffeeMachine.#fixWaterAmount(123); // Error +coffeeMachine.#waterLimit = 1000; // Error +*/!* +``` + +On the language level, `#` is a special sign that the field is private. We can't access it from outside or from inheriting classes. + +Private fields do not conflict with public ones. We can have both private `#waterAmount` and public `waterAmount` fields at the same time. + +For instance, let's make `waterAmount` an accessor for `#waterAmount`: + +```js run +class CoffeeMachine { + + #waterAmount = 0; + + get waterAmount() { + return this.#waterAmount; + } + + set waterAmount(value) { + if (value < 0) value = 0; + this.#waterAmount = value; + } +} + +let machine = new CoffeeMachine(); + +machine.waterAmount = 100; +alert(machine.#waterAmount); // Error +``` + +Unlike protected ones, private fields are enforced by the language itself. That's a good thing. + +But if we inherit from `CoffeeMachine`, then we'll have no direct access to `#waterAmount`. We'll need to rely on `waterAmount` getter/setter: + +```js +class MegaCoffeeMachine extends CoffeeMachine { + method() { +*!* + alert( this.#waterAmount ); // Error: can only access from CoffeeMachine +*/!* + } +} +``` + +In many scenarios such limitation is too severe. If we extend a `CoffeeMachine`, we may have legitimate reasons to access its internals. That's why protected fields are used more often, even though they are not supported by the language syntax. + +````warn header="Private fields are not available as this[name]" +Private fields are special. + +As we know, usually we can access fields using `this[name]`: + +```js +class User { + ... + sayHi() { + let fieldName = "name"; + alert(`Hello, ${*!*this[fieldName]*/!*}`); + } +} +``` + +With private fields that's impossible: `this['#name']` doesn't work. That's a syntax limitation to ensure privacy. +```` + +## Summary + +In terms of OOP, delimiting of the internal interface from the external one is called [encapsulation](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)). + +It gives the following benefits: + +Protection for users, so that they don't shoot themselves in the foot +: Imagine, there's a team of developers using a coffee machine. It was made by the "Best CoffeeMachine" company, and works fine, but a protective cover was removed. So the internal interface is exposed. + + All developers are civilized -- they use the coffee machine as intended. But one of them, John, decided that he's the smartest one, and made some tweaks in the coffee machine internals. So the coffee machine failed two days later. + + That's surely not John's fault, but rather the person who removed the protective cover and let John do his manipulations. + + The same in programming. If a user of a class will change things not intended to be changed from the outside -- the consequences are unpredictable. + +Supportable +: The situation in programming is more complex than with a real-life coffee machine, because we don't just buy it once. The code constantly undergoes development and improvement. + + **If we strictly delimit the internal interface, then the developer of the class can freely change its internal properties and methods, even without informing the users.** + + If you're a developer of such class, it's great to know that private methods can be safely renamed, their parameters can be changed, and even removed, because no external code depends on them. + + For users, when a new version comes out, it may be a total overhaul internally, but still simple to upgrade if the external interface is the same. + +Hiding complexity +: People adore using things that are simple. At least from outside. What's inside is a different thing. + + Programmers are not an exception. + + **It's always convenient when implementation details are hidden, and a simple, well-documented external interface is available.** + +To hide an internal interface we use either protected or private properties: + +- Protected fields start with `_`. That's a well-known convention, not enforced at the language level. Programmers should only access a field starting with `_` from its class and classes inheriting from it. +- Private fields start with `#`. JavaScript makes sure we can only access those from inside the class. + +Right now, private fields are not well-supported among browsers, but can be polyfilled. diff --git a/1-js/09-classes/04-private-protected-properties-methods/coffee-inside.jpg b/1-js/09-classes/04-private-protected-properties-methods/coffee-inside.jpg new file mode 100644 index 0000000000..60f84664d1 Binary files /dev/null and b/1-js/09-classes/04-private-protected-properties-methods/coffee-inside.jpg differ diff --git a/1-js/09-classes/04-private-protected-properties-methods/coffee.jpg b/1-js/09-classes/04-private-protected-properties-methods/coffee.jpg new file mode 100644 index 0000000000..ee26e1c064 Binary files /dev/null and b/1-js/09-classes/04-private-protected-properties-methods/coffee.jpg differ diff --git a/1-js/09-classes/05-extend-natives/article.md b/1-js/09-classes/05-extend-natives/article.md new file mode 100644 index 0000000000..28b4c6eb64 --- /dev/null +++ b/1-js/09-classes/05-extend-natives/article.md @@ -0,0 +1,89 @@ + +# Extending built-in classes + +Built-in classes like Array, Map and others are extendable also. + +For instance, here `PowerArray` inherits from the native `Array`: + +```js run +// add one more method to it (can do more) +class PowerArray extends Array { + isEmpty() { + return this.length === 0; + } +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +let filteredArr = arr.filter(item => item >= 10); +alert(filteredArr); // 10, 50 +alert(filteredArr.isEmpty()); // false +``` + +Please note a very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type `PowerArray`. Their internal implementation uses the object's `constructor` property for that. + +In the example above, +```js +arr.constructor === PowerArray +``` + +When `arr.filter()` is called, it internally creates the new array of results using exactly `arr.constructor`, not basic `Array`. That's actually very cool, because we can keep using `PowerArray` methods further on the result. + +Even more, we can customize that behavior. + +We can add a special static getter `Symbol.species` to the class. If it exists, it should return the constructor that JavaScript will use internally to create new entities in `map`, `filter` and so on. + +If we'd like built-in methods like `map` or `filter` to return regular arrays, we can return `Array` in `Symbol.species`, like here: + +```js run +class PowerArray extends Array { + isEmpty() { + return this.length === 0; + } + +*!* + // built-in methods will use this as the constructor + static get [Symbol.species]() { + return Array; + } +*/!* +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +// filter creates new array using arr.constructor[Symbol.species] as constructor +let filteredArr = arr.filter(item => item >= 10); + +*!* +// filteredArr is not PowerArray, but Array +*/!* +alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function +``` + +As you can see, now `.filter` returns `Array`. So the extended functionality is not passed any further. + +```smart header="Other collections work similarly" +Other collections, such as `Map` and `Set`, work alike. They also use `Symbol.species`. +``` + +## No static inheritance in built-ins + +Built-in objects have their own static methods, for instance `Object.keys`, `Array.isArray` etc. + +As we already know, native classes extend each other. For instance, `Array` extends `Object`. + +Normally, when one class extends another, both static and non-static methods are inherited. That was thoroughly explained in the article [](info:static-properties-methods#statics-and-inheritance). + +But built-in classes are an exception. They don't inherit statics from each other. + +For example, both `Array` and `Date` inherit from `Object`, so their instances have methods from `Object.prototype`. But `Array.[[Prototype]]` does not reference `Object`, so there's no, for instance, `Array.keys()` (or `Date.keys()`) static method. + +Here's the picture structure for `Date` and `Object`: + +![](object-date-inheritance.svg) + +As you can see, there's no link between `Date` and `Object`. They are independent, only `Date.prototype` inherits from `Object.prototype`. + +That's an important difference of inheritance between built-in objects compared to what we get with `extends`. diff --git a/1-js/09-classes/05-extend-natives/object-date-inheritance.svg b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg new file mode 100644 index 0000000000..be47d7fd96 --- /dev/null +++ b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg @@ -0,0 +1 @@ +constructor: Object toString: function hasOwnProperty: function ...Object.prototypeconstructor: Date toString: function getDate: function ...Date.prototypeObjectDatenew Date()[[Prototype]][[Prototype]]prototypeprototypedefineProperty keys ...now parse ...1 Jan 2019 \ No newline at end of file diff --git a/1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/solution.md b/1-js/09-classes/06-instanceof/1-strange-instanceof/solution.md similarity index 100% rename from 1-js/07-object-oriented-programming/11-instanceof/1-strange-instanceof/solution.md rename to 1-js/09-classes/06-instanceof/1-strange-instanceof/solution.md diff --git a/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md b/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md new file mode 100644 index 0000000000..5b8dc7de3c --- /dev/null +++ b/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md @@ -0,0 +1,20 @@ +importance: 5 + +--- + +# Strange instanceof + +In the code below, why does `instanceof` return `true`? We can easily see that `a` is not created by `B()`. + +```js run +function A() {} +function B() {} + +A.prototype = B.prototype = {}; + +let a = new A(); + +*!* +alert( a instanceof B ); // true +*/!* +``` diff --git a/1-js/09-classes/06-instanceof/article.md b/1-js/09-classes/06-instanceof/article.md new file mode 100644 index 0000000000..f9db989ca9 --- /dev/null +++ b/1-js/09-classes/06-instanceof/article.md @@ -0,0 +1,218 @@ +# Class checking: "instanceof" + +The `instanceof` operator allows to check whether an object belongs to a certain class. It also takes inheritance into account. + +Such a check may be necessary in many cases. For example, it can be used for building a *polymorphic* function, the one that treats arguments differently depending on their type. + +## The instanceof operator [#ref-instanceof] + +The syntax is: +```js +obj instanceof Class +``` + +It returns `true` if `obj` belongs to the `Class` or a class inheriting from it. + +For instance: + +```js run +class Rabbit {} +let rabbit = new Rabbit(); + +// is it an object of Rabbit class? +*!* +alert( rabbit instanceof Rabbit ); // true +*/!* +``` + +It also works with constructor functions: + +```js run +*!* +// instead of class +function Rabbit() {} +*/!* + +alert( new Rabbit() instanceof Rabbit ); // true +``` + +...And with built-in classes like `Array`: + +```js run +let arr = [1, 2, 3]; +alert( arr instanceof Array ); // true +alert( arr instanceof Object ); // true +``` + +Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypically inherits from `Object`. + +Normally, `instanceof` examines the prototype chain for the check. We can also set a custom logic in the static method `Symbol.hasInstance`. + +The algorithm of `obj instanceof Class` works roughly as follows: + +1. If there's a static method `Symbol.hasInstance`, then just call it: `Class[Symbol.hasInstance](obj)`. It should return either `true` or `false`, and we're done. That's how we can customize the behavior of `instanceof`. + + For example: + + ```js run + // setup instanceOf check that assumes that + // anything with canEat property is an animal + class Animal { + static [Symbol.hasInstance](obj) { + if (obj.canEat) return true; + } + } + + let obj = { canEat: true }; + + alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called + ``` + +2. Most classes do not have `Symbol.hasInstance`. In that case, the standard logic is used: `obj instanceOf Class` checks whether `Class.prototype` is equal to one of the prototypes in the `obj` prototype chain. + + In other words, compare one after another: + ```js + obj.__proto__ === Class.prototype? + obj.__proto__.__proto__ === Class.prototype? + obj.__proto__.__proto__.__proto__ === Class.prototype? + ... + // if any answer is true, return true + // otherwise, if we reached the end of the chain, return false + ``` + + In the example above `rabbit.__proto__ === Rabbit.prototype`, so that gives the answer immediately. + + In the case of an inheritance, the match will be at the second step: + + ```js run + class Animal {} + class Rabbit extends Animal {} + + let rabbit = new Rabbit(); + *!* + alert(rabbit instanceof Animal); // true + */!* + + // rabbit.__proto__ === Animal.prototype (no match) + *!* + // rabbit.__proto__.__proto__ === Animal.prototype (match!) + */!* + ``` + +Here's the illustration of what `rabbit instanceof Animal` compares with `Animal.prototype`: + +![](instanceof.svg) + +By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), that returns `true` if `objA` is somewhere in the chain of prototypes for `objB`. So the test of `obj instanceof Class` can be rephrased as `Class.prototype.isPrototypeOf(obj)`. + +It's funny, but the `Class` constructor itself does not participate in the check! Only the chain of prototypes and `Class.prototype` matters. + +That can lead to interesting consequences when a `prototype` property is changed after the object is created. + +Like here: + +```js run +function Rabbit() {} +let rabbit = new Rabbit(); + +// changed the prototype +Rabbit.prototype = {}; + +// ...not a rabbit any more! +*!* +alert( rabbit instanceof Rabbit ); // false +*/!* +``` + +## Bonus: Object.prototype.toString for the type + +We already know that plain objects are converted to string as `[object Object]`: + +```js run +let obj = {}; + +alert(obj); // [object Object] +alert(obj.toString()); // the same +``` + +That's their implementation of `toString`. But there's a hidden feature that makes `toString` actually much more powerful than that. We can use it as an extended `typeof` and an alternative for `instanceof`. + +Sounds strange? Indeed. Let's demystify. + +By [specification](https://tc39.github.io/ecma262/#sec-object.prototype.tostring), the built-in `toString` can be extracted from the object and executed in the context of any other value. And its result depends on that value. + +- For a number, it will be `[object Number]` +- For a boolean, it will be `[object Boolean]` +- For `null`: `[object Null]` +- For `undefined`: `[object Undefined]` +- For arrays: `[object Array]` +- ...etc (customizable). + +Let's demonstrate: + +```js run +// copy toString method into a variable for convenience +let objectToString = Object.prototype.toString; + +// what type is this? +let arr = []; + +alert( objectToString.call(arr) ); // [object *!*Array*/!*] +``` + +Here we used [call](mdn:js/function/call) as described in the chapter [](info:call-apply-decorators) to execute the function `objectToString` in the context `this=arr`. + +Internally, the `toString` algorithm examines `this` and returns the corresponding result. More examples: + +```js run +let s = Object.prototype.toString; + +alert( s.call(123) ); // [object Number] +alert( s.call(null) ); // [object Null] +alert( s.call(alert) ); // [object Function] +``` + +### Symbol.toStringTag + +The behavior of Object `toString` can be customized using a special object property `Symbol.toStringTag`. + +For instance: + +```js run +let user = { + [Symbol.toStringTag]: "User" +}; + +alert( {}.toString.call(user) ); // [object User] +``` + +For most environment-specific objects, there is such a property. Here are some browser specific examples: + +```js run +// toStringTag for the environment-specific object and class: +alert( window[Symbol.toStringTag]); // Window +alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest + +alert( {}.toString.call(window) ); // [object Window] +alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] +``` + +As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped into `[object ...]`. + +At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized. + +We can use `{}.toString.call` instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check. + +## Summary + +Let's summarize the type-checking methods that we know: + +| | works for | returns | +|---------------|-------------|---------------| +| `typeof` | primitives | string | +| `{}.toString` | primitives, built-in objects, objects with `Symbol.toStringTag` | string | +| `instanceof` | objects | true/false | + +As we can see, `{}.toString` is technically a "more advanced" `typeof`. + +And `instanceof` operator really shines when we are working with a class hierarchy and want to check for the class taking into account inheritance. diff --git a/1-js/09-classes/06-instanceof/instanceof.svg b/1-js/09-classes/06-instanceof/instanceof.svg new file mode 100644 index 0000000000..d63b03a8a2 --- /dev/null +++ b/1-js/09-classes/06-instanceof/instanceof.svg @@ -0,0 +1 @@ +Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file diff --git a/1-js/09-classes/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md new file mode 100644 index 0000000000..526b832efa --- /dev/null +++ b/1-js/09-classes/07-mixins/article.md @@ -0,0 +1,208 @@ +# Mixins + +In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]` for an object. And a class may extend only one other class. + +But sometimes that feels limiting. For instance, we have a class `StreetSweeper` and a class `Bicycle`, and want to make their mix: a `StreetSweepingBicycle`. + +Or we have a class `User` and a class `EventEmitter` that implements event generation, and we'd like to add the functionality of `EventEmitter` to `User`, so that our users can emit events. + +There's a concept that can help here, called "mixins". + +As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class containing methods that can be used by other classes without a need to inherit from it. + +In other words, a *mixin* provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes. + +## A mixin example + +The simplest way to implement a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class. + +For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`: + +```js run +*!* +// mixin +*/!* +let sayHiMixin = { + sayHi() { + alert(`Hello ${this.name}`); + }, + sayBye() { + alert(`Bye ${this.name}`); + } +}; + +*!* +// usage: +*/!* +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hello Dude! +``` + +There's no inheritance, but a simple method copying. So `User` may inherit from another class and also include the mixin to "mix-in" the additional methods, like this: + +```js +class User extends Person { + // ... +} + +Object.assign(User.prototype, sayHiMixin); +``` + +Mixins can make use of inheritance inside themselves. + +For instance, here `sayHiMixin` inherits from `sayMixin`: + +```js run +let sayMixin = { + say(phrase) { + alert(phrase); + } +}; + +let sayHiMixin = { + __proto__: sayMixin, // (or we could use Object.setPrototypeOf to set the prototype here) + + sayHi() { + *!* + // call parent method + */!* + super.say(`Hello ${this.name}`); // (*) + }, + sayBye() { + super.say(`Bye ${this.name}`); // (*) + } +}; + +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hello Dude! +``` + +Please note that the call to the parent method `super.say()` from `sayHiMixin` (at lines labelled with `(*)`) looks for the method in the prototype of that mixin, not the class. + +Here's the diagram (see the right part): + +![](mixin-inheritance.svg) + +That's because methods `sayHi` and `sayBye` were initially created in `sayHiMixin`. So even though they got copied, their `[[HomeObject]]` internal property references `sayHiMixin`, as shown in the picture above. + +As `super` looks for parent methods in `[[HomeObject]].[[Prototype]]`, that means it searches `sayHiMixin.[[Prototype]]`. + +## EventMixin + +Now let's make a mixin for real life. + +An important feature of many browser objects (for instance) is that they can generate events. Events are a great way to "broadcast information" to anyone who wants it. So let's make a mixin that allows us to easily add event-related functions to any class/object. + +- The mixin will provide a method `.trigger(name, [...data])` to "generate an event" when something important happens to it. The `name` argument is a name of the event, optionally followed by additional arguments with event data. +- Also the method `.on(name, handler)` that adds `handler` function as the listener to events with the given name. It will be called when an event with the given `name` triggers, and get the arguments from the `.trigger` call. +- ...And the method `.off(name, handler)` that removes the `handler` listener. + +After adding the mixin, an object `user` will be able to generate an event `"login"` when the visitor logs in. And another object, say, `calendar` may want to listen for such events to load the calendar for the logged-in person. + +Or, a `menu` can generate the event `"select"` when a menu item is selected, and other objects may assign handlers to react on that event. And so on. + +Here's the code: + +```js run +let eventMixin = { + /** + * Subscribe to event, usage: + * menu.on('select', function(item) { ... } + */ + on(eventName, handler) { + if (!this._eventHandlers) this._eventHandlers = {}; + if (!this._eventHandlers[eventName]) { + this._eventHandlers[eventName] = []; + } + this._eventHandlers[eventName].push(handler); + }, + + /** + * Cancel the subscription, usage: + * menu.off('select', handler) + */ + off(eventName, handler) { + let handlers = this._eventHandlers?.[eventName]; + if (!handlers) return; + for (let i = 0; i < handlers.length; i++) { + if (handlers[i] === handler) { + handlers.splice(i--, 1); + } + } + }, + + /** + * Generate an event with the given name and data + * this.trigger('select', data1, data2); + */ + trigger(eventName, ...args) { + if (!this._eventHandlers?.[eventName]) { + return; // no handlers for that event name + } + + // call the handlers + this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); + } +}; +``` + + +- `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name occurs. Technically, there's an `_eventHandlers` property that stores an array of handlers for each event name, and it just adds it to the list. +- `.off(eventName, handler)` -- removes the function from the handlers list. +- `.trigger(eventName, ...args)` -- generates the event: all handlers from `_eventHandlers[eventName]` are called, with a list of arguments `...args`. + +Usage: + +```js run +// Make a class +class Menu { + choose(value) { + this.trigger("select", value); + } +} +// Add the mixin with event-related methods +Object.assign(Menu.prototype, eventMixin); + +let menu = new Menu(); + +// add a handler, to be called on selection: +*!* +menu.on("select", value => alert(`Value selected: ${value}`)); +*/!* + +// triggers the event => the handler above runs and shows: +// Value selected: 123 +menu.choose("123"); +``` + +Now, if we'd like any code to react to a menu selection, we can listen for it with `menu.on(...)`. + +And `eventMixin` mixin makes it easy to add such behavior to as many classes as we'd like, without interfering with the inheritance chain. + +## Summary + +*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes. + +Some other languages allow multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying methods into prototype. + +We can use mixins as a way to augment a class by adding multiple behaviors, like event-handling as we have seen above. + +Mixins may become a point of conflict if they accidentally overwrite existing class methods. So generally one should think well about the naming methods of a mixin, to minimize the probability of that happening. diff --git a/1-js/09-classes/07-mixins/head.html b/1-js/09-classes/07-mixins/head.html new file mode 100644 index 0000000000..20e3a63547 --- /dev/null +++ b/1-js/09-classes/07-mixins/head.html @@ -0,0 +1,43 @@ + diff --git a/1-js/09-classes/07-mixins/mixin-inheritance.svg b/1-js/09-classes/07-mixins/mixin-inheritance.svg new file mode 100644 index 0000000000..1fdc223936 --- /dev/null +++ b/1-js/09-classes/07-mixins/mixin-inheritance.svg @@ -0,0 +1 @@ +sayHi: function sayBye: functionsayHiMixinsay: functionsayMixin[[Prototype]]constructor: User sayHi: function sayBye: functionUser.prototype[[Prototype]]name: ...user[[HomeObject] \ No newline at end of file diff --git a/1-js/09-classes/index.md b/1-js/09-classes/index.md new file mode 100644 index 0000000000..87846ef6ba --- /dev/null +++ b/1-js/09-classes/index.md @@ -0,0 +1 @@ +# Classes diff --git a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/solution.md b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/solution.md new file mode 100644 index 0000000000..ec0dabc9a6 --- /dev/null +++ b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/solution.md @@ -0,0 +1,47 @@ +The difference becomes obvious when we look at the code inside a function. + +The behavior is different if there's a "jump out" of `try...catch`. + +For instance, when there's a `return` inside `try...catch`. The `finally` clause works in case of *any* exit from `try...catch`, even via the `return` statement: right after `try...catch` is done, but before the calling code gets the control. + +```js run +function f() { + try { + alert('start'); +*!* + return "result"; +*/!* + } catch (err) { + /// ... + } finally { + alert('cleanup!'); + } +} + +f(); // cleanup! +``` + +...Or when there's a `throw`, like here: + +```js run +function f() { + try { + alert('start'); + throw new Error("an error"); + } catch (err) { + // ... + if("can't handle the error") { +*!* + throw err; +*/!* + } + + } finally { + alert('cleanup!') + } +} + +f(); // cleanup! +``` + +It's `finally` that guarantees the cleanup here. If we just put the code at the end of `f`, it wouldn't run in these situations. diff --git a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md new file mode 100644 index 0000000000..b6dc813261 --- /dev/null +++ b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md @@ -0,0 +1,38 @@ +importance: 5 + +--- + +# Finally or just the code? + +Compare the two code fragments. + +1. The first one uses `finally` to execute the code after `try...catch`: + + ```js + try { + work work + } catch (err) { + handle errors + } finally { + *!* + cleanup the working space + */!* + } + ``` +2. The second fragment puts the cleaning right after `try...catch`: + + ```js + try { + work work + } catch (err) { + handle errors + } + + *!* + cleanup the working space + */!* + ``` + +We definitely need the cleanup after the work, doesn't matter if there was an error or not. + +Is there an advantage here in using `finally` or both code fragments are equal? If there is such an advantage, then give an example when it matters. diff --git a/1-js/10-error-handling/1-try-catch/article.md b/1-js/10-error-handling/1-try-catch/article.md new file mode 100644 index 0000000000..bf548373ad --- /dev/null +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -0,0 +1,675 @@ +# Error handling, "try...catch" + +No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response, and for a thousand other reasons. + +Usually, a script "dies" (immediately stops) in case of an error, printing it to console. + +But there's a syntax construct `try...catch` that allows us to "catch" errors so the script can, instead of dying, do something more reasonable. + +## The "try...catch" syntax + +The `try...catch` construct has two main blocks: `try`, and then `catch`: + +```js +try { + + // code... + +} catch (err) { + + // error handling + +} +``` + +It works like this: + +1. First, the code in `try {...}` is executed. +2. If there were no errors, then `catch (err)` is ignored: the execution reaches the end of `try` and goes on, skipping `catch`. +3. If an error occurs, then the `try` execution is stopped, and control flows to the beginning of `catch (err)`. The `err` variable (we can use any name for it) will contain an error object with details about what happened. + +![](try-catch-flow.svg) + +So, an error inside the `try {...}` block does not kill the script -- we have a chance to handle it in `catch`. + +Let's look at some examples. + +- An errorless example: shows `alert` `(1)` and `(2)`: + + ```js run + try { + + alert('Start of try runs'); // *!*(1) <--*/!* + + // ...no errors here + + alert('End of try runs'); // *!*(2) <--*/!* + + } catch (err) { + + alert('Catch is ignored, because there are no errors'); // (3) + + } + ``` +- An example with an error: shows `(1)` and `(3)`: + + ```js run + try { + + alert('Start of try runs'); // *!*(1) <--*/!* + + *!* + lalala; // error, variable is not defined! + */!* + + alert('End of try (never reached)'); // (2) + + } catch (err) { + + alert(`Error has occurred!`); // *!*(3) <--*/!* + + } + ``` + + +````warn header="`try...catch` only works for runtime errors" +For `try...catch` to work, the code must be runnable. In other words, it should be valid JavaScript. + +It won't work if the code is syntactically wrong, for instance it has unmatched curly braces: + +```js run +try { + {{{{{{{{{{{{ +} catch (err) { + alert("The engine can't understand this code, it's invalid"); +} +``` + +The JavaScript engine first reads the code, and then runs it. The errors that occur on the reading phase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code. + +So, `try...catch` can only handle errors that occur in valid code. Such errors are called "runtime errors" or, sometimes, "exceptions". +```` + + +````warn header="`try...catch` works synchronously" +If an exception happens in "scheduled" code, like in `setTimeout`, then `try...catch` won't catch it: + +```js run +try { + setTimeout(function() { + noSuchVariable; // script will die here + }, 1000); +} catch (err) { + alert( "won't work" ); +} +``` + +That's because the function itself is executed later, when the engine has already left the `try...catch` construct. + +To catch an exception inside a scheduled function, `try...catch` must be inside that function: +```js run +setTimeout(function() { + try { + noSuchVariable; // try...catch handles the error! + } catch { + alert( "error is caught here!" ); + } +}, 1000); +``` +```` + +## Error object + +When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to `catch`: + +```js +try { + // ... +} catch (err) { // <-- the "error object", could use another word instead of err + // ... +} +``` + +For all built-in errors, the error object has two main properties: + +`name` +: Error name. For instance, for an undefined variable that's `"ReferenceError"`. + +`message` +: Textual message about error details. + +There are other non-standard properties available in most environments. One of most widely used and supported is: + +`stack` +: Current call stack: a string with information about the sequence of nested calls that led to the error. Used for debugging purposes. + +For instance: + +```js run untrusted +try { +*!* + lalala; // error, variable is not defined! +*/!* +} catch (err) { + alert(err.name); // ReferenceError + alert(err.message); // lalala is not defined + alert(err.stack); // ReferenceError: lalala is not defined at (...call stack) + + // Can also show an error as a whole + // The error is converted to string as "name: message" + alert(err); // ReferenceError: lalala is not defined +} +``` + +## Optional "catch" binding + +[recent browser=new] + +If we don't need error details, `catch` may omit it: + +```js +try { + // ... +} catch { // <-- without (err) + // ... +} +``` + +## Using "try...catch" + +Let's explore a real-life use case of `try...catch`. + +As we already know, JavaScript supports the [JSON.parse(str)](mdn:js/JSON/parse) method to read JSON-encoded values. + +Usually it's used to decode data received over the network, from the server or another source. + +We receive it and call `JSON.parse` like this: + +```js run +let json = '{"name":"John", "age": 30}'; // data from the server + +*!* +let user = JSON.parse(json); // convert the text representation to JS object +*/!* + +// now user is an object with properties from the string +alert( user.name ); // John +alert( user.age ); // 30 +``` + +You can find more detailed information about JSON in the chapter. + +**If `json` is malformed, `JSON.parse` generates an error, so the script "dies".** + +Should we be satisfied with that? Of course not! + +This way, if something's wrong with the data, the visitor will never know that (unless they open the developer console). And people really don't like when something "just dies" without any error message. + +Let's use `try...catch` to handle the error: + +```js run +let json = "{ bad json }"; + +try { + +*!* + let user = JSON.parse(json); // <-- when an error occurs... +*/!* + alert( user.name ); // doesn't work + +} catch (err) { +*!* + // ...the execution jumps here + alert( "Our apologies, the data has errors, we'll try to request it one more time." ); + alert( err.name ); + alert( err.message ); +*/!* +} +``` + +Here we use the `catch` block only to show the message, but we can do much more: send a new network request, suggest an alternative to the visitor, send information about the error to a logging facility, ... . All much better than just dying. + +## Throwing our own errors + +What if `json` is syntactically correct, but doesn't have a required `name` property? + +Like this: + +```js run +let json = '{ "age": 30 }'; // incomplete data + +try { + + let user = JSON.parse(json); // <-- no errors +*!* + alert( user.name ); // no name! +*/!* + +} catch (err) { + alert( "doesn't execute" ); +} +``` + +Here `JSON.parse` runs normally, but the absence of `name` is actually an error for us. + +To unify error handling, we'll use the `throw` operator. + +### "Throw" operator + +The `throw` operator generates an error. + +The syntax is: + +```js +throw +``` + +Technically, we can use anything as an error object. That may be even a primitive, like a number or a string, but it's better to use objects, preferably with `name` and `message` properties (to stay somewhat compatible with built-in errors). + +JavaScript has many built-in constructors for standard errors: `Error`, `SyntaxError`, `ReferenceError`, `TypeError` and others. We can use them to create error objects as well. + +Their syntax is: + +```js +let error = new Error(message); +// or +let error = new SyntaxError(message); +let error = new ReferenceError(message); +// ... +``` + +For built-in errors (not for any objects, just for errors), the `name` property is exactly the name of the constructor. And `message` is taken from the argument. + +For instance: + +```js run +let error = new Error("Things happen o_O"); + +alert(error.name); // Error +alert(error.message); // Things happen o_O +``` + +Let's see what kind of error `JSON.parse` generates: + +```js run +try { + JSON.parse("{ bad json o_O }"); +} catch (err) { +*!* + alert(err.name); // SyntaxError +*/!* + alert(err.message); // Unexpected token b in JSON at position 2 +} +``` + +As we can see, that's a `SyntaxError`. + +And in our case, the absence of `name` is an error, as users must have a `name`. + +So let's throw it: + +```js run +let json = '{ "age": 30 }'; // incomplete data + +try { + + let user = JSON.parse(json); // <-- no errors + + if (!user.name) { +*!* + throw new SyntaxError("Incomplete data: no name"); // (*) +*/!* + } + + alert( user.name ); + +} catch (err) { + alert( "JSON Error: " + err.message ); // JSON Error: Incomplete data: no name +} +``` + +In the line `(*)`, the `throw` operator generates a `SyntaxError` with the given `message`, the same way as JavaScript would generate it itself. The execution of `try` immediately stops and the control flow jumps into `catch`. + +Now `catch` became a single place for all error handling: both for `JSON.parse` and other cases. + +## Rethrowing + +In the example above we use `try...catch` to handle incorrect data. But is it possible that *another unexpected error* occurs within the `try {...}` block? Like a programming error (variable is not defined) or something else, not just this "incorrect data" thing. + +For example: + +```js run +let json = '{ "age": 30 }'; // incomplete data + +try { + user = JSON.parse(json); // <-- forgot to put "let" before user + + // ... +} catch (err) { + alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined + // (no JSON Error actually) +} +``` + +Of course, everything's possible! Programmers do make mistakes. Even in open-source utilities used by millions for decades -- suddenly a bug may be discovered that leads to terrible hacks. + +In our case, `try...catch` is placed to catch "incorrect data" errors. But by its nature, `catch` gets *all* errors from `try`. Here it gets an unexpected error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug. + +To avoid such problems, we can employ the "rethrowing" technique. The rule is simple: + +**Catch should only process errors that it knows and "rethrow" all others.** + +The "rethrowing" technique can be explained in more detail as: + +1. Catch gets all errors. +2. In the `catch (err) {...}` block we analyze the error object `err`. +3. If we don't know how to handle it, we do `throw err`. + +Usually, we can check the error type using the `instanceof` operator: + +```js run +try { + user = { /*...*/ }; +} catch (err) { +*!* + if (err instanceof ReferenceError) { +*/!* + alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable + } +} +``` + +We can also get the error class name from `err.name` property. All native errors have it. Another option is to read `err.constructor.name`. + +In the code below, we use rethrowing so that `catch` only handles `SyntaxError`: + +```js run +let json = '{ "age": 30 }'; // incomplete data +try { + + let user = JSON.parse(json); + + if (!user.name) { + throw new SyntaxError("Incomplete data: no name"); + } + +*!* + blabla(); // unexpected error +*/!* + + alert( user.name ); + +} catch (err) { + +*!* + if (err instanceof SyntaxError) { + alert( "JSON Error: " + err.message ); + } else { + throw err; // rethrow (*) + } +*/!* + +} +``` + +The error throwing on line `(*)` from inside `catch` block "falls out" of `try...catch` and can be either caught by an outer `try...catch` construct (if it exists), or it kills the script. + +So the `catch` block actually handles only errors that it knows how to deal with and "skips" all others. + +The example below demonstrates how such errors can be caught by one more level of `try...catch`: + +```js run +function readData() { + let json = '{ "age": 30 }'; + + try { + // ... +*!* + blabla(); // error! +*/!* + } catch (err) { + // ... + if (!(err instanceof SyntaxError)) { +*!* + throw err; // rethrow (don't know how to deal with it) +*/!* + } + } +} + +try { + readData(); +} catch (err) { +*!* + alert( "External catch got: " + err ); // caught it! +*/!* +} +``` + +Here `readData` only knows how to handle `SyntaxError`, while the outer `try...catch` knows how to handle everything. + +## try...catch...finally + +Wait, that's not all. + +The `try...catch` construct may have one more code clause: `finally`. + +If it exists, it runs in all cases: + +- after `try`, if there were no errors, +- after `catch`, if there were errors. + +The extended syntax looks like this: + +```js +*!*try*/!* { + ... try to execute the code ... +} *!*catch*/!* (err) { + ... handle errors ... +} *!*finally*/!* { + ... execute always ... +} +``` + +Try running this code: + +```js run +try { + alert( 'try' ); + if (confirm('Make an error?')) BAD_CODE(); +} catch (err) { + alert( 'catch' ); +} finally { + alert( 'finally' ); +} +``` + +The code has two ways of execution: + +1. If you answer "Yes" to "Make an error?", then `try -> catch -> finally`. +2. If you say "No", then `try -> finally`. + +The `finally` clause is often used when we start doing something and want to finalize it in any case of outcome. + +For instance, we want to measure the time that a Fibonacci numbers function `fib(n)` takes. Naturally, we can start measuring before it runs and finish afterwards. But what if there's an error during the function call? In particular, the implementation of `fib(n)` in the code below returns an error for negative or non-integer numbers. + +The `finally` clause is a great place to finish the measurements no matter what. + +Here `finally` guarantees that the time will be measured correctly in both situations -- in case of a successful execution of `fib` and in case of an error in it: + +```js run +let num = +prompt("Enter a positive integer number?", 35) + +let diff, result; + +function fib(n) { + if (n < 0 || Math.trunc(n) != n) { + throw new Error("Must not be negative, and also an integer."); + } + return n <= 1 ? n : fib(n - 1) + fib(n - 2); +} + +let start = Date.now(); + +try { + result = fib(num); +} catch (err) { + result = 0; +*!* +} finally { + diff = Date.now() - start; +} +*/!* + +alert(result || "error occurred"); + +alert( `execution took ${diff}ms` ); +``` + +You can check by running the code with entering `35` into `prompt` -- it executes normally, `finally` after `try`. And then enter `-1` -- there will be an immediate error, and the execution will take `0ms`. Both measurements are done correctly. + +In other words, the function may finish with `return` or `throw`, that doesn't matter. The `finally` clause executes in both cases. + + +```smart header="Variables are local inside `try...catch...finally`" +Please note that `result` and `diff` variables in the code above are declared *before* `try...catch`. + +Otherwise, if we declared `let` in `try` block, it would only be visible inside of it. +``` + +````smart header="`finally` and `return`" +The `finally` clause works for *any* exit from `try...catch`. That includes an explicit `return`. + +In the example below, there's a `return` in `try`. In this case, `finally` is executed just before the control returns to the outer code. + +```js run +function func() { + + try { +*!* + return 1; +*/!* + + } catch (err) { + /* ... */ + } finally { +*!* + alert( 'finally' ); +*/!* + } +} + +alert( func() ); // first works alert from finally, and then this one +``` +```` + +````smart header="`try...finally`" + +The `try...finally` construct, without `catch` clause, is also useful. We apply it when we don't want to handle errors here (let them fall through), but want to be sure that processes that we started are finalized. + +```js +function func() { + // start doing something that needs completion (like measurements) + try { + // ... + } finally { + // complete that thing even if all dies + } +} +``` +In the code above, an error inside `try` always falls out, because there's no `catch`. But `finally` works before the execution flow leaves the function. +```` + +## Global catch + +```warn header="Environment-specific" +The information from this section is not a part of the core JavaScript. +``` + +Let's imagine we've got a fatal error outside of `try...catch`, and the script died. Like a programming error or some other terrible thing. + +Is there a way to react on such occurrences? We may want to log the error, show something to the user (normally they don't see error messages), etc. + +There is none in the specification, but environments usually provide it, because it's really useful. For instance, Node.js has [`process.on("uncaughtException")`](https://nodejs.org/api/process.html#process_event_uncaughtexception) for that. And in the browser we can assign a function to the special [window.onerror](mdn:api/GlobalEventHandlers/onerror) property, that will run in case of an uncaught error. + +The syntax: + +```js +window.onerror = function(message, url, line, col, error) { + // ... +}; +``` + +`message` +: Error message. + +`url` +: URL of the script where error happened. + +`line`, `col` +: Line and column numbers where error happened. + +`error` +: Error object. + +For instance: + +```html run untrusted refresh height=1 + +``` + +The role of the global handler `window.onerror` is usually not to recover the script execution -- that's probably impossible in case of programming errors, but to send the error message to developers. + +There are also web-services that provide error-logging for such cases, like or . + +They work like this: + +1. We register at the service and get a piece of JS (or a script URL) from them to insert on pages. +2. That JS script sets a custom `window.onerror` function. +3. When an error occurs, it sends a network request about it to the service. +4. We can log in to the service web interface and see errors. + +## Summary + +The `try...catch` construct allows to handle runtime errors. It literally allows to "try" running the code and "catch" errors that may occur in it. + +The syntax is: + +```js +try { + // run this code +} catch (err) { + // if an error happened, then jump here + // err is the error object +} finally { + // do in any case after try/catch +} +``` + +There may be no `catch` section or no `finally`, so shorter constructs `try...catch` and `try...finally` are also valid. + +Error objects have following properties: + +- `message` -- the human-readable error message. +- `name` -- the string with error name (error constructor name). +- `stack` (non-standard, but well-supported) -- the stack at the moment of error creation. + +If an error object is not needed, we can omit it by using `catch {` instead of `catch (err) {`. + +We can also generate our own errors using the `throw` operator. Technically, the argument of `throw` can be anything, but usually it's an error object inheriting from the built-in `Error` class. More on extending errors in the next chapter. + +*Rethrowing* is a very important pattern of error handling: a `catch` block usually expects and knows how to handle the particular error type, so it should rethrow errors it doesn't know. + +Even if we don't have `try...catch`, most environments allow us to setup a "global" error handler to catch errors that "fall out". In-browser, that's `window.onerror`. diff --git a/1-js/10-error-handling/1-try-catch/try-catch-flow.svg b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg new file mode 100644 index 0000000000..2c0d71348c --- /dev/null +++ b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg @@ -0,0 +1 @@ +BeginNo ErrorsAn error occured in the codeIgnore catch blockIgnore the rest of tryExecute catch blocktry { }// code... \ No newline at end of file diff --git a/1-js/10-error-handling/2-custom-errors/1-format-error/solution.md b/1-js/10-error-handling/2-custom-errors/1-format-error/solution.md new file mode 100644 index 0000000000..754e68f9a4 --- /dev/null +++ b/1-js/10-error-handling/2-custom-errors/1-format-error/solution.md @@ -0,0 +1,16 @@ +```js run untrusted +class FormatError extends SyntaxError { + constructor(message) { + super(message); + this.name = this.constructor.name; + } +} + +let err = new FormatError("formatting error"); + +alert( err.message ); // formatting error +alert( err.name ); // FormatError +alert( err.stack ); // stack + +alert( err instanceof SyntaxError ); // true +``` diff --git a/1-js/08-error-handling/2-custom-errors/1-format-error/task.md b/1-js/10-error-handling/2-custom-errors/1-format-error/task.md similarity index 100% rename from 1-js/08-error-handling/2-custom-errors/1-format-error/task.md rename to 1-js/10-error-handling/2-custom-errors/1-format-error/task.md diff --git a/1-js/10-error-handling/2-custom-errors/article.md b/1-js/10-error-handling/2-custom-errors/article.md new file mode 100644 index 0000000000..d28b07439b --- /dev/null +++ b/1-js/10-error-handling/2-custom-errors/article.md @@ -0,0 +1,330 @@ +# Custom errors, extending Error + +When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need `HttpError`, for database operations `DbError`, for searching operations `NotFoundError` and so on. + +Our errors should support basic error properties like `message`, `name` and, preferably, `stack`. But they also may have other properties of their own, e.g. `HttpError` objects may have a `statusCode` property with a value like `404` or `403` or `500`. + +JavaScript allows to use `throw` with any argument, so technically our custom error classes don't need to inherit from `Error`. But if we inherit, then it becomes possible to use `obj instanceof Error` to identify error objects. So it's better to inherit from it. + +As the application grows, our own errors naturally form a hierarchy. For instance, `HttpTimeoutError` may inherit from `HttpError`, and so on. + +## Extending Error + +As an example, let's consider a function `readUser(json)` that should read JSON with user data. + +Here's an example of how a valid `json` may look: +```js +let json = `{ "name": "John", "age": 30 }`; +``` + +Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it throws `SyntaxError`. But even if `json` is syntactically correct, that doesn't mean that it's a valid user, right? It may miss the necessary data. For instance, it may not have `name` and `age` properties that are essential for our users. + +Our function `readUser(json)` will not only read JSON, but check ("validate") the data. If there are no required fields, or the format is wrong, then that's an error. And that's not a `SyntaxError`, because the data is syntactically correct, but another kind of error. We'll call it `ValidationError` and create a class for it. An error of that kind should also carry the information about the offending field. + +Our `ValidationError` class should inherit from the `Error` class. + +The `Error` class is built-in, but here's its approximate code so we can understand what we're extending: + +```js +// The "pseudocode" for the built-in Error class defined by JavaScript itself +class Error { + constructor(message) { + this.message = message; + this.name = "Error"; // (different names for different built-in error classes) + this.stack = ; // non-standard, but most environments support it + } +} +``` + +Now let's inherit `ValidationError` from it and try it in action: + +```js run +*!* +class ValidationError extends Error { +*/!* + constructor(message) { + super(message); // (1) + this.name = "ValidationError"; // (2) + } +} + +function test() { + throw new ValidationError("Whoops!"); +} + +try { + test(); +} catch(err) { + alert(err.message); // Whoops! + alert(err.name); // ValidationError + alert(err.stack); // a list of nested calls with line numbers for each +} +``` + +Please note: in the line `(1)` we call the parent constructor. JavaScript requires us to call `super` in the child constructor, so that's obligatory. The parent constructor sets the `message` property. + +The parent constructor also sets the `name` property to `"Error"`, so in the line `(2)` we reset it to the right value. + +Let's try to use it in `readUser(json)`: + +```js run +class ValidationError extends Error { + constructor(message) { + super(message); + this.name = "ValidationError"; + } +} + +// Usage +function readUser(json) { + let user = JSON.parse(json); + + if (!user.age) { + throw new ValidationError("No field: age"); + } + if (!user.name) { + throw new ValidationError("No field: name"); + } + + return user; +} + +// Working example with try..catch + +try { + let user = readUser('{ "age": 25 }'); +} catch (err) { + if (err instanceof ValidationError) { +*!* + alert("Invalid data: " + err.message); // Invalid data: No field: name +*/!* + } else if (err instanceof SyntaxError) { // (*) + alert("JSON Syntax Error: " + err.message); + } else { + throw err; // unknown error, rethrow it (**) + } +} +``` + +The `try..catch` block in the code above handles both our `ValidationError` and the built-in `SyntaxError` from `JSON.parse`. + +Please take a look at how we use `instanceof` to check for the specific error type in the line `(*)`. + +We could also look at `err.name`, like this: + +```js +// ... +// instead of (err instanceof SyntaxError) +} else if (err.name == "SyntaxError") { // (*) +// ... +``` + +The `instanceof` version is much better, because in the future we are going to extend `ValidationError`, make subtypes of it, like `PropertyRequiredError`. And `instanceof` check will continue to work for new inheriting classes. So that's future-proof. + +Also it's important that if `catch` meets an unknown error, then it rethrows it in the line `(**)`. The `catch` block only knows how to handle validation and syntax errors, other kinds (caused by a typo in the code or other unknown reasons) should fall through. + +## Further inheritance + +The `ValidationError` class is very generic. Many things may go wrong. The property may be absent or it may be in a wrong format (like a string value for `age` instead of a number). Let's make a more concrete class `PropertyRequiredError`, exactly for absent properties. It will carry additional information about the property that's missing. + +```js run +class ValidationError extends Error { + constructor(message) { + super(message); + this.name = "ValidationError"; + } +} + +*!* +class PropertyRequiredError extends ValidationError { + constructor(property) { + super("No property: " + property); + this.name = "PropertyRequiredError"; + this.property = property; + } +} +*/!* + +// Usage +function readUser(json) { + let user = JSON.parse(json); + + if (!user.age) { + throw new PropertyRequiredError("age"); + } + if (!user.name) { + throw new PropertyRequiredError("name"); + } + + return user; +} + +// Working example with try..catch + +try { + let user = readUser('{ "age": 25 }'); +} catch (err) { + if (err instanceof ValidationError) { +*!* + alert("Invalid data: " + err.message); // Invalid data: No property: name + alert(err.name); // PropertyRequiredError + alert(err.property); // name +*/!* + } else if (err instanceof SyntaxError) { + alert("JSON Syntax Error: " + err.message); + } else { + throw err; // unknown error, rethrow it + } +} +``` + +The new class `PropertyRequiredError` is easy to use: we only need to pass the property name: `new PropertyRequiredError(property)`. The human-readable `message` is generated by the constructor. + +Please note that `this.name` in `PropertyRequiredError` constructor is again assigned manually. That may become a bit tedious -- to assign `this.name = ` in every custom error class. We can avoid it by making our own "basic error" class that assigns `this.name = this.constructor.name`. And then inherit all our custom errors from it. + +Let's call it `MyError`. + +Here's the code with `MyError` and other custom error classes, simplified: + +```js run +class MyError extends Error { + constructor(message) { + super(message); +*!* + this.name = this.constructor.name; +*/!* + } +} + +class ValidationError extends MyError { } + +class PropertyRequiredError extends ValidationError { + constructor(property) { + super("No property: " + property); + this.property = property; + } +} + +// name is correct +alert( new PropertyRequiredError("field").name ); // PropertyRequiredError +``` + +Now custom errors are much shorter, especially `ValidationError`, as we got rid of the `"this.name = ..."` line in the constructor. + +## Wrapping exceptions + +The purpose of the function `readUser` in the code above is "to read the user data". There may occur different kinds of errors in the process. Right now we have `SyntaxError` and `ValidationError`, but in the future `readUser` function may grow and probably generate other kinds of errors. + +The code which calls `readUser` should handle these errors. Right now it uses multiple `if`s in the `catch` block, that check the class and handle known errors and rethrow the unknown ones. + +The scheme is like this: + +```js +try { + ... + readUser() // the potential error source + ... +} catch (err) { + if (err instanceof ValidationError) { + // handle validation errors + } else if (err instanceof SyntaxError) { + // handle syntax errors + } else { + throw err; // unknown error, rethrow it + } +} +``` + +In the code above we can see two types of errors, but there can be more. + +If the `readUser` function generates several kinds of errors, then we should ask ourselves: do we really want to check for all error types one-by-one every time? + +Often the answer is "No": we'd like to be "one level above all that". We just want to know if there was a "data reading error" -- why exactly it happened is often irrelevant (the error message describes it). Or, even better, we'd like to have a way to get the error details, but only if we need to. + +The technique that we describe here is called "wrapping exceptions". + +1. We'll make a new class `ReadError` to represent a generic "data reading" error. +2. The function `readUser` will catch data reading errors that occur inside it, such as `ValidationError` and `SyntaxError`, and generate a `ReadError` instead. +3. The `ReadError` object will keep the reference to the original error in its `cause` property. + +Then the code that calls `readUser` will only have to check for `ReadError`, not for every kind of data reading errors. And if it needs more details of an error, it can check its `cause` property. + +Here's the code that defines `ReadError` and demonstrates its use in `readUser` and `try..catch`: + +```js run +class ReadError extends Error { + constructor(message, cause) { + super(message); + this.cause = cause; + this.name = 'ReadError'; + } +} + +class ValidationError extends Error { /*...*/ } +class PropertyRequiredError extends ValidationError { /* ... */ } + +function validateUser(user) { + if (!user.age) { + throw new PropertyRequiredError("age"); + } + + if (!user.name) { + throw new PropertyRequiredError("name"); + } +} + +function readUser(json) { + let user; + + try { + user = JSON.parse(json); + } catch (err) { +*!* + if (err instanceof SyntaxError) { + throw new ReadError("Syntax Error", err); + } else { + throw err; + } +*/!* + } + + try { + validateUser(user); + } catch (err) { +*!* + if (err instanceof ValidationError) { + throw new ReadError("Validation Error", err); + } else { + throw err; + } +*/!* + } + +} + +try { + readUser('{bad json}'); +} catch (e) { + if (e instanceof ReadError) { +*!* + alert(e); + // Original error: SyntaxError: Unexpected token b in JSON at position 1 + alert("Original error: " + e.cause); +*/!* + } else { + throw e; + } +} +``` + +In the code above, `readUser` works exactly as described -- catches syntax and validation errors and throws `ReadError` errors instead (unknown errors are rethrown as usual). + +So the outer code checks `instanceof ReadError` and that's it. No need to list all possible error types. + +The approach is called "wrapping exceptions", because we take "low level" exceptions and "wrap" them into `ReadError` that is more abstract. It is widely used in object-oriented programming. + +## Summary + +- We can inherit from `Error` and other built-in error classes normally. We just need to take care of the `name` property and don't forget to call `super`. +- We can use `instanceof` to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from a 3rd-party library and there's no easy way to get its class. Then `name` property can be used for such checks. +- Wrapping exceptions is a widespread technique: a function handles low-level exceptions and creates higher-level errors instead of various low-level ones. Low-level exceptions sometimes become properties of that object like `err.cause` in the examples above, but that's not strictly required. diff --git a/1-js/08-error-handling/index.md b/1-js/10-error-handling/index.md similarity index 100% rename from 1-js/08-error-handling/index.md rename to 1-js/10-error-handling/index.md diff --git a/1-js/11-async/01-callbacks/article.md b/1-js/11-async/01-callbacks/article.md new file mode 100644 index 0000000000..57115a9098 --- /dev/null +++ b/1-js/11-async/01-callbacks/article.md @@ -0,0 +1,312 @@ + + +# Introduction: callbacks + +```warn header="We use browser methods in examples here" +To demonstrate the use of callbacks, promises and other abstract concepts, we'll be using some browser methods: specifically, loading scripts and performing simple document manipulations. + +If you're not familiar with these methods, and their usage in the examples is confusing, you may want to read a few chapters from the [next part](/document) of the tutorial. + +Although, we'll try to make things clear anyway. There won't be anything really complex browser-wise. +``` + +Many functions are provided by JavaScript host environments that allow you to schedule *asynchronous* actions. In other words, actions that we initiate now, but they finish later. + +For instance, one such function is the `setTimeout` function. + +There are other real-world examples of asynchronous actions, e.g. loading scripts and modules (we'll cover them in later chapters). + +Take a look at the function `loadScript(src)`, that loads a script with the given `src`: + +```js +function loadScript(src) { + // creates a + + + + diff --git a/6-async/02-promise-basics/03-animate-circle-promise/task.md b/1-js/11-async/02-promise-basics/03-animate-circle-promise/task.md similarity index 100% rename from 6-async/02-promise-basics/03-animate-circle-promise/task.md rename to 1-js/11-async/02-promise-basics/03-animate-circle-promise/task.md diff --git a/1-js/11-async/02-promise-basics/article.md b/1-js/11-async/02-promise-basics/article.md new file mode 100644 index 0000000000..b15643f0a2 --- /dev/null +++ b/1-js/11-async/02-promise-basics/article.md @@ -0,0 +1,364 @@ +# Promise + +Imagine that you're a top singer, and fans ask day and night for your upcoming song. + +To get some relief, you promise to send it to them when it's published. You give your fans a list. They can fill in their email addresses, so that when the song becomes available, all subscribed parties instantly receive it. And even if something goes very wrong, say, a fire in the studio, so that you can't publish the song, they will still be notified. + +Everyone is happy: you, because the people don't crowd you anymore, and fans, because they won't miss the song. + +This is a real-life analogy for things we often have in programming: + +1. A "producing code" that does something and takes time. For instance, some code that loads the data over a network. That's a "singer". +2. A "consuming code" that wants the result of the "producing code" once it's ready. Many functions may need that result. These are the "fans". +3. A *promise* is a special JavaScript object that links the "producing code" and the "consuming code" together. In terms of our analogy: this is the "subscription list". The "producing code" takes whatever time it needs to produce the promised result, and the "promise" makes that result available to all of the subscribed code when it's ready. + +The analogy isn't terribly accurate, because JavaScript promises are more complex than a simple subscription list: they have additional features and limitations. But it's fine to begin with. + +The constructor syntax for a promise object is: + +```js +let promise = new Promise(function(resolve, reject) { + // executor (the producing code, "singer") +}); +``` + +The function passed to `new Promise` is called the *executor*. When `new Promise` is created, the executor runs automatically. It contains the producing code which should eventually produce the result. In terms of the analogy above: the executor is the "singer". + +Its arguments `resolve` and `reject` are callbacks provided by JavaScript itself. Our code is only inside the executor. + +When the executor obtains the result, be it soon or late, doesn't matter, it should call one of these callbacks: + +- `resolve(value)` — if the job is finished successfully, with result `value`. +- `reject(error)` — if an error has occurred, `error` is the error object. + +So to summarize: the executor runs automatically and attempts to perform a job. When it is finished with the attempt, it calls `resolve` if it was successful or `reject` if there was an error. + +The `promise` object returned by the `new Promise` constructor has these internal properties: + +- `state` — initially `"pending"`, then changes to either `"fulfilled"` when `resolve` is called or `"rejected"` when `reject` is called. +- `result` — initially `undefined`, then changes to `value` when `resolve(value)` is called or `error` when `reject(error)` is called. + +So the executor eventually moves `promise` to one of these states: + +![](promise-resolve-reject.svg) + +Later we'll see how "fans" can subscribe to these changes. + +Here's an example of a promise constructor and a simple executor function with "producing code" that takes time (via `setTimeout`): + +```js +let promise = new Promise(function(resolve, reject) { + // the function is executed automatically when the promise is constructed + + // after 1 second signal that the job is done with the result "done" + setTimeout(() => *!*resolve("done")*/!*, 1000); +}); +``` + +We can see two things by running the code above: + +1. The executor is called automatically and immediately (by `new Promise`). +2. The executor receives two arguments: `resolve` and `reject`. These functions are pre-defined by the JavaScript engine, so we don't need to create them. We should only call one of them when ready. + + After one second of "processing", the executor calls `resolve("done")` to produce the result. This changes the state of the `promise` object: + + ![](promise-resolve-1.svg) + +That was an example of a successful job completion, a "fulfilled promise". + +And now an example of the executor rejecting the promise with an error: + +```js +let promise = new Promise(function(resolve, reject) { + // after 1 second signal that the job is finished with an error + setTimeout(() => *!*reject(new Error("Whoops!"))*/!*, 1000); +}); +``` + +The call to `reject(...)` moves the promise object to `"rejected"` state: + +![](promise-reject-1.svg) + +To summarize, the executor should perform a job (usually something that takes time) and then call `resolve` or `reject` to change the state of the corresponding promise object. + +A promise that is either resolved or rejected is called "settled", as opposed to an initially "pending" promise. + +````smart header="There can be only a single result or an error" +The executor should call only one `resolve` or one `reject`. Any state change is final. + +All further calls of `resolve` and `reject` are ignored: + +```js +let promise = new Promise(function(resolve, reject) { +*!* + resolve("done"); +*/!* + + reject(new Error("…")); // ignored + setTimeout(() => resolve("…")); // ignored +}); +``` + +The idea is that a job done by the executor may have only one result or an error. + +Also, `resolve`/`reject` expect only one argument (or none) and will ignore additional arguments. +```` + +```smart header="Reject with `Error` objects" +In case something goes wrong, the executor should call `reject`. That can be done with any type of argument (just like `resolve`). But it is recommended to use `Error` objects (or objects that inherit from `Error`). The reasoning for that will soon become apparent. +``` + +````smart header="Immediately calling `resolve`/`reject`" +In practice, an executor usually does something asynchronously and calls `resolve`/`reject` after some time, but it doesn't have to. We also can call `resolve` or `reject` immediately, like this: + +```js +let promise = new Promise(function(resolve, reject) { + // not taking our time to do the job + resolve(123); // immediately give the result: 123 +}); +``` + +For instance, this might happen when we start to do a job but then see that everything has already been completed and cached. + +That's fine. We immediately have a resolved promise. +```` + +```smart header="The `state` and `result` are internal" +The properties `state` and `result` of the Promise object are internal. We can't directly access them. We can use the methods `.then`/`.catch`/`.finally` for that. They are described below. +``` + +## Consumers: then, catch + +A Promise object serves as a link between the executor (the "producing code" or "singer") and the consuming functions (the "fans"), which will receive the result or error. Consuming functions can be registered (subscribed) using the methods `.then` and `.catch`. + +### then + +The most important, fundamental one is `.then`. + +The syntax is: + +```js +promise.then( + function(result) { *!*/* handle a successful result */*/!* }, + function(error) { *!*/* handle an error */*/!* } +); +``` + +The first argument of `.then` is a function that runs when the promise is resolved and receives the result. + +The second argument of `.then` is a function that runs when the promise is rejected and receives the error. + +For instance, here's a reaction to a successfully resolved promise: + +```js run +let promise = new Promise(function(resolve, reject) { + setTimeout(() => resolve("done!"), 1000); +}); + +// resolve runs the first function in .then +promise.then( +*!* + result => alert(result), // shows "done!" after 1 second +*/!* + error => alert(error) // doesn't run +); +``` + +The first function was executed. + +And in the case of a rejection, the second one: + +```js run +let promise = new Promise(function(resolve, reject) { + setTimeout(() => reject(new Error("Whoops!")), 1000); +}); + +// reject runs the second function in .then +promise.then( + result => alert(result), // doesn't run +*!* + error => alert(error) // shows "Error: Whoops!" after 1 second +*/!* +); +``` + +If we're interested only in successful completions, then we can provide only one function argument to `.then`: + +```js run +let promise = new Promise(resolve => { + setTimeout(() => resolve("done!"), 1000); +}); + +*!* +promise.then(alert); // shows "done!" after 1 second +*/!* +``` + +### catch + +If we're interested only in errors, then we can use `null` as the first argument: `.then(null, errorHandlingFunction)`. Or we can use `.catch(errorHandlingFunction)`, which is exactly the same: + + +```js run +let promise = new Promise((resolve, reject) => { + setTimeout(() => reject(new Error("Whoops!")), 1000); +}); + +*!* +// .catch(f) is the same as promise.then(null, f) +promise.catch(alert); // shows "Error: Whoops!" after 1 second +*/!* +``` + +The call `.catch(f)` is a complete analog of `.then(null, f)`, it's just a shorthand. + +## Cleanup: finally + +Just like there's a `finally` clause in a regular `try {...} catch {...}`, there's `finally` in promises. + +The call `.finally(f)` is similar to `.then(f, f)` in the sense that `f` runs always, when the promise is settled: be it resolve or reject. + +The idea of `finally` is to set up a handler for performing cleanup/finalizing after the previous operations are complete. + +E.g. stopping loading indicators, closing no longer needed connections, etc. + +Think of it as a party finisher. No matter was a party good or bad, how many friends were in it, we still need (or at least should) do a cleanup after it. + +The code may look like this: + +```js +new Promise((resolve, reject) => { + /* do something that takes time, and then call resolve or maybe reject */ +}) +*!* + // runs when the promise is settled, doesn't matter successfully or not + .finally(() => stop loading indicator) + // so the loading indicator is always stopped before we go on +*/!* + .then(result => show result, err => show error) +``` + +Please note that `finally(f)` isn't exactly an alias of `then(f,f)` though. + +There are important differences: + +1. A `finally` handler has no arguments. In `finally` we don't know whether the promise is successful or not. That's all right, as our task is usually to perform "general" finalizing procedures. + + Please take a look at the example above: as you can see, the `finally` handler has no arguments, and the promise outcome is handled by the next handler. +2. A `finally` handler "passes through" the result or error to the next suitable handler. + + For instance, here the result is passed through `finally` to `then`: + + ```js run + new Promise((resolve, reject) => { + setTimeout(() => resolve("value"), 2000); + }) + .finally(() => alert("Promise ready")) // triggers first + .then(result => alert(result)); // <-- .then shows "value" + ``` + + As you can see, the `value` returned by the first promise is passed through `finally` to the next `then`. + + That's very convenient, because `finally` is not meant to process a promise result. As said, it's a place to do generic cleanup, no matter what the outcome was. + + And here's an example of an error, for us to see how it's passed through `finally` to `catch`: + + ```js run + new Promise((resolve, reject) => { + throw new Error("error"); + }) + .finally(() => alert("Promise ready")) // triggers first + .catch(err => alert(err)); // <-- .catch shows the error + ``` + +3. A `finally` handler also shouldn't return anything. If it does, the returned value is silently ignored. + + The only exception to this rule is when a `finally` handler throws an error. Then this error goes to the next handler, instead of any previous outcome. + +To summarize: + +- A `finally` handler doesn't get the outcome of the previous handler (it has no arguments). This outcome is passed through instead, to the next suitable handler. +- If a `finally` handler returns something, it's ignored. +- When `finally` throws an error, then the execution goes to the nearest error handler. + +These features are helpful and make things work just the right way if we use `finally` how it's supposed to be used: for generic cleanup procedures. + +````smart header="We can attach handlers to settled promises" +If a promise is pending, `.then/catch/finally` handlers wait for its outcome. + +Sometimes, it might be that a promise is already settled when we add a handler to it. + +In such case, these handlers just run immediately: + +```js run +// the promise becomes resolved immediately upon creation +let promise = new Promise(resolve => resolve("done!")); + +promise.then(alert); // done! (shows up right now) +``` + +Note that this makes promises more powerful than the real life "subscription list" scenario. If the singer has already released their song and then a person signs up on the subscription list, they probably won't receive that song. Subscriptions in real life must be done prior to the event. + +Promises are more flexible. We can add handlers any time: if the result is already there, they just execute. +```` + +## Example: loadScript [#loadscript] + +Next, let's see more practical examples of how promises can help us write asynchronous code. + +We've got the `loadScript` function for loading a script from the previous chapter. + +Here's the callback-based variant, just to remind us of it: + +```js +function loadScript(src, callback) { + let script = document.createElement('script'); + script.src = src; + + script.onload = () => callback(null, script); + script.onerror = () => callback(new Error(`Script load error for ${src}`)); + + document.head.append(script); +} +``` + +Let's rewrite it using Promises. + +The new function `loadScript` will not require a callback. Instead, it will create and return a Promise object that resolves when the loading is complete. The outer code can add handlers (subscribing functions) to it using `.then`: + +```js run +function loadScript(src) { + return new Promise(function(resolve, reject) { + let script = document.createElement('script'); + script.src = src; + + script.onload = () => resolve(script); + script.onerror = () => reject(new Error(`Script load error for ${src}`)); + + document.head.append(script); + }); +} +``` + +Usage: + +```js run +let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); + +promise.then( + script => alert(`${script.src} is loaded!`), + error => alert(`Error: ${error.message}`) +); + +promise.then(script => alert('Another handler...')); +``` + +We can immediately see a few benefits over the callback-based pattern: + + +| Promises | Callbacks | +|----------|-----------| +| Promises allow us to do things in the natural order. First, we run `loadScript(script)`, and `.then` we write what to do with the result. | We must have a `callback` function at our disposal when calling `loadScript(script, callback)`. In other words, we must know what to do with the result *before* `loadScript` is called. | +| We can call `.then` on a Promise as many times as we want. Each time, we're adding a new "fan", a new subscribing function, to the "subscription list". More about this in the next chapter: [](info:promise-chaining). | There can be only one callback. | + +So promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters. diff --git a/6-async/02-promise-basics/head.html b/1-js/11-async/02-promise-basics/head.html similarity index 100% rename from 6-async/02-promise-basics/head.html rename to 1-js/11-async/02-promise-basics/head.html 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 new file mode 100644 index 0000000000..777e477394 --- /dev/null +++ b/1-js/11-async/02-promise-basics/promise-reject-1.svg @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..f1f34eaee1 --- /dev/null +++ b/1-js/11-async/02-promise-basics/promise-resolve-1.svg @@ -0,0 +1 @@ +new Promise(executor)state: "pending" result: undefinedresolve("done")state: "fulfilled" result: "done" \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/promise-resolve-reject.svg b/1-js/11-async/02-promise-basics/promise-resolve-reject.svg new file mode 100644 index 0000000000..80593a3a4b --- /dev/null +++ b/1-js/11-async/02-promise-basics/promise-resolve-reject.svg @@ -0,0 +1 @@ +new Promise(executor)state: "pending" result: undefinedresolve(value)reject(error)state: "fulfilled" result: valuestate: "rejected" result: error \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/01-then-vs-catch/solution.md b/1-js/11-async/03-promise-chaining/01-then-vs-catch/solution.md new file mode 100644 index 0000000000..bdd1c643bd --- /dev/null +++ b/1-js/11-async/03-promise-chaining/01-then-vs-catch/solution.md @@ -0,0 +1,20 @@ +The short answer is: **no, they are not equal**: + +The difference is that if an error happens in `f1`, then it is handled by `.catch` here: + +```js run +promise + .then(f1) + .catch(f2); +``` + +...But not here: + +```js run +promise + .then(f1, f2); +``` + +That's because an error is passed down the chain, and in the second code piece there's no chain below `f1`. + +In other words, `.then` passes results/errors to the next `.then/catch`. So in the first example, there's a `catch` below, and in the second one there isn't, so the error is unhandled. diff --git a/1-js/11-async/03-promise-chaining/01-then-vs-catch/task.md b/1-js/11-async/03-promise-chaining/01-then-vs-catch/task.md new file mode 100644 index 0000000000..cefca60aaa --- /dev/null +++ b/1-js/11-async/03-promise-chaining/01-then-vs-catch/task.md @@ -0,0 +1,13 @@ +# Promise: then versus catch + +Are these code fragments equal? In other words, do they behave the same way in any circumstances, for any handler functions? + +```js +promise.then(f1).catch(f2); +``` + +Versus: + +```js +promise.then(f1, f2); +``` diff --git a/1-js/11-async/03-promise-chaining/article.md b/1-js/11-async/03-promise-chaining/article.md new file mode 100644 index 0000000000..a33ca258c3 --- /dev/null +++ b/1-js/11-async/03-promise-chaining/article.md @@ -0,0 +1,366 @@ + +# Promises chaining + +Let's return to the problem mentioned in the chapter : we have a sequence of asynchronous tasks to be performed one after another — for instance, loading scripts. How can we code it well? + +Promises provide a couple of recipes to do that. + +In this chapter we cover promise chaining. + +It looks like this: + +```js run +new Promise(function(resolve, reject) { + + setTimeout(() => resolve(1), 1000); // (*) + +}).then(function(result) { // (**) + + alert(result); // 1 + return result * 2; + +}).then(function(result) { // (***) + + alert(result); // 2 + return result * 2; + +}).then(function(result) { + + alert(result); // 4 + return result * 2; + +}); +``` + +The idea is that the result is passed through the chain of `.then` handlers. + +Here the flow is: +1. The initial promise resolves in 1 second `(*)`, +2. Then the `.then` handler is called `(**)`, which in turn creates a new promise (resolved with `2` value). +3. The next `then` `(***)` gets the result of the previous one, processes it (doubles) and passes it to the next handler. +4. ...and so on. + +As the result is passed along the chain of handlers, we can see a sequence of `alert` calls: `1` -> `2` -> `4`. + +![](promise-then-chain.svg) + +The whole thing works, because every call to a `.then` returns a new promise, so that we can call the next `.then` on it. + +When a handler returns a value, it becomes the result of that promise, so the next `.then` is called with it. + +**A classic newbie error: technically we can also add many `.then` to a single promise. This is not chaining.** + +For example: +```js run +let promise = new Promise(function(resolve, reject) { + setTimeout(() => resolve(1), 1000); +}); + +promise.then(function(result) { + alert(result); // 1 + return result * 2; +}); + +promise.then(function(result) { + alert(result); // 1 + return result * 2; +}); + +promise.then(function(result) { + alert(result); // 1 + return result * 2; +}); +``` + +What we did here is just adding several handlers to one promise. They don't pass the result to each other; instead they process it independently. + +Here's the picture (compare it with the chaining above): + +![](promise-then-many.svg) + +All `.then` on the same promise get the same result -- the result of that promise. So in the code above all `alert` show the same: `1`. + +In practice we rarely need multiple handlers for one promise. Chaining is used much more often. + +## Returning promises + +A handler, used in `.then(handler)` may create and return a promise. + +In that case further handlers wait until it settles, and then get its result. + +For instance: + +```js run +new Promise(function(resolve, reject) { + + setTimeout(() => resolve(1), 1000); + +}).then(function(result) { + + alert(result); // 1 + +*!* + return new Promise((resolve, reject) => { // (*) + setTimeout(() => resolve(result * 2), 1000); + }); +*/!* + +}).then(function(result) { // (**) + + alert(result); // 2 + + return new Promise((resolve, reject) => { + setTimeout(() => resolve(result * 2), 1000); + }); + +}).then(function(result) { + + alert(result); // 4 + +}); +``` + +Here the first `.then` shows `1` and returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result (the argument of `resolve`, here it's `result * 2`) is passed on to the handler of the second `.then`. That handler is in the line `(**)`, it shows `2` and does the same thing. + +So the output is the same as in the previous example: 1 -> 2 -> 4, but now with 1 second delay between `alert` calls. + +Returning promises allows us to build chains of asynchronous actions. + +## Example: loadScript + +Let's use this feature with the promisified `loadScript`, defined in the [previous chapter](info:promise-basics#loadscript), to load scripts one by one, in sequence: + +```js run +loadScript("/article/promise-chaining/one.js") + .then(function(script) { + return loadScript("/article/promise-chaining/two.js"); + }) + .then(function(script) { + return loadScript("/article/promise-chaining/three.js"); + }) + .then(function(script) { + // use functions declared in scripts + // to show that they indeed loaded + one(); + two(); + three(); + }); +``` + +This code can be made bit shorter with arrow functions: + +```js run +loadScript("/article/promise-chaining/one.js") + .then(script => loadScript("/article/promise-chaining/two.js")) + .then(script => loadScript("/article/promise-chaining/three.js")) + .then(script => { + // scripts are loaded, we can use functions declared there + one(); + two(); + three(); + }); +``` + + +Here each `loadScript` call returns a promise, and the next `.then` runs when it resolves. Then it initiates the loading of the next script. So scripts are loaded one after another. + +We can add more asynchronous actions to the chain. Please note that the code is still "flat" — it grows down, not to the right. There are no signs of the "pyramid of doom". + +Technically, we could add `.then` directly to each `loadScript`, like this: + +```js run +loadScript("/article/promise-chaining/one.js").then(script1 => { + loadScript("/article/promise-chaining/two.js").then(script2 => { + loadScript("/article/promise-chaining/three.js").then(script3 => { + // this function has access to variables script1, script2 and script3 + one(); + two(); + three(); + }); + }); +}); +``` + +This code does the same: loads 3 scripts in sequence. But it "grows to the right". So we have the same problem as with callbacks. + +People who start to use promises sometimes don't know about chaining, so they write it this way. Generally, chaining is preferred. + +Sometimes it's ok to write `.then` directly, because the nested function has access to the outer scope. In the example above the most nested callback has access to all variables `script1`, `script2`, `script3`. But that's an exception rather than a rule. + + +````smart header="Thenables" +To be precise, a handler may return not exactly a promise, but a so-called "thenable" object - an arbitrary object that has a method `.then`. It will be treated the same way as a promise. + +The idea is that 3rd-party libraries may implement "promise-compatible" objects of their own. They can have an extended set of methods, but also be compatible with native promises, because they implement `.then`. + +Here's an example of a thenable object: + +```js run +class Thenable { + constructor(num) { + this.num = num; + } + then(resolve, reject) { + alert(resolve); // function() { native code } + // resolve with this.num*2 after the 1 second + setTimeout(() => resolve(this.num * 2), 1000); // (**) + } +} + +new Promise(resolve => resolve(1)) + .then(result => { +*!* + return new Thenable(result); // (*) +*/!* + }) + .then(alert); // shows 2 after 1000ms +``` + +JavaScript checks the object returned by the `.then` handler in line `(*)`: if it has a callable method named `then`, then it calls that method providing native functions `resolve`, `reject` as arguments (similar to an executor) and waits until one of them is called. In the example above `resolve(2)` is called after 1 second `(**)`. Then the result is passed further down the chain. + +This feature allows us to integrate custom objects with promise chains without having to inherit from `Promise`. +```` + + +## Bigger example: fetch + +In frontend programming, promises are often used for network requests. So let's see an extended example of that. + +We'll use the [fetch](info:fetch) method to load the information about the user from the remote server. It has a lot of optional parameters covered in [separate chapters](info:fetch), but the basic syntax is quite simple: + +```js +let promise = fetch(url); +``` + +This makes a network request to the `url` and returns a promise. The promise resolves with a `response` object when the remote server responds with headers, but *before the full response is downloaded*. + +To read the full response, we should call the method `response.text()`: it returns a promise that resolves when the full text is downloaded from the remote server, with that text as a result. + +The code below makes a request to `user.json` and loads its text from the server: + +```js run +fetch('/article/promise-chaining/user.json') + // .then below runs when the remote server responds + .then(function(response) { + // response.text() returns a new promise that resolves with the full response text + // when it loads + return response.text(); + }) + .then(function(text) { + // ...and here's the content of the remote file + alert(text); // {"name": "iliakan", "isAdmin": true} + }); +``` + +The `response` object returned from `fetch` also includes the method `response.json()` that reads the remote data and parses it as JSON. In our case that's even more convenient, so let's switch to it. + +We'll also use arrow functions for brevity: + +```js run +// same as above, but response.json() parses the remote content as JSON +fetch('/article/promise-chaining/user.json') + .then(response => response.json()) + .then(user => alert(user.name)); // iliakan, got user name +``` + +Now let's do something with the loaded user. + +For instance, we can make one more request to GitHub, load the user profile and show the avatar: + +```js run +// Make a request for user.json +fetch('/article/promise-chaining/user.json') + // Load it as json + .then(response => response.json()) + // Make a request to GitHub + .then(user => fetch(`https://api.github.com/users/${user.name}`)) + // Load the response as json + .then(response => response.json()) + // Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it) + .then(githubUser => { + let img = document.createElement('img'); + img.src = githubUser.avatar_url; + img.className = "promise-avatar-example"; + document.body.append(img); + + setTimeout(() => img.remove(), 3000); // (*) + }); +``` + +The code works; see comments about the details. However, there's a potential problem in it, a typical error for those who begin to use promises. + +Look at the line `(*)`: how can we do something *after* the avatar has finished showing and gets removed? For instance, we'd like to show a form for editing that user or something else. As of now, there's no way. + +To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing. + +Like this: + +```js run +fetch('/article/promise-chaining/user.json') + .then(response => response.json()) + .then(user => fetch(`https://api.github.com/users/${user.name}`)) + .then(response => response.json()) +*!* + .then(githubUser => new Promise(function(resolve, reject) { // (*) +*/!* + let img = document.createElement('img'); + img.src = githubUser.avatar_url; + img.className = "promise-avatar-example"; + document.body.append(img); + + setTimeout(() => { + img.remove(); +*!* + resolve(githubUser); // (**) +*/!* + }, 3000); + })) + // triggers after 3 seconds + .then(githubUser => alert(`Finished showing ${githubUser.name}`)); +``` + +That is, the `.then` handler in line `(*)` now returns `new Promise`, that becomes settled only after the call of `resolve(githubUser)` in `setTimeout` `(**)`. The next `.then` in the chain will wait for that. + +As a good practice, an asynchronous action should always return a promise. That makes it possible to plan actions after it; even if we don't plan to extend the chain now, we may need it later. + +Finally, we can split the code into reusable functions: + +```js run +function loadJson(url) { + return fetch(url) + .then(response => response.json()); +} + +function loadGithubUser(name) { + return loadJson(`https://api.github.com/users/${name}`); +} + +function showAvatar(githubUser) { + return new Promise(function(resolve, reject) { + let img = document.createElement('img'); + img.src = githubUser.avatar_url; + img.className = "promise-avatar-example"; + document.body.append(img); + + setTimeout(() => { + img.remove(); + resolve(githubUser); + }, 3000); + }); +} + +// Use them: +loadJson('/article/promise-chaining/user.json') + .then(user => loadGithubUser(user.name)) + .then(showAvatar) + .then(githubUser => alert(`Finished showing ${githubUser.name}`)); + // ... +``` + +## Summary + +If a `.then` (or `catch/finally`, doesn't matter) handler returns a promise, the rest of the chain waits until it settles. When it does, its result (or error) is passed further. + +Here's a full picture: + +![](promise-handler-variants.svg) diff --git a/6-async/03-promise-chaining/getMessage.js b/1-js/11-async/03-promise-chaining/getMessage.js similarity index 100% rename from 6-async/03-promise-chaining/getMessage.js rename to 1-js/11-async/03-promise-chaining/getMessage.js diff --git a/1-js/11-async/03-promise-chaining/head.html b/1-js/11-async/03-promise-chaining/head.html new file mode 100644 index 0000000000..0a0075fb92 --- /dev/null +++ b/1-js/11-async/03-promise-chaining/head.html @@ -0,0 +1,22 @@ + + + diff --git a/6-async/03-promise-chaining/one.js b/1-js/11-async/03-promise-chaining/one.js similarity index 100% rename from 6-async/03-promise-chaining/one.js rename to 1-js/11-async/03-promise-chaining/one.js diff --git a/1-js/11-async/03-promise-chaining/promise-handler-variants.svg b/1-js/11-async/03-promise-chaining/promise-handler-variants.svg new file mode 100644 index 0000000000..664a4dbebc --- /dev/null +++ b/1-js/11-async/03-promise-chaining/promise-handler-variants.svg @@ -0,0 +1 @@ +return valuereturn promisethrow errorstate: "fulfilled" result: valuestate: "rejected" result: error...with the result of the new promise...state: "pending" result: undefinedthe call of .then(handler) always returns a promise:if handler ends with…that promise settles with: \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-then-chain.svg b/1-js/11-async/03-promise-chaining/promise-then-chain.svg new file mode 100644 index 0000000000..fb60142fbd --- /dev/null +++ b/1-js/11-async/03-promise-chaining/promise-then-chain.svg @@ -0,0 +1 @@ +.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-then-many.svg b/1-js/11-async/03-promise-chaining/promise-then-many.svg new file mode 100644 index 0000000000..8fea7beaaf --- /dev/null +++ b/1-js/11-async/03-promise-chaining/promise-then-many.svg @@ -0,0 +1 @@ +.thennew Promiseresolve(1).then.then \ No newline at end of file diff --git a/6-async/03-promise-chaining/three.js b/1-js/11-async/03-promise-chaining/three.js similarity index 100% rename from 6-async/03-promise-chaining/three.js rename to 1-js/11-async/03-promise-chaining/three.js diff --git a/6-async/03-promise-chaining/two.js b/1-js/11-async/03-promise-chaining/two.js similarity index 100% rename from 6-async/03-promise-chaining/two.js rename to 1-js/11-async/03-promise-chaining/two.js diff --git a/6-async/03-promise-chaining/user.json b/1-js/11-async/03-promise-chaining/user.json similarity index 100% rename from 6-async/03-promise-chaining/user.json rename to 1-js/11-async/03-promise-chaining/user.json diff --git a/6-async/03-promise-chaining/02-error-async/solution.md b/1-js/11-async/04-promise-error-handling/01-error-async/solution.md similarity index 100% rename from 6-async/03-promise-chaining/02-error-async/solution.md rename to 1-js/11-async/04-promise-error-handling/01-error-async/solution.md diff --git a/6-async/03-promise-chaining/02-error-async/task.md b/1-js/11-async/04-promise-error-handling/01-error-async/task.md similarity index 100% rename from 6-async/03-promise-chaining/02-error-async/task.md rename to 1-js/11-async/04-promise-error-handling/01-error-async/task.md diff --git a/1-js/11-async/04-promise-error-handling/article.md b/1-js/11-async/04-promise-error-handling/article.md new file mode 100644 index 0000000000..c5b4206ab2 --- /dev/null +++ b/1-js/11-async/04-promise-error-handling/article.md @@ -0,0 +1,205 @@ + +# Error handling with promises + +Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That's very convenient in practice. + +For instance, in the code below the URL to `fetch` is wrong (no such site) and `.catch` handles the error: + +```js run +*!* +fetch('https://no-such-server.blabla') // rejects +*/!* + .then(response => response.json()) + .catch(err => alert(err)) // TypeError: failed to fetch (the text may vary) +``` + +As you can see, the `.catch` doesn't have to be immediate. It may appear after one or maybe several `.then`. + +Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors is to append `.catch` to the end of chain: + +```js run +fetch('/article/promise-chaining/user.json') + .then(response => response.json()) + .then(user => fetch(`https://api.github.com/users/${user.name}`)) + .then(response => response.json()) + .then(githubUser => new Promise((resolve, reject) => { + let img = document.createElement('img'); + img.src = githubUser.avatar_url; + img.className = "promise-avatar-example"; + document.body.append(img); + + setTimeout(() => { + img.remove(); + resolve(githubUser); + }, 3000); + })) +*!* + .catch(error => alert(error.message)); +*/!* +``` + +Normally, such `.catch` doesn't trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it. + +## Implicit try..catch + +The code of a promise executor and promise handlers has an "invisible `try..catch`" around it. If an exception happens, it gets caught and treated as a rejection. + +For instance, this code: + +```js run +new Promise((resolve, reject) => { +*!* + throw new Error("Whoops!"); +*/!* +}).catch(alert); // Error: Whoops! +``` + +...Works exactly the same as this: + +```js run +new Promise((resolve, reject) => { +*!* + reject(new Error("Whoops!")); +*/!* +}).catch(alert); // Error: Whoops! +``` + +The "invisible `try..catch`" around the executor automatically catches the error and turns it into rejected promise. + +This happens not only in the executor function, but in its handlers as well. If we `throw` inside a `.then` handler, that means a rejected promise, so the control jumps to the nearest error handler. + +Here's an example: + +```js run +new Promise((resolve, reject) => { + resolve("ok"); +}).then((result) => { +*!* + throw new Error("Whoops!"); // rejects the promise +*/!* +}).catch(alert); // Error: Whoops! +``` + +This happens for all errors, not just those caused by the `throw` statement. For example, a programming error: + +```js run +new Promise((resolve, reject) => { + resolve("ok"); +}).then((result) => { +*!* + blabla(); // no such function +*/!* +}).catch(alert); // ReferenceError: blabla is not defined +``` + +The final `.catch` not only catches explicit rejections, but also accidental errors in the handlers above. + +## Rethrowing + +As we already noticed, `.catch` at the end of the chain is similar to `try..catch`. We may have as many `.then` handlers as we want, and then use a single `.catch` at the end to handle errors in all of them. + +In a regular `try..catch` we can analyze the error and maybe rethrow it if it can't be handled. The same thing is possible for promises. + +If we `throw` inside `.catch`, then the control goes to the next closest error handler. And if we handle the error and finish normally, then it continues to the next closest successful `.then` handler. + +In the example below the `.catch` successfully handles the error: + +```js run +// the execution: catch -> then +new Promise((resolve, reject) => { + + throw new Error("Whoops!"); + +}).catch(function(error) { + + alert("The error is handled, continue normally"); + +}).then(() => alert("Next successful handler runs")); +``` + +Here the `.catch` block finishes normally. So the next successful `.then` handler is called. + +In the example below we see the other situation with `.catch`. The handler `(*)` catches the error and just can't handle it (e.g. it only knows how to handle `URIError`), so it throws it again: + +```js run +// the execution: catch -> catch +new Promise((resolve, reject) => { + + throw new Error("Whoops!"); + +}).catch(function(error) { // (*) + + if (error instanceof URIError) { + // handle it + } else { + alert("Can't handle such error"); + +*!* + throw error; // throwing this or another error jumps to the next catch +*/!* + } + +}).then(function() { + /* doesn't run here */ +}).catch(error => { // (**) + + alert(`The unknown error has occurred: ${error}`); + // don't return anything => execution goes the normal way + +}); +``` + +The execution jumps from the first `.catch` `(*)` to the next one `(**)` down the chain. + +## Unhandled rejections + +What happens when an error is not handled? For instance, we forgot to append `.catch` to the end of the chain, like here: + +```js untrusted run refresh +new Promise(function() { + noSuchFunction(); // Error here (no such function) +}) + .then(() => { + // successful promise handlers, one or more + }); // without .catch at the end! +``` + +In case of an error, the promise becomes rejected, and the execution should jump to the closest rejection handler. But there is none. So the error gets "stuck". There's no code to handle it. + +In practice, just like with regular unhandled errors in code, it means that something has gone terribly wrong. + +What happens when a regular error occurs and is not caught by `try..catch`? The script dies with a message in the console. A similar thing happens with unhandled promise rejections. + +The JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above. + +In the browser we can catch such errors using the event `unhandledrejection`: + +```js run +*!* +window.addEventListener('unhandledrejection', function(event) { + // the event object has two special properties: + alert(event.promise); // [object Promise] - the promise that generated the error + alert(event.reason); // Error: Whoops! - the unhandled error object +}); +*/!* + +new Promise(function() { + throw new Error("Whoops!"); +}); // no catch to handle the error +``` + +The event is the part of the [HTML standard](https://html.spec.whatwg.org/multipage/webappapis.html#unhandled-promise-rejections). + +If an error occurs, and there's no `.catch`, the `unhandledrejection` handler triggers, and gets the `event` object with the information about the error, so we can do something. + +Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server. + +In non-browser environments like Node.js there are other ways to track unhandled errors. + +## Summary + +- `.catch` handles errors in promises of all kinds: be it a `reject()` call, or an error thrown in a handler. +- `.then` also catches errors in the same manner, if given the second argument (which is the error handler). +- We should place `.catch` exactly in places where we want to handle errors and know how to handle them. The handler should analyze errors (custom error classes help) and rethrow unknown ones (maybe they are programming mistakes). +- It's ok not to use `.catch` at all, if there's no way to recover from an error. +- In any case we should have the `unhandledrejection` event handler (for browsers, and analogs for other environments) to track unhandled errors and inform the user (and probably our server) about them, so that our app never "just dies". diff --git a/1-js/11-async/04-promise-error-handling/getMessage.js b/1-js/11-async/04-promise-error-handling/getMessage.js new file mode 100644 index 0000000000..6c5893433a --- /dev/null +++ b/1-js/11-async/04-promise-error-handling/getMessage.js @@ -0,0 +1,3 @@ +function getMessage() { + return "Hello, world!"; +} diff --git a/1-js/11-async/04-promise-error-handling/head.html b/1-js/11-async/04-promise-error-handling/head.html new file mode 100644 index 0000000000..a0b7419623 --- /dev/null +++ b/1-js/11-async/04-promise-error-handling/head.html @@ -0,0 +1,29 @@ + + + diff --git a/6-async/04-promise-api/one.js b/1-js/11-async/04-promise-error-handling/one.js similarity index 100% rename from 6-async/04-promise-api/one.js rename to 1-js/11-async/04-promise-error-handling/one.js diff --git a/1-js/11-async/04-promise-error-handling/promise-then-chain.svg b/1-js/11-async/04-promise-error-handling/promise-then-chain.svg new file mode 100644 index 0000000000..fb60142fbd --- /dev/null +++ b/1-js/11-async/04-promise-error-handling/promise-then-chain.svg @@ -0,0 +1 @@ +.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file diff --git a/1-js/11-async/04-promise-error-handling/promise-then-many.svg b/1-js/11-async/04-promise-error-handling/promise-then-many.svg new file mode 100644 index 0000000000..8fea7beaaf --- /dev/null +++ b/1-js/11-async/04-promise-error-handling/promise-then-many.svg @@ -0,0 +1 @@ +.thennew Promiseresolve(1).then.then \ No newline at end of file diff --git a/1-js/11-async/04-promise-error-handling/three.js b/1-js/11-async/04-promise-error-handling/three.js new file mode 100644 index 0000000000..8536e85a18 --- /dev/null +++ b/1-js/11-async/04-promise-error-handling/three.js @@ -0,0 +1,3 @@ +function three() { + alert(3); +} diff --git a/6-async/04-promise-api/two.js b/1-js/11-async/04-promise-error-handling/two.js similarity index 100% rename from 6-async/04-promise-api/two.js rename to 1-js/11-async/04-promise-error-handling/two.js diff --git a/6-async/04-promise-api/iliakan.json b/1-js/11-async/04-promise-error-handling/user.json similarity index 100% rename from 6-async/04-promise-api/iliakan.json rename to 1-js/11-async/04-promise-error-handling/user.json diff --git a/1-js/11-async/05-promise-api/article.md b/1-js/11-async/05-promise-api/article.md new file mode 100644 index 0000000000..7be84ce2cc --- /dev/null +++ b/1-js/11-async/05-promise-api/article.md @@ -0,0 +1,323 @@ +# Promise API + +There are 6 static methods in the `Promise` class. We'll quickly cover their use cases here. + +## Promise.all + +Let's say we want many promises to execute in parallel and wait until all of them are ready. + +For instance, download several URLs in parallel and process the content once they are all done. + +That's what `Promise.all` is for. + +The syntax is: + +```js +let promise = Promise.all(iterable); +``` + +`Promise.all` takes an iterable (usually, an array of promises) and returns a new promise. + +The new promise resolves when all listed promises are resolved, and the array of their results becomes its result. + +For instance, the `Promise.all` below settles after 3 seconds, and then its result is an array `[1, 2, 3]`: + +```js run +Promise.all([ + new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 + new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 + new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 +]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member +``` + +Please note that the order of the resulting array members is the same as in its source promises. Even though the first promise takes the longest time to resolve, it's still first in the array of results. + +A common trick is to map an array of job data into an array of promises, and then wrap that into `Promise.all`. + +For instance, if we have an array of URLs, we can fetch them all like this: + +```js run +let urls = [ + 'https://api.github.com/users/iliakan', + 'https://api.github.com/users/remy', + 'https://api.github.com/users/jeresig' +]; + +// map every url to the promise of the fetch +let requests = urls.map(url => fetch(url)); + +// Promise.all waits until all jobs are resolved +Promise.all(requests) + .then(responses => responses.forEach( + response => alert(`${response.url}: ${response.status}`) + )); +``` + +A bigger example with fetching user information for an array of GitHub users by their names (we could fetch an array of goods by their ids, the logic is identical): + +```js run +let names = ['iliakan', 'remy', 'jeresig']; + +let requests = names.map(name => fetch(`https://api.github.com/users/${name}`)); + +Promise.all(requests) + .then(responses => { + // all responses are resolved successfully + for(let response of responses) { + alert(`${response.url}: ${response.status}`); // shows 200 for every url + } + + return responses; + }) + // map array of responses into an array of response.json() to read their content + .then(responses => Promise.all(responses.map(r => r.json()))) + // all JSON answers are parsed: "users" is the array of them + .then(users => users.forEach(user => alert(user.name))); +``` + +**If any of the promises is rejected, the promise returned by `Promise.all` immediately rejects with that error.** + +For instance: + +```js run +Promise.all([ + new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), +*!* + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), +*/!* + new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) +]).catch(alert); // Error: Whoops! +``` + +Here the second promise rejects in two seconds. That leads to an immediate rejection of `Promise.all`, so `.catch` executes: the rejection error becomes the outcome of the entire `Promise.all`. + +```warn header="In case of an error, other promises are ignored" +If one promise rejects, `Promise.all` immediately rejects, completely forgetting about the other ones in the list. Their results are ignored. + +For example, if there are multiple `fetch` calls, like in the example above, and one fails, the others will still continue to execute, but `Promise.all` won't watch them anymore. They will probably settle, but their results will be ignored. + +`Promise.all` does nothing to cancel them, as there's no concept of "cancellation" in promises. In [another chapter](info:fetch-abort) we'll cover `AbortController` that can help with that, but it's not a part of the Promise API. +``` + +````smart header="`Promise.all(iterable)` allows non-promise \"regular\" values in `iterable`" +Normally, `Promise.all(...)` accepts an iterable (in most cases an array) of promises. But if any of those objects is not a promise, it's passed to the resulting array "as is". + +For instance, here the results are `[1, 2, 3]`: + +```js run +Promise.all([ + new Promise((resolve, reject) => { + setTimeout(() => resolve(1), 1000) + }), + 2, + 3 +]).then(alert); // 1, 2, 3 +``` + +So we are able to pass ready values to `Promise.all` where convenient. +```` + +## Promise.allSettled + +[recent browser="new"] + +`Promise.all` rejects as a whole if any promise rejects. That's good for "all or nothing" cases, when we need *all* results successful to proceed: + +```js +Promise.all([ + fetch('/template.html'), + fetch('/style.css'), + fetch('/data.json') +]).then(render); // render method needs results of all fetches +``` + +`Promise.allSettled` just waits for all promises to settle, regardless of the result. The resulting array has: + +- `{status:"fulfilled", value:result}` for successful responses, +- `{status:"rejected", reason:error}` for errors. + +For example, we'd like to fetch the information about multiple users. Even if one request fails, we're still interested in the others. + +Let's use `Promise.allSettled`: + +```js run +let urls = [ + 'https://api.github.com/users/iliakan', + 'https://api.github.com/users/remy', + 'https://no-such-url' +]; + +Promise.allSettled(urls.map(url => fetch(url))) + .then(results => { // (*) + results.forEach((result, num) => { + if (result.status == "fulfilled") { + alert(`${urls[num]}: ${result.value.status}`); + } + if (result.status == "rejected") { + alert(`${urls[num]}: ${result.reason}`); + } + }); + }); +``` + +The `results` in the line `(*)` above will be: +```js +[ + {status: 'fulfilled', value: ...response...}, + {status: 'fulfilled', value: ...response...}, + {status: 'rejected', reason: ...error object...} +] +``` + +So for each promise we get its status and `value/error`. + +### Polyfill + +If the browser doesn't support `Promise.allSettled`, it's easy to polyfill: + +```js +if (!Promise.allSettled) { + const rejectHandler = reason => ({ status: 'rejected', reason }); + + const resolveHandler = value => ({ status: 'fulfilled', value }); + + Promise.allSettled = function (promises) { + const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler)); + return Promise.all(convertedPromises); + }; +} +``` + +In this code, `promises.map` takes input values, turns them into promises (just in case a non-promise was passed) with `p => Promise.resolve(p)`, and then adds `.then` handler to every one. + +That handler turns a successful result `value` into `{status:'fulfilled', value}`, and an error `reason` into `{status:'rejected', reason}`. That's exactly the format of `Promise.allSettled`. + +Now we can use `Promise.allSettled` to get the results of *all* given promises, even if some of them reject. + +## Promise.race + +Similar to `Promise.all`, but waits only for the first settled promise and gets its result (or error). + +The syntax is: + +```js +let promise = Promise.race(iterable); +``` + +For instance, here the result will be `1`: + +```js run +Promise.race([ + new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), + new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) +]).then(alert); // 1 +``` + +The first promise here was fastest, so it became the result. After the first settled promise "wins the race", all further results/errors are ignored. + + +## Promise.any + +Similar to `Promise.race`, but waits only for the first fulfilled promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with [`AggregateError`](mdn:js/AggregateError) - a special error object that stores all promise errors in its `errors` property. + +The syntax is: + +```js +let promise = Promise.any(iterable); +``` + +For instance, here the result will be `1`: + +```js run +Promise.any([ + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)), + new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)), + new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) +]).then(alert); // 1 +``` + +The first promise here was fastest, but it was rejected, so the second promise became the result. After the first fulfilled promise "wins the race", all further results are ignored. + +Here's an example when all promises fail: + +```js run +Promise.any([ + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)), + new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000)) +]).catch(error => { + console.log(error.constructor.name); // AggregateError + console.log(error.errors[0]); // Error: Ouch! + console.log(error.errors[1]); // Error: Error! +}); +``` + +As you can see, error objects for failed promises are available in the `errors` property of the `AggregateError` object. + +## Promise.resolve/reject + +Methods `Promise.resolve` and `Promise.reject` are rarely needed in modern code, because `async/await` syntax (we'll cover it [a bit later](info:async-await)) makes them somewhat obsolete. + +We cover them here for completeness and for those who can't use `async/await` for some reason. + +### Promise.resolve + +`Promise.resolve(value)` creates a resolved promise with the result `value`. + +Same as: + +```js +let promise = new Promise(resolve => resolve(value)); +``` + +The method is used for compatibility, when a function is expected to return a promise. + +For example, the `loadCached` function below fetches a URL and remembers (caches) its content. For future calls with the same URL it immediately gets the previous content from cache, but uses `Promise.resolve` to make a promise of it, so the returned value is always a promise: + +```js +let cache = new Map(); + +function loadCached(url) { + if (cache.has(url)) { +*!* + return Promise.resolve(cache.get(url)); // (*) +*/!* + } + + return fetch(url) + .then(response => response.text()) + .then(text => { + cache.set(url,text); + return text; + }); +} +``` + +We can write `loadCached(url).then(…)`, because the function is guaranteed to return a promise. We can always use `.then` after `loadCached`. That's the purpose of `Promise.resolve` in the line `(*)`. + +### Promise.reject + +`Promise.reject(error)` creates a rejected promise with `error`. + +Same as: + +```js +let promise = new Promise((resolve, reject) => reject(error)); +``` + +In practice, this method is almost never used. + +## Summary + +There are 6 static methods of `Promise` class: + +1. `Promise.all(promises)` -- waits for all promises to resolve and returns an array of their results. If any of the given promises rejects, it becomes the error of `Promise.all`, and all other results are ignored. +2. `Promise.allSettled(promises)` (recently added method) -- waits for all promises to settle and returns their results as an array of objects with: + - `status`: `"fulfilled"` or `"rejected"` + - `value` (if fulfilled) or `reason` (if rejected). +3. `Promise.race(promises)` -- waits for the first promise to settle, and its result/error becomes the outcome. +4. `Promise.any(promises)` (recently added method) -- waits for the first promise to fulfill, and its result becomes the outcome. If all of the given promises are rejected, [`AggregateError`](mdn:js/AggregateError) becomes the error of `Promise.any`. +5. `Promise.resolve(value)` -- makes a resolved promise with the given value. +6. `Promise.reject(error)` -- makes a rejected promise with the given error. + +Of all these, `Promise.all` is probably the most common in practice. diff --git a/6-async/04-promise-api/head.html b/1-js/11-async/05-promise-api/head.html similarity index 100% rename from 6-async/04-promise-api/head.html rename to 1-js/11-async/05-promise-api/head.html diff --git a/1-js/11-async/05-promise-api/iliakan.json b/1-js/11-async/05-promise-api/iliakan.json new file mode 100644 index 0000000000..32f89971a8 --- /dev/null +++ b/1-js/11-async/05-promise-api/iliakan.json @@ -0,0 +1,4 @@ +{ + "name": "iliakan", + "isAdmin": true +} diff --git a/1-js/11-async/05-promise-api/one.js b/1-js/11-async/05-promise-api/one.js new file mode 100644 index 0000000000..948a60e075 --- /dev/null +++ b/1-js/11-async/05-promise-api/one.js @@ -0,0 +1,3 @@ +function one() { + alert(1); +} diff --git a/1-js/11-async/05-promise-api/two.js b/1-js/11-async/05-promise-api/two.js new file mode 100644 index 0000000000..b04795b86c --- /dev/null +++ b/1-js/11-async/05-promise-api/two.js @@ -0,0 +1,3 @@ +function two() { + alert(2); +} diff --git a/1-js/11-async/06-promisify/article.md b/1-js/11-async/06-promisify/article.md new file mode 100644 index 0000000000..855678e5be --- /dev/null +++ b/1-js/11-async/06-promisify/article.md @@ -0,0 +1,132 @@ +# Promisification + +"Promisification" is a long word for a simple transformation. It's the conversion of a function that accepts a callback into a function that returns a promise. + +Such transformations are often required in real-life, as many functions and libraries are callback-based. But promises are more convenient, so it makes sense to promisify them. + +For better understanding, let's see an example. + +For instance, we have `loadScript(src, callback)` from the chapter . + +```js run +function loadScript(src, callback) { + let script = document.createElement('script'); + script.src = src; + + script.onload = () => callback(null, script); + script.onerror = () => callback(new Error(`Script load error for ${src}`)); + + document.head.append(script); +} + +// usage: +// loadScript('path/script.js', (err, script) => {...}) +``` + +The function loads a script with the given `src`, and then calls `callback(err)` in case of an error, or `callback(null, script)` in case of successful loading. That's a widespread agreement for using callbacks, we saw it before. + +Let's promisify it. + +We'll make a new function `loadScriptPromise(src)`, that does the same (loads the script), but returns a promise instead of using callbacks. + +In other words, we pass it only `src` (no `callback`) and get a promise in return, that resolves with `script` when the load is successful, and rejects with the error otherwise. + +Here it is: +```js +let loadScriptPromise = function(src) { + return new Promise((resolve, reject) => { + loadScript(src, (err, script) => { + if (err) reject(err); + else resolve(script); + }); + }); +}; + +// usage: +// loadScriptPromise('path/script.js').then(...) +``` + +As we can see, the new function is a wrapper around the original `loadScript` function. It calls it providing its own callback that translates to promise `resolve/reject`. + +Now `loadScriptPromise` fits well in promise-based code. If we like promises more than callbacks (and soon we'll see more reasons for that), then we will use it instead. + +In practice we may need to promisify more than one function, so it makes sense to use a helper. + +We'll call it `promisify(f)`: it accepts a to-promisify function `f` and returns a wrapper function. + +```js +function promisify(f) { + return function (...args) { // return a wrapper-function (*) + return new Promise((resolve, reject) => { + function callback(err, result) { // our custom callback for f (**) + if (err) { + reject(err); + } else { + resolve(result); + } + } + + args.push(callback); // append our custom callback to the end of f arguments + + f.call(this, ...args); // call the original function + }); + }; +} + +// usage: +let loadScriptPromise = promisify(loadScript); +loadScriptPromise(...).then(...); +``` + +The code may look a bit complex, but it's essentially the same that we wrote above, while promisifying `loadScript` function. + +A call to `promisify(f)` returns a wrapper around `f` `(*)`. That wrapper returns a promise and forwards the call to the original `f`, tracking the result in the custom callback `(**)`. + +Here, `promisify` assumes that the original function expects a callback with exactly two arguments `(err, result)`. That's what we encounter most often. Then our custom callback is in exactly the right format, and `promisify` works great for such a case. + +But what if the original `f` expects a callback with more arguments `callback(err, res1, res2, ...)`? + +We can improve our helper. Let's make a more advanced version of `promisify`. + +- When called as `promisify(f)` it should work similar to the version above. +- When called as `promisify(f, true)`, it should return the promise that resolves with the array of callback results. That's exactly for callbacks with many arguments. + +```js +// promisify(f, true) to get array of results +function promisify(f, manyArgs = false) { + return function (...args) { + return new Promise((resolve, reject) => { + function *!*callback(err, ...results*/!*) { // our custom callback for f + if (err) { + reject(err); + } else { + // resolve with all callback results if manyArgs is specified + *!*resolve(manyArgs ? results : results[0]);*/!* + } + } + + args.push(callback); + + f.call(this, ...args); + }); + }; +} + +// usage: +f = promisify(f, true); +f(...).then(arrayOfResults => ..., err => ...); +``` + +As you can see it's essentially the same as above, but `resolve` is called with only one or all arguments depending on whether `manyArgs` is truthy. + +For more exotic callback formats, like those without `err` at all: `callback(result)`, we can promisify such functions manually without using the helper. + +There are also modules with a bit more flexible promisification functions, e.g. [es6-promisify](https://github.com/digitaldesignlabs/es6-promisify). In Node.js, there's a built-in `util.promisify` function for that. + +```smart +Promisification is a great approach, especially when you use `async/await` (covered later in the chapter ), but not a total replacement for callbacks. + +Remember, a promise may have only one result, but a callback may technically be called many times. + +So promisification is only meant for functions that call the callback once. Further calls will be ignored. +``` diff --git a/1-js/11-async/07-microtask-queue/article.md b/1-js/11-async/07-microtask-queue/article.md new file mode 100644 index 0000000000..014dd93c06 --- /dev/null +++ b/1-js/11-async/07-microtask-queue/article.md @@ -0,0 +1,112 @@ + +# Microtasks + +Promise handlers `.then`/`.catch`/`.finally` are always asynchronous. + +Even when a Promise is immediately resolved, the code on the lines *below* `.then`/`.catch`/`.finally` will still execute before these handlers. + +Here's a demo: + +```js run +let promise = Promise.resolve(); + +promise.then(() => alert("promise done!")); + +alert("code finished"); // this alert shows first +``` + +If you run it, you see `code finished` first, and then `promise done!`. + +That's strange, because the promise is definitely done from the beginning. + +Why did the `.then` trigger afterwards? What's going on? + +## Microtasks queue + +Asynchronous tasks need proper management. For that, the ECMA standard specifies an internal queue `PromiseJobs`, more often referred to as the "microtask queue" (V8 term). + +As stated in the [specification](https://tc39.github.io/ecma262/#sec-jobs-and-job-queues): + +- The queue is first-in-first-out: tasks enqueued first are run first. +- Execution of a task is initiated only when nothing else is running. + +Or, to put it more simply, when a promise is ready, its `.then/catch/finally` handlers are put into the queue; they are not executed yet. When the JavaScript engine becomes free from the current code, it takes a task from the queue and executes it. + +That's why "code finished" in the example above shows first. + +![](promiseQueue.svg) + +Promise handlers always go through this internal queue. + +If there's a chain with multiple `.then/catch/finally`, then every one of them is executed asynchronously. That is, it first gets queued, then executed when the current code is complete and previously queued handlers are finished. + +**What if the order matters for us? How can we make `code finished` appear after `promise done`?** + +Easy, just put it into the queue with `.then`: + +```js run +Promise.resolve() + .then(() => alert("promise done!")) + .then(() => alert("code finished")); +``` + +Now the order is as intended. + +## Unhandled rejection + +Remember the `unhandledrejection` event from the article ? + +Now we can see exactly how JavaScript finds out that there was an unhandled rejection. + +**An "unhandled rejection" occurs when a promise error is not handled at the end of the microtask queue.** + +Normally, if we expect an error, we add `.catch` to the promise chain to handle it: + +```js run +let promise = Promise.reject(new Error("Promise Failed!")); +*!* +promise.catch(err => alert('caught')); +*/!* + +// doesn't run: error handled +window.addEventListener('unhandledrejection', event => alert(event.reason)); +``` + +But if we forget to add `.catch`, then, after the microtask queue is empty, the engine triggers the event: + +```js run +let promise = Promise.reject(new Error("Promise Failed!")); + +// Promise Failed! +window.addEventListener('unhandledrejection', event => alert(event.reason)); +``` + +What if we handle the error later? Like this: + +```js run +let promise = Promise.reject(new Error("Promise Failed!")); +*!* +setTimeout(() => promise.catch(err => alert('caught')), 1000); +*/!* + +// Error: Promise Failed! +window.addEventListener('unhandledrejection', event => alert(event.reason)); +``` + +Now, if we run it, we'll see `Promise Failed!` first and then `caught`. + +If we didn't know about the microtasks queue, we could wonder: "Why did `unhandledrejection` handler run? We did catch and handle the error!" + +But now we understand that `unhandledrejection` is generated when the microtask queue is complete: the engine examines promises and, if any of them is in the "rejected" state, then the event triggers. + +In the example above, `.catch` added by `setTimeout` also triggers. But it does so later, after `unhandledrejection` has already occurred, so it doesn't change anything. + +## Summary + +Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (V8 term). + +So `.then/catch/finally` handlers are always called after the current code is finished. + +If we need to guarantee that a piece of code is executed after `.then/catch/finally`, we can add it into a chained `.then` call. + +In most Javascript engines, including browsers and Node.js, the concept of microtasks is closely tied with the "event loop" and "macrotasks". As these have no direct relation to promises, they are covered in another part of the tutorial, in the article . diff --git a/1-js/11-async/07-microtask-queue/promiseQueue.svg b/1-js/11-async/07-microtask-queue/promiseQueue.svg new file mode 100644 index 0000000000..c802c44a01 --- /dev/null +++ b/1-js/11-async/07-microtask-queue/promiseQueue.svg @@ -0,0 +1 @@ +promise . then ( handler ); ... alert ( "code finished" );handler enqueuedqueued handler runsscript execution finished \ No newline at end of file diff --git a/1-js/11-async/08-async-await/01-rewrite-async/solution.md b/1-js/11-async/08-async-await/01-rewrite-async/solution.md new file mode 100644 index 0000000000..3337ef3c4d --- /dev/null +++ b/1-js/11-async/08-async-await/01-rewrite-async/solution.md @@ -0,0 +1,33 @@ + +The notes are below the code: + +```js run +async function loadJson(url) { // (1) + let response = await fetch(url); // (2) + + if (response.status == 200) { + let json = await response.json(); // (3) + return json; + } + + throw new Error(response.status); +} + +loadJson('https://javascript.info/no-such-user.json') + .catch(alert); // Error: 404 (4) +``` + +Notes: + +1. The function `loadJson` becomes `async`. +2. All `.then` inside are replaced with `await`. +3. We can `return response.json()` instead of awaiting for it, like this: + + ```js + if (response.status == 200) { + return response.json(); // (3) + } + ``` + + Then the outer code would have to `await` for that promise to resolve. In our case it doesn't matter. +4. The error thrown from `loadJson` is handled by `.catch`. We can't use `await loadJson(…)` there, because we're not in an `async` function. diff --git a/1-js/11-async/08-async-await/01-rewrite-async/task.md b/1-js/11-async/08-async-await/01-rewrite-async/task.md new file mode 100644 index 0000000000..0c31737da9 --- /dev/null +++ b/1-js/11-async/08-async-await/01-rewrite-async/task.md @@ -0,0 +1,20 @@ + +# Rewrite using async/await + +Rewrite this example code from the chapter using `async/await` instead of `.then/catch`: + +```js run +function loadJson(url) { + return fetch(url) + .then(response => { + if (response.status == 200) { + return response.json(); + } else { + throw new Error(response.status); + } + }); +} + +loadJson('https://javascript.info/no-such-user.json') + .catch(alert); // Error: 404 +``` diff --git a/1-js/11-async/08-async-await/02-rewrite-async-2/solution.md b/1-js/11-async/08-async-await/02-rewrite-async-2/solution.md new file mode 100644 index 0000000000..aa462dbf77 --- /dev/null +++ b/1-js/11-async/08-async-await/02-rewrite-async-2/solution.md @@ -0,0 +1,49 @@ + +There are no tricks here. Just replace `.catch` with `try..catch` inside `demoGithubUser` and add `async/await` where needed: + +```js run +class HttpError extends Error { + constructor(response) { + super(`${response.status} for ${response.url}`); + this.name = 'HttpError'; + this.response = response; + } +} + +async function loadJson(url) { + let response = await fetch(url); + if (response.status == 200) { + return response.json(); + } else { + throw new HttpError(response); + } +} + +// Ask for a user name until github returns a valid user +async function demoGithubUser() { + + let user; + while(true) { + let name = prompt("Enter a name?", "iliakan"); + + try { + user = await loadJson(`https://api.github.com/users/${name}`); + break; // no error, exit loop + } catch(err) { + if (err instanceof HttpError && err.response.status == 404) { + // loop continues after the alert + alert("No such user, please reenter."); + } else { + // unknown error, rethrow + throw err; + } + } + } + + + alert(`Full name: ${user.name}.`); + return user; +} + +demoGithubUser(); +``` diff --git a/1-js/11-async/08-async-await/02-rewrite-async-2/task.md b/1-js/11-async/08-async-await/02-rewrite-async-2/task.md new file mode 100644 index 0000000000..13d625d2a8 --- /dev/null +++ b/1-js/11-async/08-async-await/02-rewrite-async-2/task.md @@ -0,0 +1,48 @@ + +# Rewrite "rethrow" with async/await + +Below you can find the "rethrow" example. Rewrite it using `async/await` instead of `.then/catch`. + +And get rid of the recursion in favour of a loop in `demoGithubUser`: with `async/await` that becomes easy to do. + +```js run +class HttpError extends Error { + constructor(response) { + super(`${response.status} for ${response.url}`); + this.name = 'HttpError'; + this.response = response; + } +} + +function loadJson(url) { + return fetch(url) + .then(response => { + if (response.status == 200) { + return response.json(); + } else { + throw new HttpError(response); + } + }); +} + +// Ask for a user name until github returns a valid user +function demoGithubUser() { + let name = prompt("Enter a name?", "iliakan"); + + return loadJson(`https://api.github.com/users/${name}`) + .then(user => { + alert(`Full name: ${user.name}.`); + return user; + }) + .catch(err => { + if (err instanceof HttpError && err.response.status == 404) { + alert("No such user, please reenter."); + return demoGithubUser(); + } else { + throw err; + } + }); +} + +demoGithubUser(); +``` diff --git a/1-js/11-async/08-async-await/03-async-from-regular/solution.md b/1-js/11-async/08-async-await/03-async-from-regular/solution.md new file mode 100644 index 0000000000..7e2ab597eb --- /dev/null +++ b/1-js/11-async/08-async-await/03-async-from-regular/solution.md @@ -0,0 +1,20 @@ + +That's the case when knowing how it works inside is helpful. + +Just treat `async` call as promise and attach `.then` to it: +```js run +async function wait() { + await new Promise(resolve => setTimeout(resolve, 1000)); + + return 10; +} + +function f() { + // shows 10 after 1 second +*!* + wait().then(result => alert(result)); +*/!* +} + +f(); +``` diff --git a/1-js/11-async/08-async-await/03-async-from-regular/task.md b/1-js/11-async/08-async-await/03-async-from-regular/task.md new file mode 100644 index 0000000000..ca7c186ff2 --- /dev/null +++ b/1-js/11-async/08-async-await/03-async-from-regular/task.md @@ -0,0 +1,20 @@ + +# Call async from non-async + +We have a "regular" function called `f`. How can you call the `async` function `wait()` and use its result inside of `f`? + +```js +async function wait() { + await new Promise(resolve => setTimeout(resolve, 1000)); + + return 10; +} + +function f() { + // ...what should you write here? + // we need to call async wait() and wait to get 10 + // remember, we can't use "await" +} +``` + +P.S. The task is technically very simple, but the question is quite common for developers new to async/await. diff --git a/1-js/11-async/08-async-await/article.md b/1-js/11-async/08-async-await/article.md new file mode 100644 index 0000000000..e679b1c4c8 --- /dev/null +++ b/1-js/11-async/08-async-await/article.md @@ -0,0 +1,311 @@ +# Async/await + +There's a special syntax to work with promises in a more comfortable fashion, called "async/await". It's surprisingly easy to understand and use. + +## Async functions + +Let's start with the `async` keyword. It can be placed before a function, like this: + +```js +async function f() { + return 1; +} +``` + +The word "async" before a function means one simple thing: a function always returns a promise. Other values are wrapped in a resolved promise automatically. + +For instance, this function returns a resolved promise with the result of `1`; let's test it: + +```js run +async function f() { + return 1; +} + +f().then(alert); // 1 +``` + +...We could explicitly return a promise, which would be the same: + +```js run +async function f() { + return Promise.resolve(1); +} + +f().then(alert); // 1 +``` + +So, `async` ensures that the function returns a promise, and wraps non-promises in it. Simple enough, right? But not only that. There's another keyword, `await`, that works only inside `async` functions, and it's pretty cool. + +## Await + +The syntax: + +```js +// works only inside async functions +let value = await promise; +``` + +The keyword `await` makes JavaScript wait until that promise settles and returns its result. + +Here's an example with a promise that resolves in 1 second: +```js run +async function f() { + + let promise = new Promise((resolve, reject) => { + setTimeout(() => resolve("done!"), 1000) + }); + +*!* + let result = await promise; // wait until the promise resolves (*) +*/!* + + alert(result); // "done!" +} + +f(); +``` + +The function execution "pauses" at the line `(*)` and resumes when the promise settles, with `result` becoming its result. So the code above shows "done!" in one second. + +Let's emphasize: `await` literally suspends the function execution until the promise settles, and then resumes it with the promise result. That doesn't cost any CPU resources, because the JavaScript engine can do other jobs in the meantime: execute other scripts, handle events, etc. + +It's just a more elegant syntax of getting the promise result than `promise.then`. And, it's easier to read and write. + +````warn header="Can't use `await` in regular functions" +If we try to use `await` in a non-async function, there would be a syntax error: + +```js run +function f() { + let promise = Promise.resolve(1); +*!* + let result = await promise; // Syntax error +*/!* +} +``` + +We may get this error if we forget to put `async` before a function. As stated earlier, `await` only works inside an `async` function. +```` + +Let's take the `showAvatar()` example from the chapter and rewrite it using `async/await`: + +1. We'll need to replace `.then` calls with `await`. +2. Also we should make the function `async` for them to work. + +```js run +async function showAvatar() { + + // read our JSON + let response = await fetch('/article/promise-chaining/user.json'); + let user = await response.json(); + + // read github user + let githubResponse = await fetch(`https://api.github.com/users/${user.name}`); + let githubUser = await githubResponse.json(); + + // show the avatar + let img = document.createElement('img'); + img.src = githubUser.avatar_url; + img.className = "promise-avatar-example"; + document.body.append(img); + + // wait 3 seconds + await new Promise((resolve, reject) => setTimeout(resolve, 3000)); + + img.remove(); + + return githubUser; +} + +showAvatar(); +``` + +Pretty clean and easy to read, right? Much better than before. + +````smart header="Modern browsers allow top-level `await` in modules" +In modern browsers, `await` on top level works just fine, when we're inside a module. We'll cover modules in article . + +For instance: + +```js run module +// we assume this code runs at top level, inside a module +let response = await fetch('/article/promise-chaining/user.json'); +let user = await response.json(); + +console.log(user); +``` + +If we're not using modules, or [older browsers](https://caniuse.com/mdn-javascript_operators_await_top_level) must be supported, there's a universal recipe: wrapping into an anonymous async function. + +Like this: + +```js +(async () => { + let response = await fetch('/article/promise-chaining/user.json'); + let user = await response.json(); + ... +})(); +``` + +```` + +````smart header="`await` accepts \"thenables\"" +Like `promise.then`, `await` allows us to use thenable objects (those with a callable `then` method). The idea is that a third-party object may not be a promise, but promise-compatible: if it supports `.then`, that's enough to use it with `await`. + +Here's a demo `Thenable` class; the `await` below accepts its instances: + +```js run +class Thenable { + constructor(num) { + this.num = num; + } + then(resolve, reject) { + alert(resolve); + // resolve with this.num*2 after 1000ms + setTimeout(() => resolve(this.num * 2), 1000); // (*) + } +} + +async function f() { + // waits for 1 second, then result becomes 2 + let result = await new Thenable(1); + alert(result); +} + +f(); +``` + +If `await` gets a non-promise object with `.then`, it calls that method providing the built-in functions `resolve` and `reject` as arguments (just as it does for a regular `Promise` executor). Then `await` waits until one of them is called (in the example above it happens in the line `(*)`) and then proceeds with the result. +```` + +````smart header="Async class methods" +To declare an async class method, just prepend it with `async`: + +```js run +class Waiter { +*!* + async wait() { +*/!* + return await Promise.resolve(1); + } +} + +new Waiter() + .wait() + .then(alert); // 1 (this is the same as (result => alert(result))) +``` +The meaning is the same: it ensures that the returned value is a promise and enables `await`. + +```` +## Error handling + +If a promise resolves normally, then `await promise` returns the result. But in the case of a rejection, it throws the error, just as if there were a `throw` statement at that line. + +This code: + +```js +async function f() { +*!* + await Promise.reject(new Error("Whoops!")); +*/!* +} +``` + +...is the same as this: + +```js +async function f() { +*!* + throw new Error("Whoops!"); +*/!* +} +``` + +In real situations, the promise may take some time before it rejects. In that case there will be a delay before `await` throws an error. + +We can catch that error using `try..catch`, the same way as a regular `throw`: + +```js run +async function f() { + + try { + let response = await fetch('http://no-such-url'); + } catch(err) { +*!* + alert(err); // TypeError: failed to fetch +*/!* + } +} + +f(); +``` + +In the case of an error, the control jumps to the `catch` block. We can also wrap multiple lines: + +```js run +async function f() { + + try { + let response = await fetch('/no-user-here'); + let user = await response.json(); + } catch(err) { + // catches errors both in fetch and response.json + alert(err); + } +} + +f(); +``` + +If we don't have `try..catch`, then the promise generated by the call of the async function `f()` becomes rejected. We can append `.catch` to handle it: + +```js run +async function f() { + let response = await fetch('http://no-such-url'); +} + +// f() becomes a rejected promise +*!* +f().catch(alert); // TypeError: failed to fetch // (*) +*/!* +``` + +If we forget to add `.catch` there, then we get an unhandled promise error (viewable in the console). We can catch such errors using a global `unhandledrejection` event handler as described in the chapter . + + +```smart header="`async/await` and `promise.then/catch`" +When we use `async/await`, we rarely need `.then`, because `await` handles the waiting for us. And we can use a regular `try..catch` instead of `.catch`. That's usually (but not always) more convenient. + +But at the top level of the code, when we're outside any `async` function, we're syntactically unable to use `await`, so it's a normal practice to add `.then/catch` to handle the final result or falling-through error, like in the line `(*)` of the example above. +``` + +````smart header="`async/await` works well with `Promise.all`" +When we need to wait for multiple promises, we can wrap them in `Promise.all` and then `await`: + +```js +// wait for the array of results +let results = await Promise.all([ + fetch(url1), + fetch(url2), + ... +]); +``` + +In the case of an error, it propagates as usual, from the failed promise to `Promise.all`, and then becomes an exception that we can catch using `try..catch` around the call. + +```` + +## Summary + +The `async` keyword before a function has two effects: + +1. Makes it always return a promise. +2. Allows `await` to be used in it. + +The `await` keyword before a promise makes JavaScript wait until that promise settles, and then: + +1. If it's an error, an exception is generated — same as if `throw error` were called at that very place. +2. Otherwise, it returns the result. + +Together they provide a great framework to write asynchronous code that is easy to both read and write. + +With `async/await` we rarely need to write `promise.then/catch`, but we still shouldn't forget that they are based on promises, because sometimes (e.g. in the outermost scope) we have to use these methods. Also `Promise.all` is nice when we are waiting for many tasks simultaneously. diff --git a/6-async/05-async-await/head.html b/1-js/11-async/08-async-await/head.html similarity index 100% rename from 6-async/05-async-await/head.html rename to 1-js/11-async/08-async-await/head.html diff --git a/6-async/index.md b/1-js/11-async/index.md similarity index 100% rename from 6-async/index.md rename to 1-js/11-async/index.md diff --git a/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/_js.view/solution.js b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/_js.view/solution.js new file mode 100644 index 0000000000..45c8b071bb --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/_js.view/solution.js @@ -0,0 +1,9 @@ +function* pseudoRandom(seed) { + let value = seed; + + while(true) { + value = value * 16807 % 2147483647 + yield value; + } + +}; diff --git a/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/_js.view/test.js b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/_js.view/test.js new file mode 100644 index 0000000000..5f3a0ae87e --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/_js.view/test.js @@ -0,0 +1,21 @@ +describe("pseudoRandom", function() { + + it("follows the formula", function() { + let generator = pseudoRandom(1); + + assert.equal(generator.next().value, 16807); + assert.equal(generator.next().value, 282475249); + assert.equal(generator.next().value, 1622650073); + }); + + + it("returns same value for the same seed", function() { + let generator1 = pseudoRandom(123); + let generator2 = pseudoRandom(123); + + assert.deepEqual(generator1.next(), generator2.next()); + assert.deepEqual(generator1.next(), generator2.next()); + assert.deepEqual(generator1.next(), generator2.next()); + }); + +}); diff --git a/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/solution.md b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/solution.md new file mode 100644 index 0000000000..4355d0cfc0 --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/solution.md @@ -0,0 +1,38 @@ +```js run demo +function* pseudoRandom(seed) { + let value = seed; + + while(true) { + value = value * 16807 % 2147483647; + yield value; + } + +}; + +let generator = pseudoRandom(1); + +alert(generator.next().value); // 16807 +alert(generator.next().value); // 282475249 +alert(generator.next().value); // 1622650073 +``` + +Please note, the same can be done with a regular function, like this: + +```js run +function pseudoRandom(seed) { + let value = seed; + + return function() { + value = value * 16807 % 2147483647; + return value; + } +} + +let generator = pseudoRandom(1); + +alert(generator()); // 16807 +alert(generator()); // 282475249 +alert(generator()); // 1622650073 +``` + +That also works. But then we lose ability to iterate with `for..of` and to use generator composition, that may be useful elsewhere. diff --git a/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/task.md b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/task.md new file mode 100644 index 0000000000..e7c251ad36 --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/01-pseudo-random-generator/task.md @@ -0,0 +1,34 @@ + +# Pseudo-random generator + +There are many areas where we need random data. + +One of them is testing. We may need random data: text, numbers, etc. to test things out well. + +In JavaScript, we could use `Math.random()`. But if something goes wrong, we'd like to be able to repeat the test, using exactly the same data. + +For that, so called "seeded pseudo-random generators" are used. They take a "seed", the first value, and then generate the next ones using a formula so that the same seed yields the same sequence, and hence the whole flow is easily reproducible. We only need to remember the seed to repeat it. + +An example of such formula, that generates somewhat uniformly distributed values: + +``` +next = previous * 16807 % 2147483647 +``` + +If we use `1` as the seed, the values will be: +1. `16807` +2. `282475249` +3. `1622650073` +4. ...and so on... + +The task is to create a generator function `pseudoRandom(seed)` that takes `seed` and creates the generator with this formula. + +Usage example: + +```js +let generator = pseudoRandom(1); + +alert(generator.next().value); // 16807 +alert(generator.next().value); // 282475249 +alert(generator.next().value); // 1622650073 +``` diff --git a/1-js/12-generators-iterators/1-generators/article.md b/1-js/12-generators-iterators/1-generators/article.md new file mode 100644 index 0000000000..55f6bf9032 --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/article.md @@ -0,0 +1,483 @@ +# Generators + +Regular functions return only one, single value (or nothing). + +Generators can return ("yield") multiple values, one after another, on-demand. They work great with [iterables](info:iterable), allowing to create data streams with ease. + +## Generator functions + +To create a generator, we need a special syntax construct: `function*`, so-called "generator function". + +It looks like this: + +```js +function* generateSequence() { + yield 1; + yield 2; + return 3; +} +``` + +Generator functions behave differently from regular ones. When such function is called, it doesn't run its code. Instead it returns a special object, called "generator object", to manage the execution. + +Here, take a look: + +```js run +function* generateSequence() { + yield 1; + yield 2; + return 3; +} + +// "generator function" creates "generator object" +let generator = generateSequence(); +*!* +alert(generator); // [object Generator] +*/!* +``` + +The function code execution hasn't started yet: + +![](generateSequence-1.svg) + +The main method of a generator is `next()`. When called, it runs the execution until the nearest `yield ` statement (`value` can be omitted, then it's `undefined`). Then the function execution pauses, and the yielded `value` is returned to the outer code. + +The result of `next()` is always an object with two properties: +- `value`: the yielded value. +- `done`: `true` if the function code has finished, otherwise `false`. + +For instance, here we create the generator and get its first yielded value: + +```js run +function* generateSequence() { + yield 1; + yield 2; + return 3; +} + +let generator = generateSequence(); + +*!* +let one = generator.next(); +*/!* + +alert(JSON.stringify(one)); // {value: 1, done: false} +``` + +As of now, we got the first value only, and the function execution is on the second line: + +![](generateSequence-2.svg) + +Let's call `generator.next()` again. It resumes the code execution and returns the next `yield`: + +```js +let two = generator.next(); + +alert(JSON.stringify(two)); // {value: 2, done: false} +``` + +![](generateSequence-3.svg) + +And, if we call it a third time, the execution reaches the `return` statement that finishes the function: + +```js +let three = generator.next(); + +alert(JSON.stringify(three)); // {value: 3, *!*done: true*/!*} +``` + +![](generateSequence-4.svg) + +Now the generator is done. We should see it from `done:true` and process `value:3` as the final result. + +New calls to `generator.next()` don't make sense any more. If we do them, they return the same object: `{done: true}`. + +```smart header="`function* f(…)` or `function *f(…)`?" +Both syntaxes are correct. + +But usually the first syntax is preferred, as the star `*` denotes that it's a generator function, it describes the kind, not the name, so it should stick with the `function` keyword. +``` + +## Generators are iterable + +As you probably already guessed looking at the `next()` method, generators are [iterable](info:iterable). + +We can loop over their values using `for..of`: + +```js run +function* generateSequence() { + yield 1; + yield 2; + return 3; +} + +let generator = generateSequence(); + +for(let value of generator) { + alert(value); // 1, then 2 +} +``` + +Looks a lot nicer than calling `.next().value`, right? + +...But please note: the example above shows `1`, then `2`, and that's all. It doesn't show `3`! + +It's because `for..of` iteration ignores the last `value`, when `done: true`. So, if we want all results to be shown by `for..of`, we must return them with `yield`: + +```js run +function* generateSequence() { + yield 1; + yield 2; +*!* + yield 3; +*/!* +} + +let generator = generateSequence(); + +for(let value of generator) { + alert(value); // 1, then 2, then 3 +} +``` + +As generators are iterable, we can call all related functionality, e.g. the spread syntax `...`: + +```js run +function* generateSequence() { + yield 1; + yield 2; + yield 3; +} + +let sequence = [0, ...generateSequence()]; + +alert(sequence); // 0, 1, 2, 3 +``` + +In the code above, `...generateSequence()` turns the iterable generator object into an array of items (read more about the spread syntax in the chapter [](info:rest-parameters-spread#spread-syntax)) + +## Using generators for iterables + +Some time ago, in the chapter [](info:iterable) we created an iterable `range` object that returns values `from..to`. + +Here, let's remember the code: + +```js run +let range = { + from: 1, + to: 5, + + // for..of range calls this method once in the very beginning + [Symbol.iterator]() { + // ...it returns the iterator object: + // onward, for..of works only with that object, asking it for next values + return { + current: this.from, + last: this.to, + + // next() is called on each iteration by the for..of loop + next() { + // it should return the value as an object {done:.., value :...} + if (this.current <= this.last) { + return { done: false, value: this.current++ }; + } else { + return { done: true }; + } + } + }; + } +}; + +// iteration over range returns numbers from range.from to range.to +alert([...range]); // 1,2,3,4,5 +``` + +We can use a generator function for iteration by providing it as `Symbol.iterator`. + +Here's the same `range`, but much more compact: + +```js run +let range = { + from: 1, + to: 5, + + *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*() + for(let value = this.from; value <= this.to; value++) { + yield value; + } + } +}; + +alert( [...range] ); // 1,2,3,4,5 +``` + +That works, because `range[Symbol.iterator]()` now returns a generator, and generator methods are exactly what `for..of` expects: +- it has a `.next()` method +- that returns values in the form `{value: ..., done: true/false}` + +That's not a coincidence, of course. Generators were added to JavaScript language with iterators in mind, to implement them easily. + +The variant with a generator is much more concise than the original iterable code of `range`, and keeps the same functionality. + +```smart header="Generators may generate values forever" +In the examples above we generated finite sequences, but we can also make a generator that yields values forever. For instance, an unending sequence of pseudo-random numbers. + +That surely would require a `break` (or `return`) in `for..of` over such generator. Otherwise, the loop would repeat forever and hang. +``` + +## Generator composition + +Generator composition is a special feature of generators that allows to transparently "embed" generators in each other. + +For instance, we have a function that generates a sequence of numbers: + +```js +function* generateSequence(start, end) { + for (let i = start; i <= end; i++) yield i; +} +``` + +Now we'd like to reuse it to generate a more complex sequence: +- first, digits `0..9` (with character codes 48..57), +- followed by uppercase alphabet letters `A..Z` (character codes 65..90) +- followed by lowercase alphabet letters `a..z` (character codes 97..122) + +We can use this sequence e.g. to create passwords by selecting characters from it (could add syntax characters as well), but let's generate it first. + +In a regular function, to combine results from multiple other functions, we call them, store the results, and then join at the end. + +For generators, there's a special `yield*` syntax to "embed" (compose) one generator into another. + +The composed generator: + +```js run +function* generateSequence(start, end) { + for (let i = start; i <= end; i++) yield i; +} + +function* generatePasswordCodes() { + +*!* + // 0..9 + yield* generateSequence(48, 57); + + // A..Z + yield* generateSequence(65, 90); + + // a..z + yield* generateSequence(97, 122); +*/!* + +} + +let str = ''; + +for(let code of generatePasswordCodes()) { + str += String.fromCharCode(code); +} + +alert(str); // 0..9A..Za..z +``` + +The `yield*` directive *delegates* the execution to another generator. This term means that `yield* gen` iterates over the generator `gen` and transparently forwards its yields outside. As if the values were yielded by the outer generator. + +The result is the same as if we inlined the code from nested generators: + +```js run +function* generateSequence(start, end) { + for (let i = start; i <= end; i++) yield i; +} + +function* generateAlphaNum() { + +*!* + // yield* generateSequence(48, 57); + for (let i = 48; i <= 57; i++) yield i; + + // yield* generateSequence(65, 90); + for (let i = 65; i <= 90; i++) yield i; + + // yield* generateSequence(97, 122); + for (let i = 97; i <= 122; i++) yield i; +*/!* + +} + +let str = ''; + +for(let code of generateAlphaNum()) { + str += String.fromCharCode(code); +} + +alert(str); // 0..9A..Za..z +``` + +A generator composition is a natural way to insert a flow of one generator into another. It doesn't use extra memory to store intermediate results. + +## "yield" is a two-way street + +Until this moment, generators were similar to iterable objects, with a special syntax to generate values. But in fact they are much more powerful and flexible. + +That's because `yield` is a two-way street: it not only returns the result to the outside, but also can pass the value inside the generator. + +To do so, we should call `generator.next(arg)`, with an argument. That argument becomes the result of `yield`. + +Let's see an example: + +```js run +function* gen() { +*!* + // Pass a question to the outer code and wait for an answer + let result = yield "2 + 2 = ?"; // (*) +*/!* + + alert(result); +} + +let generator = gen(); + +let question = generator.next().value; // <-- yield returns the value + +generator.next(4); // --> pass the result into the generator +``` + +![](genYield2.svg) + +1. The first call `generator.next()` should be always made without an argument (the argument is ignored if passed). It starts the execution and returns the result of the first `yield "2+2=?"`. At this point the generator pauses the execution, while staying on the line `(*)`. +2. Then, as shown at the picture above, the result of `yield` gets into the `question` variable in the calling code. +3. On `generator.next(4)`, the generator resumes, and `4` gets in as the result: `let result = 4`. + +Please note, the outer code does not have to immediately call `next(4)`. It may take time. That's not a problem: the generator will wait. + +For instance: + +```js +// resume the generator after some time +setTimeout(() => generator.next(4), 1000); +``` + +As we can see, unlike regular functions, a generator and the calling code can exchange results by passing values in `next/yield`. + +To make things more obvious, here's another example, with more calls: + +```js run +function* gen() { + let ask1 = yield "2 + 2 = ?"; + + alert(ask1); // 4 + + let ask2 = yield "3 * 3 = ?" + + alert(ask2); // 9 +} + +let generator = gen(); + +alert( generator.next().value ); // "2 + 2 = ?" + +alert( generator.next(4).value ); // "3 * 3 = ?" + +alert( generator.next(9).done ); // true +``` + +The execution picture: + +![](genYield2-2.svg) + +1. The first `.next()` starts the execution... It reaches the first `yield`. +2. The result is returned to the outer code. +3. The second `.next(4)` passes `4` back to the generator as the result of the first `yield`, and resumes the execution. +4. ...It reaches the second `yield`, that becomes the result of the generator call. +5. The third `next(9)` passes `9` into the generator as the result of the second `yield` and resumes the execution that reaches the end of the function, so `done: true`. + +It's like a "ping-pong" game. Each `next(value)` (excluding the first one) passes a value into the generator, that becomes the result of the current `yield`, and then gets back the result of the next `yield`. + +## generator.throw + +As we observed in the examples above, the outer code may pass a value into the generator, as the result of `yield`. + +...But it can also initiate (throw) an error there. That's natural, as an error is a kind of result. + +To pass an error into a `yield`, we should call `generator.throw(err)`. In that case, the `err` is thrown in the line with that `yield`. + +For instance, here the yield of `"2 + 2 = ?"` leads to an error: + +```js run +function* gen() { + try { + let result = yield "2 + 2 = ?"; // (1) + + alert("The execution does not reach here, because the exception is thrown above"); + } catch(e) { + alert(e); // shows the error + } +} + +let generator = gen(); + +let question = generator.next().value; + +*!* +generator.throw(new Error("The answer is not found in my database")); // (2) +*/!* +``` + +The error, thrown into the generator at line `(2)` leads to an exception in line `(1)` with `yield`. In the example above, `try..catch` catches it and shows it. + +If we don't catch it, then just like any exception, it "falls out" the generator into the calling code. + +The current line of the calling code is the line with `generator.throw`, labelled as `(2)`. So we can catch it here, like this: + +```js run +function* generate() { + let result = yield "2 + 2 = ?"; // Error in this line +} + +let generator = generate(); + +let question = generator.next().value; + +*!* +try { + generator.throw(new Error("The answer is not found in my database")); +} catch(e) { + alert(e); // shows the error +} +*/!* +``` + +If we don't catch the error there, then, as usual, it falls through to the outer calling code (if any) and, if uncaught, kills the script. + +## generator.return + +`generator.return(value)` finishes the generator execution and return the given `value`. + +```js +function* gen() { + yield 1; + yield 2; + yield 3; +} + +const g = gen(); + +g.next(); // { value: 1, done: false } +g.return('foo'); // { value: "foo", done: true } +g.next(); // { value: undefined, done: true } +``` + +If we again use `generator.return()` in a completed generator, it will return that value again ([MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return)). + +Often we don't use it, as most of time we want to get all returning values, but it can be useful when we want to stop generator in a specific condition. + +## Summary + +- Generators are created by generator functions `function* f(…) {…}`. +- Inside generators (only) there exists a `yield` operator. +- The outer code and the generator may exchange results via `next/yield` calls. + +In modern JavaScript, generators are rarely used. But sometimes they come in handy, because the ability of a function to exchange data with the calling code during the execution is quite unique. And, surely, they are great for making iterable objects. + +Also, in the next chapter we'll learn async generators, which are used to read streams of asynchronously generated data (e.g paginated fetches over a network) in `for await ... of` loops. + +In web-programming we often work with streamed data, so that's another very important use case. diff --git a/1-js/12-generators-iterators/1-generators/genYield2-2.svg b/1-js/12-generators-iterators/1-generators/genYield2-2.svg new file mode 100644 index 0000000000..f45e69324d --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/genYield2-2.svg @@ -0,0 +1 @@ +"2 + 2 = ?""3 * 3 = ?". next ( 4 ). next ( 9 )GeneratorCalling code \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/genYield2.svg b/1-js/12-generators-iterators/1-generators/genYield2.svg new file mode 100644 index 0000000000..8d3e257c94 --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/genYield2.svg @@ -0,0 +1 @@ +question = "2 + 2 = ?"GeneratorCalling code.next(4) \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-1.svg b/1-js/12-generators-iterators/1-generators/generateSequence-1.svg new file mode 100644 index 0000000000..138df324cc --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/generateSequence-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-2.svg b/1-js/12-generators-iterators/1-generators/generateSequence-2.svg new file mode 100644 index 0000000000..7478543a4b --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/generateSequence-2.svg @@ -0,0 +1 @@ +{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 new file mode 100644 index 0000000000..d32b114f98 --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/generateSequence-3.svg @@ -0,0 +1 @@ +{value: 2, done: false} \ No newline at end of file diff --git a/1-js/12-generators-iterators/1-generators/generateSequence-4.svg b/1-js/12-generators-iterators/1-generators/generateSequence-4.svg new file mode 100644 index 0000000000..23049fcd21 --- /dev/null +++ b/1-js/12-generators-iterators/1-generators/generateSequence-4.svg @@ -0,0 +1 @@ +{value: 3, done: true} \ No newline at end of file diff --git a/1-js/12-generators-iterators/2-async-iterators-generators/article.md b/1-js/12-generators-iterators/2-async-iterators-generators/article.md new file mode 100644 index 0000000000..d4e9f78616 --- /dev/null +++ b/1-js/12-generators-iterators/2-async-iterators-generators/article.md @@ -0,0 +1,417 @@ + +# Async iteration and generators + +Asynchronous iteration allow us to iterate over data that comes asynchronously, on-demand. Like, for instance, when we download something chunk-by-chunk over a network. And asynchronous generators make it even more convenient. + +Let's see a simple example first, to grasp the syntax, and then review a real-life use case. + +## Recall iterables + +Let's recall the topic about iterables. + +The idea is that we have an object, such as `range` here: +```js +let range = { + from: 1, + to: 5 +}; +``` + +...And we'd like to use `for..of` loop on it, such as `for(value of range)`, to get values from `1` to `5`. + +In other words, we want to add an *iteration ability* to the object. + +That can be implemented using a special method with the name `Symbol.iterator`: + +- This method is called in by the `for..of` construct when the loop is started, and it should return an object with the `next` method. +- For each iteration, the `next()` method is invoked for the next value. +- The `next()` should return a value in the form `{done: true/false, value:}`, where `done:true` means the end of the loop. + +Here's an implementation for the iterable `range`: + +```js run +let range = { + from: 1, + to: 5, + +*!* + [Symbol.iterator]() { // called once, in the beginning of for..of +*/!* + return { + current: this.from, + last: this.to, + +*!* + next() { // called every iteration, to get the next value +*/!* + if (this.current <= this.last) { + return { done: false, value: this.current++ }; + } else { + return { done: true }; + } + } + }; + } +}; + +for(let value of range) { + alert(value); // 1 then 2, then 3, then 4, then 5 +} +``` + +If anything is unclear, please visit the chapter [](info:iterable), it gives all the details about regular iterables. + +## Async iterables + +Asynchronous iteration is needed when values come asynchronously: after `setTimeout` or another kind of delay. + +The most common case is that the object needs to make a network request to deliver the next value, we'll see a real-life example of it a bit later. + +To make an object iterable asynchronously: + +1. Use `Symbol.asyncIterator` instead of `Symbol.iterator`. +2. The `next()` method should return a promise (to be fulfilled with the next value). + - The `async` keyword handles it, we can simply make `async next()`. +3. To iterate over such an object, we should use a `for await (let item of iterable)` loop. + - Note the `await` word. + +As a starting example, let's make an iterable `range` object, similar like the one before, but now it will return values asynchronously, one per second. + +All we need to do is to perform a few replacements in the code above: + +```js run +let range = { + from: 1, + to: 5, + +*!* + [Symbol.asyncIterator]() { // (1) +*/!* + return { + current: this.from, + last: this.to, + +*!* + async next() { // (2) +*/!* + +*!* + // note: we can use "await" inside the async next: + await new Promise(resolve => setTimeout(resolve, 1000)); // (3) +*/!* + + if (this.current <= this.last) { + return { done: false, value: this.current++ }; + } else { + return { done: true }; + } + } + }; + } +}; + +(async () => { + +*!* + for await (let value of range) { // (4) + alert(value); // 1,2,3,4,5 + } +*/!* + +})() +``` + +As we can see, the structure is similar to regular iterators: + +1. To make an object asynchronously iterable, it must have a method `Symbol.asyncIterator` `(1)`. +2. This method must return the object with `next()` method returning a promise `(2)`. +3. The `next()` method doesn't have to be `async`, it may be a regular method returning a promise, but `async` allows us to use `await`, so that's convenient. Here we just delay for a second `(3)`. +4. To iterate, we use `for await(let value of range)` `(4)`, namely add "await" after "for". It calls `range[Symbol.asyncIterator]()` once, and then its `next()` for values. + +Here's a small table with the differences: + +| | Iterators | Async iterators | +|-------|-----------|-----------------| +| Object method to provide iterator | `Symbol.iterator` | `Symbol.asyncIterator` | +| `next()` return value is | any value | `Promise` | +| to loop, use | `for..of` | `for await..of` | + +````warn header="The spread syntax `...` doesn't work asynchronously" +Features that require regular, synchronous iterators, don't work with asynchronous ones. + +For instance, a spread syntax won't work: +```js +alert( [...range] ); // Error, no Symbol.iterator +``` + +That's natural, as it expects to find `Symbol.iterator`, not `Symbol.asyncIterator`. + +It's also the case for `for..of`: the syntax without `await` needs `Symbol.iterator`. +```` + +## Recall generators + +Now let's recall generators, as they allow to make iteration code much shorter. Most of the time, when we'd like to make an iterable, we'll use generators. + +For sheer simplicity, omitting some important stuff, they are "functions that generate (yield) values". They are explained in detail in the chapter [](info:generators). + +Generators are labelled with `function*` (note the star) and use `yield` to generate a value, then we can use `for..of` to loop over them. + +This example generates a sequence of values from `start` to `end`: + +```js run +function* generateSequence(start, end) { + for (let i = start; i <= end; i++) { + yield i; + } +} + +for(let value of generateSequence(1, 5)) { + alert(value); // 1, then 2, then 3, then 4, then 5 +} +``` + +As we already know, to make an object iterable, we should add `Symbol.iterator` to it. + +```js +let range = { + from: 1, + to: 5, +*!* + [Symbol.iterator]() { + return + } +*/!* +} +``` + +A common practice for `Symbol.iterator` is to return a generator, it makes the code shorter, as you can see: + +```js run +let range = { + from: 1, + to: 5, + + *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*() + for(let value = this.from; value <= this.to; value++) { + yield value; + } + } +}; + +for(let value of range) { + alert(value); // 1, then 2, then 3, then 4, then 5 +} +``` + +Please see the chapter [](info:generators) if you'd like more details. + +In regular generators we can't use `await`. All values must come synchronously, as required by the `for..of` construct. + +What if we'd like to generate values asynchronously? From network requests, for instance. + +Let's switch to asynchronous generators to make it possible. + +## Async generators (finally) + +For most practical applications, when we'd like to make an object that asynchronously generates a sequence of values, we can use an asynchronous generator. + +The syntax is simple: prepend `function*` with `async`. That makes the generator asynchronous. + +And then use `for await (...)` to iterate over it, like this: + +```js run +*!*async*/!* function* generateSequence(start, end) { + + for (let i = start; i <= end; i++) { + +*!* + // Wow, can use await! + await new Promise(resolve => setTimeout(resolve, 1000)); +*/!* + + yield i; + } + +} + +(async () => { + + let generator = generateSequence(1, 5); + for *!*await*/!* (let value of generator) { + alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between) + } + +})(); +``` + +As the generator is asynchronous, we can use `await` inside it, rely on promises, perform network requests and so on. + +````smart header="Under-the-hood difference" +Technically, if you're an advanced reader who remembers the details about generators, there's an internal difference. + +For async generators, the `generator.next()` method is asynchronous, it returns promises. + +In a regular generator we'd use `result = generator.next()` to get values. In an async generator, we should add `await`, like this: + +```js +result = await generator.next(); // result = {value: ..., done: true/false} +``` +That's why async generators work with `for await...of`. +```` + +### Async iterable range + +Regular generators can be used as `Symbol.iterator` to make the iteration code shorter. + +Similar to that, async generators can be used as `Symbol.asyncIterator` to implement the asynchronous iteration. + +For instance, we can make the `range` object generate values asynchronously, once per second, by replacing synchronous `Symbol.iterator` with asynchronous `Symbol.asyncIterator`: + +```js run +let range = { + from: 1, + to: 5, + + // this line is same as [Symbol.asyncIterator]: async function*() { +*!* + async *[Symbol.asyncIterator]() { +*/!* + for(let value = this.from; value <= this.to; value++) { + + // make a pause between values, wait for something + await new Promise(resolve => setTimeout(resolve, 1000)); + + yield value; + } + } +}; + +(async () => { + + for *!*await*/!* (let value of range) { + alert(value); // 1, then 2, then 3, then 4, then 5 + } + +})(); +``` + +Now values come with a delay of 1 second between them. + +```smart +Technically, we can add both `Symbol.iterator` and `Symbol.asyncIterator` to the object, so it's both synchronously (`for..of`) and asynchronously (`for await..of`) iterable. + +In practice though, that would be a weird thing to do. +``` + +## Real-life example: paginated data + +So far we've seen basic examples, to gain understanding. Now let's review a real-life use case. + +There are many online services that deliver paginated data. For instance, when we need a list of users, a request returns a pre-defined count (e.g. 100 users) - "one page", and provides a URL to the next page. + +This pattern is very common. It's not about users, but just about anything. + +For instance, GitHub allows us to retrieve commits in the same, paginated fashion: + +- We should make a request to `fetch` in the form `https://api.github.com/repos//commits`. +- It responds with a JSON of 30 commits, and also provides a link to the next page in the `Link` header. +- Then we can use that link for the next request, to get more commits, and so on. + +For our code, we'd like to have a simpler way to get commits. + +Let's make a function `fetchCommits(repo)` that gets commits for us, making requests whenever needed. And let it care about all pagination stuff. For us it'll be a simple async iteration `for await..of`. + +So the usage will be like this: + +```js +for await (let commit of fetchCommits("username/repository")) { + // process commit +} +``` + +Here's such function, implemented as async generator: + +```js +async function* fetchCommits(repo) { + let url = `https://api.github.com/repos/${repo}/commits`; + + while (url) { + const response = await fetch(url, { // (1) + headers: {'User-Agent': 'Our script'}, // github needs any user-agent header + }); + + const body = await response.json(); // (2) response is JSON (array of commits) + + // (3) the URL of the next page is in the headers, extract it + let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/); + nextPage = nextPage?.[1]; + + url = nextPage; + + for(let commit of body) { // (4) yield commits one by one, until the page ends + yield commit; + } + } +} +``` + +More explanations about how it works: + +1. We use the browser [fetch](info:fetch) method to download the commits. + + - The initial URL is `https://api.github.com/repos//commits`, and the next page will be in the `Link` header of the response. + - The `fetch` method allows us to supply authorization and other headers if needed -- here GitHub requires `User-Agent`. +2. The commits are returned in JSON format. +3. We should get the next page URL from the `Link` header of the response. It has a special format, so we use a regular expression for that (we will learn this feature in [Regular expressions](info:regular-expressions)). + - The next page URL may look like `https://api.github.com/repositories/93253246/commits?page=2`. It's generated by GitHub itself. +4. Then we yield the received commits one by one, and when they finish, the next `while(url)` iteration will trigger, making one more request. + +An example of use (shows commit authors in console): + +```js run +(async () => { + + let count = 0; + + for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) { + + console.log(commit.author.login); + + if (++count == 100) { // let's stop at 100 commits + break; + } + } + +})(); + +// Note: If you are running this in an external sandbox, you'll need to paste here the function fetchCommits described above +``` + +That's just what we wanted. + +The internal mechanics of paginated requests is invisible from the outside. For us it's just an async generator that returns commits. + +## Summary + +Regular iterators and generators work fine with the data that doesn't take time to generate. + +When we expect the data to come asynchronously, with delays, their async counterparts can be used, and `for await..of` instead of `for..of`. + +Syntax differences between async and regular iterators: + +| | Iterable | Async Iterable | +|-------|-----------|-----------------| +| Method to provide iterator | `Symbol.iterator` | `Symbol.asyncIterator` | +| `next()` return value is | `{value:…, done: true/false}` | `Promise` that resolves to `{value:…, done: true/false}` | + +Syntax differences between async and regular generators: + +| | Generators | Async generators | +|-------|-----------|-----------------| +| Declaration | `function*` | `async function*` | +| `next()` return value is | `{value:…, done: true/false}` | `Promise` that resolves to `{value:…, done: true/false}` | + +In web-development we often meet streams of data, when it flows chunk-by-chunk. For instance, downloading or uploading a big file. + +We can use async generators to process such data. It's also noteworthy that in some environments, like in browsers, there's also another API called Streams, that provides special interfaces to work with such streams, to transform the data and to pass it from one stream to another (e.g. download from one place and immediately send elsewhere). diff --git a/1-js/12-generators-iterators/2-async-iterators-generators/head.html b/1-js/12-generators-iterators/2-async-iterators-generators/head.html new file mode 100644 index 0000000000..03f21e2bd8 --- /dev/null +++ b/1-js/12-generators-iterators/2-async-iterators-generators/head.html @@ -0,0 +1,24 @@ + diff --git a/1-js/12-generators-iterators/index.md b/1-js/12-generators-iterators/index.md new file mode 100644 index 0000000000..ccc909d1aa --- /dev/null +++ b/1-js/12-generators-iterators/index.md @@ -0,0 +1,2 @@ + +# Generators, advanced iteration diff --git a/1-js/13-modules/01-modules-intro/article.md b/1-js/13-modules/01-modules-intro/article.md new file mode 100644 index 0000000000..5ad70d1519 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/article.md @@ -0,0 +1,418 @@ + +# Modules, introduction + +As our application grows bigger, we want to split it into multiple files, so called "modules". A module may contain a class or a library of functions for a specific purpose. + +For a long time, JavaScript existed without a language-level module syntax. That wasn't a problem, because initially scripts were small and simple, so there was no need. + +But eventually scripts became more and more complex, so the community invented a variety of ways to organize code into modules, special libraries to load modules on demand. + +To name some (for historical reasons): + +- [AMD](https://en.wikipedia.org/wiki/Asynchronous_module_definition) -- one of the most ancient module systems, initially implemented by the library [require.js](https://requirejs.org/). +- [CommonJS](https://wiki.commonjs.org/wiki/Modules/1.1) -- the module system created for Node.js server. +- [UMD](https://github.com/umdjs/umd) -- one more module system, suggested as a universal one, compatible with AMD and CommonJS. + +Now these all slowly became a part of history, but we still can find them in old scripts. + +The language-level module system appeared in the standard in 2015, gradually evolved since then, and is now supported by all major browsers and in Node.js. So we'll study the modern JavaScript modules from now on. + +## What is a module? + +A module is just a file. One script is one module. As simple as that. + +Modules can load each other and use special directives `export` and `import` to interchange functionality, call functions of one module from another one: + +- `export` keyword labels variables and functions that should be accessible from outside the current module. +- `import` allows the import of functionality from other modules. + +For instance, if we have a file `sayHi.js` exporting a function: + +```js +// 📁 sayHi.js +export function sayHi(user) { + alert(`Hello, ${user}!`); +} +``` + +...Then another file may import and use it: + +```js +// 📁 main.js +import {sayHi} from './sayHi.js'; + +alert(sayHi); // function... +sayHi('John'); // Hello, John! +``` + +The `import` directive loads the module by path `./sayHi.js` relative to the current file, and assigns exported function `sayHi` to the corresponding variable. + +Let's run the example in-browser. + +As modules support special keywords and features, we must tell the browser that a script should be treated as a module, by using the attribute ` +``` + +### Module-level scope + +Each module has its own top-level scope. In other words, top-level variables and functions from a module are not seen in other scripts. + +In the example below, two scripts are imported, and `hello.js` tries to use `user` variable declared in `user.js`. It fails, because it's a separate module (you'll see the error in the console): + +[codetabs src="scopes" height="140" current="index.html"] + +Modules should `export` what they want to be accessible from outside and `import` what they need. + +- `user.js` should export the `user` variable. +- `hello.js` should import it from `user.js` module. + +In other words, with modules we use import/export instead of relying on global variables. + +This is the correct variant: + +[codetabs src="scopes-working" height="140" current="hello.js"] + +In the browser, if we talk about HTML pages, independent top-level scope also exists for each ` + + +``` + +```smart +In the browser, we can make a variable window-level global by explicitly assigning it to a `window` property, e.g. `window.user = "John"`. + +Then all scripts will see it, both with `type="module"` and without it. + +That said, making such global variables is frowned upon. Please try to avoid them. +``` + +### A module code is evaluated only the first time when imported + +If the same module is imported into multiple other modules, its code is executed only once, upon the first import. Then its exports are given to all further importers. + +The one-time evaluation has important consequences, that we should be aware of. + +Let's see a couple of examples. + +First, if executing a module code brings side-effects, like showing a message, then importing it multiple times will trigger it only once -- the first time: + +```js +// 📁 alert.js +alert("Module is evaluated!"); +``` + +```js +// Import the same module from different files + +// 📁 1.js +import `./alert.js`; // Module is evaluated! + +// 📁 2.js +import `./alert.js`; // (shows nothing) +``` + +The second import shows nothing, because the module has already been evaluated. + +There's a rule: top-level module code should be used for initialization, creation of module-specific internal data structures. If we need to make something callable multiple times - we should export it as a function, like we did with `sayHi` above. + +Now, let's consider a deeper example. + +Let's say, a module exports an object: + +```js +// 📁 admin.js +export let admin = { + name: "John" +}; +``` + +If this module is imported from multiple files, the module is only evaluated the first time, `admin` object is created, and then passed to all further importers. + +All importers get exactly the one and only `admin` object: + +```js +// 📁 1.js +import {admin} from './admin.js'; +admin.name = "Pete"; + +// 📁 2.js +import {admin} from './admin.js'; +alert(admin.name); // Pete + +*!* +// Both 1.js and 2.js reference the same admin object +// Changes made in 1.js are visible in 2.js +*/!* +``` + +As you can see, when `1.js` changes the `name` property in the imported `admin`, then `2.js` can see the new `admin.name`. + +That's exactly because the module is executed only once. Exports are generated, and then they are shared between importers, so if something changes the `admin` object, other importers will see that. + +**Such behavior is actually very convenient, because it allows us to *configure* modules.** + +In other words, a module can provide a generic functionality that needs a setup. E.g. authentication needs credentials. Then it can export a configuration object expecting the outer code to assign to it. + +Here's the classical pattern: +1. A module exports some means of configuration, e.g. a configuration object. +2. On the first import we initialize it, write to its properties. The top-level application script may do that. +3. Further imports use the module. + +For instance, the `admin.js` module may provide certain functionality (e.g. authentication), but expect the credentials to come into the `config` object from outside: + +```js +// 📁 admin.js +export let config = { }; + +export function sayHi() { + alert(`Ready to serve, ${config.user}!`); +} +``` + +Here, `admin.js` exports the `config` object (initially empty, but may have default properties too). + +Then in `init.js`, the first script of our app, we import `config` from it and set `config.user`: + +```js +// 📁 init.js +import {config} from './admin.js'; +config.user = "Pete"; +``` + +...Now the module `admin.js` is configured. + +Further importers can call it, and it correctly shows the current user: + +```js +// 📁 another.js +import {sayHi} from './admin.js'; + +sayHi(); // Ready to serve, *!*Pete*/!*! +``` + + +### import.meta + +The object `import.meta` contains the information about the current module. + +Its content depends on the environment. In the browser, it contains the URL of the script, or a current webpage URL if inside HTML: + +```html run height=0 + +``` + +### In a module, "this" is undefined + +That's kind of a minor feature, but for completeness we should mention it. + +In a module, top-level `this` is undefined. + +Compare it to non-module scripts, where `this` is a global object: + +```html run height=0 + + + +``` + +## Browser-specific features + +There are also several browser-specific differences of scripts with `type="module"` compared to regular ones. + +You may want to skip this section for now if you're reading for the first time, or if you don't use JavaScript in a browser. + +### Module scripts are deferred + +Module scripts are *always* deferred, same effect as `defer` attribute (described in the chapter [](info:script-async-defer)), for both external and inline scripts. + +In other words: +- downloading external module scripts ` + +Compare to regular script below: + + + + +``` + +Please note: the second script actually runs before the first! So we'll see `undefined` first, and then `object`. + +That's because modules are deferred, so we wait for the document to be processed. The regular script runs immediately, so we see its output first. + +When using modules, we should be aware that the HTML page shows up as it loads, and JavaScript modules run after that, so the user may see the page before the JavaScript application is ready. Some functionality may not work yet. We should put "loading indicators", or otherwise ensure that the visitor won't be confused by that. + +### Async works on inline scripts + +For non-module scripts, the `async` attribute only works on external scripts. Async scripts run immediately when ready, independently of other scripts or the HTML document. + +For module scripts, it works on inline scripts as well. + +For example, the inline script below has `async`, so it doesn't wait for anything. + +It performs the import (fetches `./analytics.js`) and runs when ready, even if the HTML document is not finished yet, or if other scripts are still pending. + +That's good for functionality that doesn't depend on anything, like counters, ads, document-level event listeners. + +```html + + + +``` + +### External scripts + +External scripts that have `type="module"` are different in two aspects: + +1. External scripts with the same `src` run only once: + ```html + + + + ``` + +2. External scripts that are fetched from another origin (e.g. another site) require [CORS](mdn:Web/HTTP/CORS) headers, as described in the chapter . In other words, if a module script is fetched from another origin, the remote server must supply a header `Access-Control-Allow-Origin` allowing the fetch. + ```html + + + + ``` + + That ensures better security by default. + +### No "bare" modules allowed + +In the browser, `import` must get either a relative or absolute URL. Modules without any path are called "bare" modules. Such modules are not allowed in `import`. + +For instance, this `import` is invalid: +```js +import {sayHi} from 'sayHi'; // Error, "bare" module +// the module must have a path, e.g. './sayHi.js' or wherever the module is +``` + +Certain environments, like Node.js or bundle tools allow bare modules, without any path, as they have their own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet. + +### Compatibility, "nomodule" + +Old browsers do not understand `type="module"`. Scripts of an unknown type are just ignored. For them, it's possible to provide a fallback using the `nomodule` attribute: + +```html run + + + +``` + +## Build tools + +In real-life, browser modules are rarely used in their "raw" form. Usually, we bundle them together with a special tool such as [Webpack](https://webpack.js.org/) and deploy to the production server. + +One of the benefits of using bundlers -- they give more control over how modules are resolved, allowing bare modules and much more, like CSS/HTML modules. + +Build tools do the following: + +1. Take a "main" module, the one intended to be put in ` +``` + +That said, native modules are also usable. So we won't be using Webpack here: you can configure it later. + +## Summary + +To summarize, the core concepts are: + +1. A module is a file. To make `import/export` work, browsers need ` diff --git a/1-js/13-modules/01-modules-intro/say.view/say.js b/1-js/13-modules/01-modules-intro/say.view/say.js new file mode 100644 index 0000000000..198a3be6da --- /dev/null +++ b/1-js/13-modules/01-modules-intro/say.view/say.js @@ -0,0 +1,3 @@ +export function sayHi(user) { + return `Hello, ${user}!`; +} diff --git a/1-js/13-modules/01-modules-intro/scopes-working.view/hello.js b/1-js/13-modules/01-modules-intro/scopes-working.view/hello.js new file mode 100644 index 0000000000..6c087ea81a --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes-working.view/hello.js @@ -0,0 +1,3 @@ +import {user} from './user.js'; + +document.body.innerHTML = user; // John diff --git a/1-js/13-modules/01-modules-intro/scopes-working.view/index.html b/1-js/13-modules/01-modules-intro/scopes-working.view/index.html new file mode 100644 index 0000000000..b78f759126 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes-working.view/index.html @@ -0,0 +1,2 @@ + + diff --git a/1-js/13-modules/01-modules-intro/scopes-working.view/user.js b/1-js/13-modules/01-modules-intro/scopes-working.view/user.js new file mode 100644 index 0000000000..d289329c61 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes-working.view/user.js @@ -0,0 +1 @@ +export let user = "John"; diff --git a/1-js/13-modules/01-modules-intro/scopes.view/hello.js b/1-js/13-modules/01-modules-intro/scopes.view/hello.js new file mode 100644 index 0000000000..714aafa1f1 --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes.view/hello.js @@ -0,0 +1 @@ +alert(user); // no such variable (each module has independent variables) diff --git a/1-js/13-modules/01-modules-intro/scopes.view/index.html b/1-js/13-modules/01-modules-intro/scopes.view/index.html new file mode 100644 index 0000000000..a87e96fdfb --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes.view/index.html @@ -0,0 +1,3 @@ + + + diff --git a/1-js/13-modules/01-modules-intro/scopes.view/user.js b/1-js/13-modules/01-modules-intro/scopes.view/user.js new file mode 100644 index 0000000000..12ec850d9a --- /dev/null +++ b/1-js/13-modules/01-modules-intro/scopes.view/user.js @@ -0,0 +1 @@ +let user = "John"; diff --git a/1-js/13-modules/02-import-export/article.md b/1-js/13-modules/02-import-export/article.md new file mode 100644 index 0000000000..1b5649c69c --- /dev/null +++ b/1-js/13-modules/02-import-export/article.md @@ -0,0 +1,454 @@ +# Export and Import + +Export and import directives have several syntax variants. + +In the previous article we saw a simple use, now let's explore more examples. + +## Export before declarations + +We can label any declaration as exported by placing `export` before it, be it a variable, function or a class. + +For instance, here all exports are valid: + +```js +// export an array +*!*export*/!* let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +// export a constant +*!*export*/!* const MODULES_BECAME_STANDARD_YEAR = 2015; + +// export a class +*!*export*/!* class User { + constructor(name) { + this.name = name; + } +} +``` + +````smart header="No semicolons after export class/function" +Please note that `export` before a class or a function does not make it a [function expression](info:function-expressions). It's still a function declaration, albeit exported. + +Most JavaScript style guides don't recommend semicolons after function and class declarations. + +That's why there's no need for a semicolon at the end of `export class` and `export function`: + +```js +export function sayHi(user) { + alert(`Hello, ${user}!`); +} *!* // no ; at the end */!* +``` + +```` + +## Export apart from declarations + +Also, we can put `export` separately. + +Here we first declare, and then export: + +```js +// 📁 say.js +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +function sayBye(user) { + alert(`Bye, ${user}!`); +} + +*!* +export {sayHi, sayBye}; // a list of exported variables +*/!* +``` + +...Or, technically we could put `export` above functions as well. + +## Import * + +Usually, we put a list of what to import in curly braces `import {...}`, like this: + +```js +// 📁 main.js +*!* +import {sayHi, sayBye} from './say.js'; +*/!* + +sayHi('John'); // Hello, John! +sayBye('John'); // Bye, John! +``` + +But if there's a lot to import, we can import everything as an object using `import * as `, for instance: + +```js +// 📁 main.js +*!* +import * as say from './say.js'; +*/!* + +say.sayHi('John'); +say.sayBye('John'); +``` + +At first sight, "import everything" seems such a cool thing, short to write, why should we ever explicitly list what we need to import? + +Well, there are few reasons. + +1. Explicitly listing what to import gives shorter names: `sayHi()` instead of `say.sayHi()`. +2. Explicit list of imports gives better overview of the code structure: what is used and where. It makes code support and refactoring easier. + +```smart header="Don't be afraid to import too much" +Modern build tools, such as [webpack](https://webpack.js.org/) and others, bundle modules together and optimize them to speedup loading. They also remove unused imports. + +For instance, if you `import * as library` from a huge code library, and then use only few methods, then unused ones [will not be included](https://github.com/webpack/webpack/tree/main/examples/harmony-unused#examplejs) into the optimized bundle. +``` + +## Import "as" + +We can also use `as` to import under different names. + +For instance, let's import `sayHi` into the local variable `hi` for brevity, and import `sayBye` as `bye`: + +```js +// 📁 main.js +*!* +import {sayHi as hi, sayBye as bye} from './say.js'; +*/!* + +hi('John'); // Hello, John! +bye('John'); // Bye, John! +``` + +## Export "as" + +The similar syntax exists for `export`. + +Let's export functions as `hi` and `bye`: + +```js +// 📁 say.js +... +export {sayHi as hi, sayBye as bye}; +``` + +Now `hi` and `bye` are official names for outsiders, to be used in imports: + +```js +// 📁 main.js +import * as say from './say.js'; + +say.*!*hi*/!*('John'); // Hello, John! +say.*!*bye*/!*('John'); // Bye, John! +``` + +## Export default + +In practice, there are mainly two kinds of modules. + +1. Modules that contain a library, pack of functions, like `say.js` above. +2. Modules that declare a single entity, e.g. a module `user.js` exports only `class User`. + +Mostly, the second approach is preferred, so that every "thing" resides in its own module. + +Naturally, that requires a lot of files, as everything wants its own module, but that's not a problem at all. Actually, code navigation becomes easier if files are well-named and structured into folders. + +Modules provide a special `export default` ("the default export") syntax to make the "one thing per module" way look better. + +Put `export default` before the entity to export: + +```js +// 📁 user.js +export *!*default*/!* class User { // just add "default" + constructor(name) { + this.name = name; + } +} +``` + +There may be only one `export default` per file. + +...And then import it without curly braces: + +```js +// 📁 main.js +import *!*User*/!* from './user.js'; // not {User}, just User + +new User('John'); +``` + +Imports without curly braces look nicer. A common mistake when starting to use modules is to forget curly braces at all. So, remember, `import` needs curly braces for named exports and doesn't need them for the default one. + +| Named export | Default export | +|--------------|----------------| +| `export class User {...}` | `export default class User {...}` | +| `import {User} from ...` | `import User from ...`| + +Technically, we may have both default and named exports in a single module, but in practice people usually don't mix them. A module has either named exports or the default one. + +As there may be at most one default export per file, the exported entity may have no name. + +For instance, these are all perfectly valid default exports: + +```js +export default class { // no class name + constructor() { ... } +} +``` + +```js +export default function(user) { // no function name + alert(`Hello, ${user}!`); +} +``` + +```js +// export a single value, without making a variable +export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +``` + +Not giving a name is fine, because there is only one `export default` per file, so `import` without curly braces knows what to import. + +Without `default`, such an export would give an error: + +```js +export class { // Error! (non-default export needs a name) + constructor() {} +} +``` + +### The "default" name + +In some situations the `default` keyword is used to reference the default export. + +For example, to export a function separately from its definition: + +```js +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +// same as if we added "export default" before the function +export {sayHi as default}; +``` + +Or, another situation, let's say a module `user.js` exports one main "default" thing, and a few named ones (rarely the case, but it happens): + +```js +// 📁 user.js +export default class User { + constructor(name) { + this.name = name; + } +} + +export function sayHi(user) { + alert(`Hello, ${user}!`); +} +``` + +Here's how to import the default export along with a named one: + +```js +// 📁 main.js +import {*!*default as User*/!*, sayHi} from './user.js'; + +new User('John'); +``` + +And, finally, if importing everything `*` as an object, then the `default` property is exactly the default export: + +```js +// 📁 main.js +import * as user from './user.js'; + +let User = user.default; // the default export +new User('John'); +``` + +### A word against default exports + +Named exports are explicit. They exactly name what they import, so we have that information from them; that's a good thing. + +Named exports force us to use exactly the right name to import: + +```js +import {User} from './user.js'; +// import {MyUser} won't work, the name must be {User} +``` + +...While for a default export, we always choose the name when importing: + +```js +import User from './user.js'; // works +import MyUser from './user.js'; // works too +// could be import Anything... and it'll still work +``` + +So team members may use different names to import the same thing, and that's not good. + +Usually, to avoid that and keep the code consistent, there's a rule that imported variables should correspond to file names, e.g: + +```js +import User from './user.js'; +import LoginForm from './loginForm.js'; +import func from '/path/to/func.js'; +... +``` + +Still, some teams consider it a serious drawback of default exports. So they prefer to always use named exports. Even if only a single thing is exported, it's still exported under a name, without `default`. + +That also makes re-export (see below) a little bit easier. + +## Re-export + +"Re-export" syntax `export ... from ...` allows to import things and immediately export them (possibly under another name), like this: + +```js +export {sayHi} from './say.js'; // re-export sayHi + +export {default as User} from './user.js'; // re-export default +``` + +Why would that be needed? Let's see a practical use case. + +Imagine, we're writing a "package": a folder with a lot of modules, with some of the functionality exported outside (tools like NPM allow us to publish and distribute such packages, but we don't have to use them), and many modules are just "helpers", for internal use in other package modules. + +The file structure could be like this: +``` +auth/ + index.js + user.js + helpers.js + tests/ + login.js + providers/ + github.js + facebook.js + ... +``` + +We'd like to expose the package functionality via a single entry point. + +In other words, a person who would like to use our package, should import only from the "main file" `auth/index.js`. + +Like this: + +```js +import {login, logout} from 'auth/index.js' +``` + +The "main file", `auth/index.js` exports all the functionality that we'd like to provide in our package. + +The idea is that outsiders, other programmers who use our package, should not meddle with its internal structure, search for files inside our package folder. We export only what's necessary in `auth/index.js` and keep the rest hidden from prying eyes. + +As the actual exported functionality is scattered among the package, we can import it into `auth/index.js` and export from it: + +```js +// 📁 auth/index.js + +// import login/logout and immediately export them +import {login, logout} from './helpers.js'; +export {login, logout}; + +// import default as User and export it +import User from './user.js'; +export {User}; +... +``` + +Now users of our package can `import {login} from "auth/index.js"`. + +The syntax `export ... from ...` is just a shorter notation for such import-export: + +```js +// 📁 auth/index.js +// re-export login/logout +export {login, logout} from './helpers.js'; + +// re-export the default export as User +export {default as User} from './user.js'; +... +``` + +The notable difference of `export ... from` compared to `import/export` is that re-exported modules aren't available in the current file. So inside the above example of `auth/index.js` we can't use re-exported `login/logout` functions. + +### Re-exporting the default export + +The default export needs separate handling when re-exporting. + +Let's say we have `user.js` with the `export default class User` and would like to re-export it: + +```js +// 📁 user.js +export default class User { + // ... +} +``` + +We can come across two problems with it: + +1. `export User from './user.js'` won't work. That would lead to a syntax error. + + To re-export the default export, we have to write `export {default as User}`, as in the example above. + +2. `export * from './user.js'` re-exports only named exports, but ignores the default one. + + If we'd like to re-export both named and default exports, then two statements are needed: + ```js + export * from './user.js'; // to re-export named exports + export {default} from './user.js'; // to re-export the default export + ``` + +Such oddities of re-exporting a default export are one of the reasons why some developers don't like default exports and prefer named ones. + +## Summary + +Here are all types of `export` that we covered in this and previous articles. + +You can check yourself by reading them and recalling what they mean: + +- Before declaration of a class/function/..: + - `export [default] class/function/variable ...` +- Standalone export: + - `export {x [as y], ...}`. +- Re-export: + - `export {x [as y], ...} from "module"` + - `export * from "module"` (doesn't re-export default). + - `export {default [as y]} from "module"` (re-export default). + +Import: + +- Importing named exports: + - `import {x [as y], ...} from "module"` +- Importing the default export: + - `import x from "module"` + - `import {default as x} from "module"` +- Import all: + - `import * as obj from "module"` +- Import the module (its code runs), but do not assign any of its exports to variables: + - `import "module"` + +We can put `import/export` statements at the top or at the bottom of a script, that doesn't matter. + +So, technically this code is fine: +```js +sayHi(); + +// ... + +import {sayHi} from './say.js'; // import at the end of the file +``` + +In practice imports are usually at the start of the file, but that's only for more convenience. + +**Please note that import/export statements don't work if inside `{...}`.** + +A conditional import, like this, won't work: +```js +if (something) { + import {sayHi} from "./say.js"; // Error: import must be at top level +} +``` + +...But what if we really need to import something conditionally? Or at the right time? Like, load a module upon request, when it's really needed? + +We'll see dynamic imports in the next article. diff --git a/1-js/13-modules/03-modules-dynamic-imports/article.md b/1-js/13-modules/03-modules-dynamic-imports/article.md new file mode 100644 index 0000000000..e48144a3ef --- /dev/null +++ b/1-js/13-modules/03-modules-dynamic-imports/article.md @@ -0,0 +1,98 @@ +# Dynamic imports + +Export and import statements that we covered in previous chapters are called "static". The syntax is very simple and strict. + +First, we can't dynamically generate any parameters of `import`. + +The module path must be a primitive string, can't be a function call. This won't work: + +```js +import ... from *!*getModuleName()*/!*; // Error, only from "string" is allowed +``` + +Second, we can't import conditionally or at run-time: + +```js +if(...) { + import ...; // Error, not allowed! +} + +{ + import ...; // Error, we can't put import in any block +} +``` + +That's because `import`/`export` aim to provide a backbone for the code structure. That's a good thing, as code structure can be analyzed, modules can be gathered and bundled into one file by special tools, unused exports can be removed ("tree-shaken"). That's possible only because the structure of imports/exports is simple and fixed. + +But how can we import a module dynamically, on-demand? + +## The import() expression + +The `import(module)` expression loads the module and returns a promise that resolves into a module object that contains all its exports. It can be called from any place in the code. + +We can use it dynamically in any place of the code, for instance: + +```js +let modulePath = prompt("Which module to load?"); + +import(modulePath) + .then(obj => ) + .catch(err => ) +``` + +Or, we could use `let module = await import(modulePath)` if inside an async function. + +For instance, if we have the following module `say.js`: + +```js +// 📁 say.js +export function hi() { + alert(`Hello`); +} + +export function bye() { + alert(`Bye`); +} +``` + +...Then dynamic import can be like this: + +```js +let {hi, bye} = await import('./say.js'); + +hi(); +bye(); +``` + +Or, if `say.js` has the default export: + +```js +// 📁 say.js +export default function() { + alert("Module loaded (export default)!"); +} +``` + +...Then, in order to access it, we can use `default` property of the module object: + +```js +let obj = await import('./say.js'); +let say = obj.default; +// or, in one line: let {default: say} = await import('./say.js'); + +say(); +``` + +Here's the full example: + +[codetabs src="say" current="index.html"] + +```smart +Dynamic imports work in regular scripts, they don't require `script type="module"`. +``` + +```smart +Although `import()` looks like a function call, it's a special syntax that just happens to use parentheses (similar to `super()`). + +So we can't copy `import` to a variable or use `call/apply` with it. It's not a function. +``` diff --git a/1-js/13-modules/03-modules-dynamic-imports/say.view/index.html b/1-js/13-modules/03-modules-dynamic-imports/say.view/index.html new file mode 100644 index 0000000000..80909cf94d --- /dev/null +++ b/1-js/13-modules/03-modules-dynamic-imports/say.view/index.html @@ -0,0 +1,10 @@ + + + diff --git a/1-js/13-modules/03-modules-dynamic-imports/say.view/say.js b/1-js/13-modules/03-modules-dynamic-imports/say.view/say.js new file mode 100644 index 0000000000..cff234b7c5 --- /dev/null +++ b/1-js/13-modules/03-modules-dynamic-imports/say.view/say.js @@ -0,0 +1,11 @@ +export function hi() { + alert(`Hello`); +} + +export function bye() { + alert(`Bye`); +} + +export default function() { + alert("Module loaded (export default)!"); +} diff --git a/1-js/13-modules/index.md b/1-js/13-modules/index.md new file mode 100644 index 0000000000..78fb060e89 --- /dev/null +++ b/1-js/13-modules/index.md @@ -0,0 +1,2 @@ + +# Modules diff --git a/1-js/99-js-misc/01-proxy/01-error-nonexisting/solution.md b/1-js/99-js-misc/01-proxy/01-error-nonexisting/solution.md new file mode 100644 index 0000000000..9db69cb2fa --- /dev/null +++ b/1-js/99-js-misc/01-proxy/01-error-nonexisting/solution.md @@ -0,0 +1,23 @@ + +```js run +let user = { + name: "John" +}; + +function wrap(target) { + return new Proxy(target, { + get(target, prop, receiver) { + if (prop in target) { + return Reflect.get(target, prop, receiver); + } else { + throw new ReferenceError(`Property doesn't exist: "${prop}"`) + } + } + }); +} + +user = wrap(user); + +alert(user.name); // John +alert(user.age); // ReferenceError: Property doesn't exist: "age" +``` diff --git a/1-js/99-js-misc/01-proxy/01-error-nonexisting/task.md b/1-js/99-js-misc/01-proxy/01-error-nonexisting/task.md new file mode 100644 index 0000000000..47985e1a76 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/01-error-nonexisting/task.md @@ -0,0 +1,32 @@ +# Error on reading non-existent property + +Usually, an attempt to read a non-existent property returns `undefined`. + +Create a proxy that throws an error for an attempt to read of a non-existent property instead. + +That can help to detect programming mistakes early. + +Write a function `wrap(target)` that takes an object `target` and return a proxy that adds this functionality aspect. + +That's how it should work: + +```js +let user = { + name: "John" +}; + +function wrap(target) { + return new Proxy(target, { +*!* + /* your code */ +*/!* + }); +} + +user = wrap(user); + +alert(user.name); // John +*!* +alert(user.age); // ReferenceError: Property doesn't exist: "age" +*/!* +``` diff --git a/1-js/99-js-misc/01-proxy/02-array-negative/solution.md b/1-js/99-js-misc/01-proxy/02-array-negative/solution.md new file mode 100644 index 0000000000..207205501f --- /dev/null +++ b/1-js/99-js-misc/01-proxy/02-array-negative/solution.md @@ -0,0 +1,19 @@ + +```js run +let array = [1, 2, 3]; + +array = new Proxy(array, { + get(target, prop, receiver) { + if (prop < 0) { + // even if we access it like arr[1] + // prop is a string, so need to convert it to number + prop = +prop + target.length; + } + return Reflect.get(target, prop, receiver); + } +}); + + +alert(array[-1]); // 3 +alert(array[-2]); // 2 +``` diff --git a/1-js/99-js-misc/01-proxy/02-array-negative/task.md b/1-js/99-js-misc/01-proxy/02-array-negative/task.md new file mode 100644 index 0000000000..9b0b13f580 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/02-array-negative/task.md @@ -0,0 +1,33 @@ + +# Accessing array[-1] + +In some programming languages, we can access array elements using negative indexes, counted from the end. + +Like this: + +```js +let array = [1, 2, 3]; + +array[-1]; // 3, the last element +array[-2]; // 2, one step from the end +array[-3]; // 1, two steps from the end +``` + +In other words, `array[-N]` is the same as `array[array.length - N]`. + +Create a proxy to implement that behavior. + +That's how it should work: + +```js +let array = [1, 2, 3]; + +array = new Proxy(array, { + /* your code */ +}); + +alert( array[-1] ); // 3 +alert( array[-2] ); // 2 + +// Other array functionality should be kept "as is" +``` diff --git a/1-js/99-js-misc/01-proxy/03-observable/solution.md b/1-js/99-js-misc/01-proxy/03-observable/solution.md new file mode 100644 index 0000000000..c0797a856b --- /dev/null +++ b/1-js/99-js-misc/01-proxy/03-observable/solution.md @@ -0,0 +1,40 @@ +The solution consists of two parts: + +1. Whenever `.observe(handler)` is called, we need to remember the handler somewhere, to be able to call it later. We can store handlers right in the object, using our symbol as the property key. +2. We need a proxy with `set` trap to call handlers in case of any change. + +```js run +let handlers = Symbol('handlers'); + +function makeObservable(target) { + // 1. Initialize handlers store + target[handlers] = []; + + // Store the handler function in array for future calls + target.observe = function(handler) { + this[handlers].push(handler); + }; + + // 2. Create a proxy to handle changes + return new Proxy(target, { + set(target, property, value, receiver) { + let success = Reflect.set(...arguments); // forward the operation to object + if (success) { // if there were no error while setting the property + // call all handlers + target[handlers].forEach(handler => handler(property, value)); + } + return success; + } + }); +} + +let user = {}; + +user = makeObservable(user); + +user.observe((key, value) => { + alert(`SET ${key}=${value}`); +}); + +user.name = "John"; +``` diff --git a/1-js/99-js-misc/01-proxy/03-observable/task.md b/1-js/99-js-misc/01-proxy/03-observable/task.md new file mode 100644 index 0000000000..754d9f3bd9 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/03-observable/task.md @@ -0,0 +1,27 @@ + +# Observable + +Create a function `makeObservable(target)` that "makes the object observable" by returning a proxy. + +Here's how it should work: + +```js run +function makeObservable(target) { + /* your code */ +} + +let user = {}; +user = makeObservable(user); + +user.observe((key, value) => { + alert(`SET ${key}=${value}`); +}); + +user.name = "John"; // alerts: SET name=John +``` + +In other words, an object returned by `makeObservable` is just like the original one, but also has the method `observe(handler)` that sets `handler` function to be called on any property change. + +Whenever a property changes, `handler(key, value)` is called with the name and value of the property. + +P.S. In this task, please only take care about writing to a property. Other operations can be implemented in a similar way. diff --git a/1-js/99-js-misc/01-proxy/article.md b/1-js/99-js-misc/01-proxy/article.md new file mode 100644 index 0000000000..1f84912e58 --- /dev/null +++ b/1-js/99-js-misc/01-proxy/article.md @@ -0,0 +1,1034 @@ +# Proxy and Reflect + +A `Proxy` object wraps another object and intercepts operations, like reading/writing properties and others, optionally handling them on its own, or transparently allowing the object to handle them. + +Proxies are used in many libraries and some browser frameworks. We'll see many practical applications in this article. + +## Proxy + +The syntax: + +```js +let proxy = new Proxy(target, handler) +``` + +- `target` -- is an object to wrap, can be anything, including functions. +- `handler` -- proxy configuration: an object with "traps", methods that intercept operations. - e.g. `get` trap for reading a property of `target`, `set` trap for writing a property into `target`, and so on. + +For operations on `proxy`, if there's a corresponding trap in `handler`, then it runs, and the proxy has a chance to handle it, otherwise the operation is performed on `target`. + +As a starting example, let's create a proxy without any traps: + +```js run +let target = {}; +let proxy = new Proxy(target, {}); // empty handler + +proxy.test = 5; // writing to proxy (1) +alert(target.test); // 5, the property appeared in target! + +alert(proxy.test); // 5, we can read it from proxy too (2) + +for(let key in proxy) alert(key); // test, iteration works (3) +``` + +As there are no traps, all operations on `proxy` are forwarded to `target`. + +1. A writing operation `proxy.test=` sets the value on `target`. +2. A reading operation `proxy.test` returns the value from `target`. +3. Iteration over `proxy` returns values from `target`. + +As we can see, without any traps, `proxy` is a transparent wrapper around `target`. + +![](proxy.svg) + +`Proxy` is a special "exotic object". It doesn't have own properties. With an empty `handler` it transparently forwards operations to `target`. + +To activate more capabilities, let's add traps. + +What can we intercept with them? + +For most operations on objects, there's a so-called "internal method" in the JavaScript specification that describes how it works at the lowest level. For instance `[[Get]]`, the internal method to read a property, `[[Set]]`, the internal method to write a property, and so on. These methods are only used in the specification, we can't call them directly by name. + +Proxy traps intercept invocations of these methods. They are listed in the [Proxy specification](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots) and in the table below. + +For every internal method, there's a trap in this table: the name of the method that we can add to the `handler` parameter of `new Proxy` to intercept the operation: + +| Internal Method | Handler Method | Triggers when... | +|-----------------|----------------|-------------| +| `[[Get]]` | `get` | reading a property | +| `[[Set]]` | `set` | writing to a property | +| `[[HasProperty]]` | `has` | `in` operator | +| `[[Delete]]` | `deleteProperty` | `delete` operator | +| `[[Call]]` | `apply` | function call | +| `[[Construct]]` | `construct` | `new` operator | +| `[[GetPrototypeOf]]` | `getPrototypeOf` | [Object.getPrototypeOf](mdn:/JavaScript/Reference/Global_Objects/Object/getPrototypeOf) | +| `[[SetPrototypeOf]]` | `setPrototypeOf` | [Object.setPrototypeOf](mdn:/JavaScript/Reference/Global_Objects/Object/setPrototypeOf) | +| `[[IsExtensible]]` | `isExtensible` | [Object.isExtensible](mdn:/JavaScript/Reference/Global_Objects/Object/isExtensible) | +| `[[PreventExtensions]]` | `preventExtensions` | [Object.preventExtensions](mdn:/JavaScript/Reference/Global_Objects/Object/preventExtensions) | +| `[[DefineOwnProperty]]` | `defineProperty` | [Object.defineProperty](mdn:/JavaScript/Reference/Global_Objects/Object/defineProperty), [Object.defineProperties](mdn:/JavaScript/Reference/Global_Objects/Object/defineProperties) | +| `[[GetOwnProperty]]` | `getOwnPropertyDescriptor` | [Object.getOwnPropertyDescriptor](mdn:/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor), `for..in`, `Object.keys/values/entries` | +| `[[OwnPropertyKeys]]` | `ownKeys` | [Object.getOwnPropertyNames](mdn:/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames), [Object.getOwnPropertySymbols](mdn:/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols), `for..in`, `Object.keys/values/entries` | + +```warn header="Invariants" +JavaScript enforces some invariants -- conditions that must be fulfilled by internal methods and traps. + +Most of them are for return values: +- `[[Set]]` must return `true` if the value was written successfully, otherwise `false`. +- `[[Delete]]` must return `true` if the value was deleted successfully, otherwise `false`. +- ...and so on, we'll see more in examples below. + +There are some other invariants, like: +- `[[GetPrototypeOf]]`, applied to the proxy object must return the same value as `[[GetPrototypeOf]]` applied to the proxy object's target object. In other words, reading prototype of a proxy must always return the prototype of the target object. + +Traps can intercept these operations, but they must follow these rules. + +Invariants ensure correct and consistent behavior of language features. The full invariants list is in [the specification](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots). You probably won't violate them if you're not doing something weird. +``` + +Let's see how that works in practical examples. + +## Default value with "get" trap + +The most common traps are for reading/writing properties. + +To intercept reading, the `handler` should have a method `get(target, property, receiver)`. + +It triggers when a property is read, with following arguments: + +- `target` -- is the target object, the one passed as the first argument to `new Proxy`, +- `property` -- property name, +- `receiver` -- if the target property is a getter, then `receiver` is the object that's going to be used as `this` in its call. Usually that's the `proxy` object itself (or an object that inherits from it, if we inherit from proxy). Right now we don't need this argument, so it will be explained in more detail later. + +Let's use `get` to implement default values for an object. + +We'll make a numeric array that returns `0` for nonexistent values. + +Usually when one tries to get a non-existing array item, they get `undefined`, but we'll wrap a regular array into the proxy that traps reading and returns `0` if there's no such property: + +```js run +let numbers = [0, 1, 2]; + +numbers = new Proxy(numbers, { + get(target, prop) { + if (prop in target) { + return target[prop]; + } else { + return 0; // default value + } + } +}); + +*!* +alert( numbers[1] ); // 1 +alert( numbers[123] ); // 0 (no such item) +*/!* +``` + +As we can see, it's quite easy to do with a `get` trap. + +We can use `Proxy` to implement any logic for "default" values. + +Imagine we have a dictionary, with phrases and their translations: + +```js run +let dictionary = { + 'Hello': 'Hola', + 'Bye': 'Adiós' +}; + +alert( dictionary['Hello'] ); // Hola +alert( dictionary['Welcome'] ); // undefined +``` + +Right now, if there's no phrase, reading from `dictionary` returns `undefined`. But in practice, leaving a phrase untranslated is usually better than `undefined`. So let's make it return an untranslated phrase in that case instead of `undefined`. + +To achieve that, we'll wrap `dictionary` in a proxy that intercepts reading operations: + +```js run +let dictionary = { + 'Hello': 'Hola', + 'Bye': 'Adiós' +}; + +dictionary = new Proxy(dictionary, { +*!* + get(target, phrase) { // intercept reading a property from dictionary +*/!* + if (phrase in target) { // if we have it in the dictionary + return target[phrase]; // return the translation + } else { + // otherwise, return the non-translated phrase + return phrase; + } + } +}); + +// Look up arbitrary phrases in the dictionary! +// At worst, they're not translated. +alert( dictionary['Hello'] ); // Hola +*!* +alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (no translation) +*/!* +``` + +````smart +Please note how the proxy overwrites the variable: + +```js +dictionary = new Proxy(dictionary, ...); +``` + +The proxy should totally replace the target object everywhere. No one should ever reference the target object after it got proxied. Otherwise it's easy to mess up. +```` + +## Validation with "set" trap + +Let's say we want an array exclusively for numbers. If a value of another type is added, there should be an error. + +The `set` trap triggers when a property is written. + +`set(target, property, value, receiver)`: + +- `target` -- is the target object, the one passed as the first argument to `new Proxy`, +- `property` -- property name, +- `value` -- property value, +- `receiver` -- similar to `get` trap, matters only for setter properties. + +The `set` trap should return `true` if setting is successful, and `false` otherwise (triggers `TypeError`). + +Let's use it to validate new values: + +```js run +let numbers = []; + +numbers = new Proxy(numbers, { // (*) +*!* + set(target, prop, val) { // to intercept property writing +*/!* + if (typeof val == 'number') { + target[prop] = val; + return true; + } else { + return false; + } + } +}); + +numbers.push(1); // added successfully +numbers.push(2); // added successfully +alert("Length is: " + numbers.length); // 2 + +*!* +numbers.push("test"); // TypeError ('set' on proxy returned false) +*/!* + +alert("This line is never reached (error in the line above)"); +``` + +Please note: the built-in functionality of arrays is still working! Values are added by `push`. The `length` property auto-increases when values are added. Our proxy doesn't break anything. + +We don't have to override value-adding array methods like `push` and `unshift`, and so on, to add checks in there, because internally they use the `[[Set]]` operation that's intercepted by the proxy. + +So the code is clean and concise. + +```warn header="Don't forget to return `true`" +As said above, there are invariants to be held. + +For `set`, it must return `true` for a successful write. + +If we forget to do it or return any falsy value, the operation triggers `TypeError`. +``` + +## Iteration with "ownKeys" and "getOwnPropertyDescriptor" + +`Object.keys`, `for..in` loop and most other methods that iterate over object properties use `[[OwnPropertyKeys]]` internal method (intercepted by `ownKeys` trap) to get a list of properties. + +Such methods differ in details: +- `Object.getOwnPropertyNames(obj)` returns non-symbol keys. +- `Object.getOwnPropertySymbols(obj)` returns symbol keys. +- `Object.keys/values()` returns non-symbol keys/values with `enumerable` flag (property flags were explained in the article ). +- `for..in` loops over non-symbol keys with `enumerable` flag, and also prototype keys. + +...But all of them start with that list. + +In the example below we use `ownKeys` trap to make `for..in` loop over `user`, and also `Object.keys` and `Object.values`, to skip properties starting with an underscore `_`: + +```js run +let user = { + name: "John", + age: 30, + _password: "***" +}; + +user = new Proxy(user, { +*!* + ownKeys(target) { +*/!* + return Object.keys(target).filter(key => !key.startsWith('_')); + } +}); + +// "ownKeys" filters out _password +for(let key in user) alert(key); // name, then: age + +// same effect on these methods: +alert( Object.keys(user) ); // name,age +alert( Object.values(user) ); // John,30 +``` + +So far, it works. + +Although, if we return a key that doesn't exist in the object, `Object.keys` won't list it: + +```js run +let user = { }; + +user = new Proxy(user, { +*!* + ownKeys(target) { +*/!* + return ['a', 'b', 'c']; + } +}); + +alert( Object.keys(user) ); // +``` + +Why? The reason is simple: `Object.keys` returns only properties with the `enumerable` flag. To check for it, it calls the internal method `[[GetOwnProperty]]` for every property to get [its descriptor](info:property-descriptors). And here, as there's no property, its descriptor is empty, no `enumerable` flag, so it's skipped. + +For `Object.keys` to return a property, we need it to either exist in the object, with the `enumerable` flag, or we can intercept calls to `[[GetOwnProperty]]` (the trap `getOwnPropertyDescriptor` does it), and return a descriptor with `enumerable: true`. + +Here's an example of that: + +```js run +let user = { }; + +user = new Proxy(user, { + ownKeys(target) { // called once to get a list of properties + return ['a', 'b', 'c']; + }, + + getOwnPropertyDescriptor(target, prop) { // called for every property + return { + enumerable: true, + configurable: true + /* ...other flags, probable "value:..." */ + }; + } + +}); + +alert( Object.keys(user) ); // a, b, c +``` + +Let's note once again: we only need to intercept `[[GetOwnProperty]]` if the property is absent in the object. + +## Protected properties with "deleteProperty" and other traps + +There's a widespread convention that properties and methods prefixed by an underscore `_` are internal. They shouldn't be accessed from outside the object. + +Technically that's possible though: + +```js run +let user = { + name: "John", + _password: "secret" +}; + +alert(user._password); // secret +``` + +Let's use proxies to prevent any access to properties starting with `_`. + +We'll need the traps: +- `get` to throw an error when reading such property, +- `set` to throw an error when writing, +- `deleteProperty` to throw an error when deleting, +- `ownKeys` to exclude properties starting with `_` from `for..in` and methods like `Object.keys`. + +Here's the code: + +```js run +let user = { + name: "John", + _password: "***" +}; + +user = new Proxy(user, { +*!* + get(target, prop) { +*/!* + if (prop.startsWith('_')) { + throw new Error("Access denied"); + } + let value = target[prop]; + return (typeof value === 'function') ? value.bind(target) : value; // (*) + }, +*!* + set(target, prop, val) { // to intercept property writing +*/!* + if (prop.startsWith('_')) { + throw new Error("Access denied"); + } else { + target[prop] = val; + return true; + } + }, +*!* + deleteProperty(target, prop) { // to intercept property deletion +*/!* + if (prop.startsWith('_')) { + throw new Error("Access denied"); + } else { + delete target[prop]; + return true; + } + }, +*!* + ownKeys(target) { // to intercept property list +*/!* + return Object.keys(target).filter(key => !key.startsWith('_')); + } +}); + +// "get" doesn't allow to read _password +try { + alert(user._password); // Error: Access denied +} catch(e) { alert(e.message); } + +// "set" doesn't allow to write _password +try { + user._password = "test"; // Error: Access denied +} catch(e) { alert(e.message); } + +// "deleteProperty" doesn't allow to delete _password +try { + delete user._password; // Error: Access denied +} catch(e) { alert(e.message); } + +// "ownKeys" filters out _password +for(let key in user) alert(key); // name +``` + +Please note the important detail in the `get` trap, in the line `(*)`: + +```js +get(target, prop) { + // ... + let value = target[prop]; +*!* + return (typeof value === 'function') ? value.bind(target) : value; // (*) +*/!* +} +``` + +Why do we need a function to call `value.bind(target)`? + +The reason is that object methods, such as `user.checkPassword()`, must be able to access `_password`: + +```js +user = { + // ... + checkPassword(value) { + // object method must be able to read _password + return value === this._password; + } +} +``` + + +A call to `user.checkPassword()` gets proxied `user` as `this` (the object before dot becomes `this`), so when it tries to access `this._password`, the `get` trap activates (it triggers on any property read) and throws an error. + +So we bind the context of object methods to the original object, `target`, in the line `(*)`. Then their future calls will use `target` as `this`, without any traps. + +That solution usually works, but isn't ideal, as a method may pass the unproxied object somewhere else, and then we'll get messed up: where's the original object, and where's the proxied one? + +Besides, an object may be proxied multiple times (multiple proxies may add different "tweaks" to the object), and if we pass an unwrapped object to a method, there may be unexpected consequences. + +So, such a proxy shouldn't be used everywhere. + +```smart header="Private properties of a class" +Modern JavaScript engines natively support private properties in classes, prefixed with `#`. They are described in the article . No proxies required. + +Such properties have their own issues though. In particular, they are not inherited. +``` + +## "In range" with "has" trap + +Let's see more examples. + +We have a range object: + +```js +let range = { + start: 1, + end: 10 +}; +``` + +We'd like to use the `in` operator to check that a number is in `range`. + +The `has` trap intercepts `in` calls. + +`has(target, property)` + +- `target` -- is the target object, passed as the first argument to `new Proxy`, +- `property` -- property name + +Here's the demo: + +```js run +let range = { + start: 1, + end: 10 +}; + +range = new Proxy(range, { +*!* + has(target, prop) { +*/!* + return prop >= target.start && prop <= target.end; + } +}); + +*!* +alert(5 in range); // true +alert(50 in range); // false +*/!* +``` + +Nice syntactic sugar, isn't it? And very simple to implement. + +## Wrapping functions: "apply" [#proxy-apply] + +We can wrap a proxy around a function as well. + +The `apply(target, thisArg, args)` trap handles calling a proxy as function: + +- `target` is the target object (function is an object in JavaScript), +- `thisArg` is the value of `this`. +- `args` is a list of arguments. + +For example, let's recall `delay(f, ms)` decorator, that we did in the article . + +In that article we did it without proxies. A call to `delay(f, ms)` returned a function that forwards all calls to `f` after `ms` milliseconds. + +Here's the previous, function-based implementation: + +```js run +function delay(f, ms) { + // return a wrapper that passes the call to f after the timeout + return function() { // (*) + setTimeout(() => f.apply(this, arguments), ms); + }; +} + +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +// after this wrapping, calls to sayHi will be delayed for 3 seconds +sayHi = delay(sayHi, 3000); + +sayHi("John"); // Hello, John! (after 3 seconds) +``` + +As we've seen already, that mostly works. The wrapper function `(*)` performs the call after the timeout. + +But a wrapper function does not forward property read/write operations or anything else. After the wrapping, the access is lost to properties of the original functions, such as `name`, `length` and others: + +```js run +function delay(f, ms) { + return function() { + setTimeout(() => f.apply(this, arguments), ms); + }; +} + +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +*!* +alert(sayHi.length); // 1 (function length is the arguments count in its declaration) +*/!* + +sayHi = delay(sayHi, 3000); + +*!* +alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments) +*/!* +``` + +`Proxy` is much more powerful, as it forwards everything to the target object. + +Let's use `Proxy` instead of a wrapping function: + +```js run +function delay(f, ms) { + return new Proxy(f, { + apply(target, thisArg, args) { + setTimeout(() => target.apply(thisArg, args), ms); + } + }); +} + +function sayHi(user) { + alert(`Hello, ${user}!`); +} + +sayHi = delay(sayHi, 3000); + +*!* +alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target +*/!* + +sayHi("John"); // Hello, John! (after 3 seconds) +``` + +The result is the same, but now not only calls, but all operations on the proxy are forwarded to the original function. So `sayHi.length` is returned correctly after the wrapping in the line `(*)`. + +We've got a "richer" wrapper. + +Other traps exist: the full list is in the beginning of this article. Their usage pattern is similar to the above. + +## Reflect + +`Reflect` is a built-in object that simplifies creation of `Proxy`. + +It was said previously that internal methods, such as `[[Get]]`, `[[Set]]` and others are specification-only, they can't be called directly. + +The `Reflect` object makes that somewhat possible. Its methods are minimal wrappers around the internal methods. + +Here are examples of operations and `Reflect` calls that do the same: + +| Operation | `Reflect` call | Internal method | +|-----------------|----------------|-------------| +| `obj[prop]` | `Reflect.get(obj, prop)` | `[[Get]]` | +| `obj[prop] = value` | `Reflect.set(obj, prop, value)` | `[[Set]]` | +| `delete obj[prop]` | `Reflect.deleteProperty(obj, prop)` | `[[Delete]]` | +| `new F(value)` | `Reflect.construct(F, value)` | `[[Construct]]` | +| ... | ... | ... | + +For example: + +```js run +let user = {}; + +Reflect.set(user, 'name', 'John'); + +alert(user.name); // John +``` + +In particular, `Reflect` allows us to call operators (`new`, `delete`...) as functions (`Reflect.construct`, `Reflect.deleteProperty`, ...). That's an interesting capability, but here another thing is important. + +**For every internal method, trappable by `Proxy`, there's a corresponding method in `Reflect`, with the same name and arguments as the `Proxy` trap.** + +So we can use `Reflect` to forward an operation to the original object. + +In this example, both traps `get` and `set` transparently (as if they didn't exist) forward reading/writing operations to the object, showing a message: + +```js run +let user = { + name: "John", +}; + +user = new Proxy(user, { + get(target, prop, receiver) { + alert(`GET ${prop}`); +*!* + return Reflect.get(target, prop, receiver); // (1) +*/!* + }, + set(target, prop, val, receiver) { + alert(`SET ${prop}=${val}`); +*!* + return Reflect.set(target, prop, val, receiver); // (2) +*/!* + } +}); + +let name = user.name; // shows "GET name" +user.name = "Pete"; // shows "SET name=Pete" +``` + +Here: + +- `Reflect.get` reads an object property. +- `Reflect.set` writes an object property and returns `true` if successful, `false` otherwise. + +That is, everything's simple: if a trap wants to forward the call to the object, it's enough to call `Reflect.` with the same arguments. + +In most cases we can do the same without `Reflect`, for instance, reading a property `Reflect.get(target, prop, receiver)` can be replaced by `target[prop]`. There are important nuances though. + +### Proxying a getter + +Let's see an example that demonstrates why `Reflect.get` is better. And we'll also see why `get/set` have the third argument `receiver`, that we didn't use before. + +We have an object `user` with `_name` property and a getter for it. + +Here's a proxy around it: + +```js run +let user = { + _name: "Guest", + get name() { + return this._name; + } +}; + +*!* +let userProxy = new Proxy(user, { + get(target, prop, receiver) { + return target[prop]; + } +}); +*/!* + +alert(userProxy.name); // Guest +``` + +The `get` trap is "transparent" here, it returns the original property, and doesn't do anything else. That's enough for our example. + +Everything seems to be all right. But let's make the example a little bit more complex. + +After inheriting another object `admin` from `user`, we can observe the incorrect behavior: + +```js run +let user = { + _name: "Guest", + get name() { + return this._name; + } +}; + +let userProxy = new Proxy(user, { + get(target, prop, receiver) { + return target[prop]; // (*) target = user + } +}); + +*!* +let admin = { + __proto__: userProxy, + _name: "Admin" +}; + +// Expected: Admin +alert(admin.name); // outputs: Guest (?!?) +*/!* +``` + +Reading `admin.name` should return `"Admin"`, not `"Guest"`! + +What's the matter? Maybe we did something wrong with the inheritance? + +But if we remove the proxy, then everything will work as expected. + +The problem is actually in the proxy, in the line `(*)`. + +1. When we read `admin.name`, as `admin` object doesn't have such own property, the search goes to its prototype. +2. The prototype is `userProxy`. +3. When reading `name` property from the proxy, its `get` trap triggers and returns it from the original object as `target[prop]` in the line `(*)`. + + A call to `target[prop]`, when `prop` is a getter, runs its code in the context `this=target`. So the result is `this._name` from the original object `target`, that is: from `user`. + +To fix such situations, we need `receiver`, the third argument of `get` trap. It keeps the correct `this` to be passed to a getter. In our case that's `admin`. + +How to pass the context for a getter? For a regular function we could use `call/apply`, but that's a getter, it's not "called", just accessed. + +`Reflect.get` can do that. Everything will work right if we use it. + +Here's the corrected variant: + +```js run +let user = { + _name: "Guest", + get name() { + return this._name; + } +}; + +let userProxy = new Proxy(user, { + get(target, prop, receiver) { // receiver = admin +*!* + return Reflect.get(target, prop, receiver); // (*) +*/!* + } +}); + + +let admin = { + __proto__: userProxy, + _name: "Admin" +}; + +*!* +alert(admin.name); // Admin +*/!* +``` + +Now `receiver` that keeps a reference to the correct `this` (that is `admin`), is passed to the getter using `Reflect.get` in the line `(*)`. + +We can rewrite the trap even shorter: + +```js +get(target, prop, receiver) { + return Reflect.get(*!*...arguments*/!*); +} +``` + + +`Reflect` calls are named exactly the same way as traps and accept the same arguments. They were specifically designed this way. + +So, `return Reflect...` provides a safe no-brainer to forward the operation and make sure we don't forget anything related to that. + +## Proxy limitations + +Proxies provide a unique way to alter or tweak the behavior of the existing objects at the lowest level. Still, it's not perfect. There are limitations. + +### Built-in objects: Internal slots + +Many built-in objects, for example `Map`, `Set`, `Date`, `Promise` and others make use of so-called "internal slots". + +These are like properties, but reserved for internal, specification-only purposes. For instance, `Map` stores items in the internal slot `[[MapData]]`. Built-in methods access them directly, not via `[[Get]]/[[Set]]` internal methods. So `Proxy` can't intercept that. + +Why care? They're internal anyway! + +Well, here's the issue. After a built-in object like that gets proxied, the proxy doesn't have these internal slots, so built-in methods will fail. + +For example: + +```js run +let map = new Map(); + +let proxy = new Proxy(map, {}); + +*!* +proxy.set('test', 1); // Error +*/!* +``` + +Internally, a `Map` stores all data in its `[[MapData]]` internal slot. The proxy doesn't have such a slot. The [built-in method `Map.prototype.set`](https://tc39.es/ecma262/#sec-map.prototype.set) method tries to access the internal property `this.[[MapData]]`, but because `this=proxy`, can't find it in `proxy` and just fails. + +Fortunately, there's a way to fix it: + +```js run +let map = new Map(); + +let proxy = new Proxy(map, { + get(target, prop, receiver) { + let value = Reflect.get(...arguments); +*!* + return typeof value == 'function' ? value.bind(target) : value; +*/!* + } +}); + +proxy.set('test', 1); +alert(proxy.get('test')); // 1 (works!) +``` + +Now it works fine, because `get` trap binds function properties, such as `map.set`, to the target object (`map`) itself. + +Unlike the previous example, the value of `this` inside `proxy.set(...)` will be not `proxy`, but the original `map`. So when the internal implementation of `set` tries to access `this.[[MapData]]` internal slot, it succeeds. + +```smart header="`Array` has no internal slots" +A notable exception: built-in `Array` doesn't use internal slots. That's for historical reasons, as it appeared so long ago. + +So there's no such problem when proxying an array. +``` + +### Private fields + +A similar thing happens with private class fields. + +For example, `getName()` method accesses the private `#name` property and breaks after proxying: + +```js run +class User { + #name = "Guest"; + + getName() { + return this.#name; + } +} + +let user = new User(); + +user = new Proxy(user, {}); + +*!* +alert(user.getName()); // Error +*/!* +``` + +The reason is that private fields are implemented using internal slots. JavaScript does not use `[[Get]]/[[Set]]` when accessing them. + +In the call `getName()` the value of `this` is the proxied `user`, and it doesn't have the slot with private fields. + +Once again, the solution with binding the method makes it work: + +```js run +class User { + #name = "Guest"; + + getName() { + return this.#name; + } +} + +let user = new User(); + +user = new Proxy(user, { + get(target, prop, receiver) { + let value = Reflect.get(...arguments); + return typeof value == 'function' ? value.bind(target) : value; + } +}); + +alert(user.getName()); // Guest +``` + +That said, the solution has drawbacks, as explained previously: it exposes the original object to the method, potentially allowing it to be passed further and breaking other proxied functionality. + +### Proxy != target + +The proxy and the original object are different objects. That's natural, right? + +So if we use the original object as a key, and then proxy it, then the proxy can't be found: + +```js run +let allUsers = new Set(); + +class User { + constructor(name) { + this.name = name; + allUsers.add(this); + } +} + +let user = new User("John"); + +alert(allUsers.has(user)); // true + +user = new Proxy(user, {}); + +*!* +alert(allUsers.has(user)); // false +*/!* +``` + +As we can see, after proxying we can't find `user` in the set `allUsers`, because the proxy is a different object. + +```warn header="Proxies can't intercept a strict equality test `===`" +Proxies can intercept many operators, such as `new` (with `construct`), `in` (with `has`), `delete` (with `deleteProperty`) and so on. + +But there's no way to intercept a strict equality test for objects. An object is strictly equal to itself only, and no other value. + +So all operations and built-in classes that compare objects for equality will differentiate between the object and the proxy. No transparent replacement here. +``` + +## Revocable proxies + +A *revocable* proxy is a proxy that can be disabled. + +Let's say we have a resource, and would like to close access to it any moment. + +What we can do is to wrap it into a revocable proxy, without any traps. Such a proxy will forward operations to object, and we can disable it at any moment. + +The syntax is: + +```js +let {proxy, revoke} = Proxy.revocable(target, handler) +``` + +The call returns an object with the `proxy` and `revoke` function to disable it. + +Here's an example: + +```js run +let object = { + data: "Valuable data" +}; + +let {proxy, revoke} = Proxy.revocable(object, {}); + +// pass the proxy somewhere instead of object... +alert(proxy.data); // Valuable data + +// later in our code +revoke(); + +// the proxy isn't working any more (revoked) +alert(proxy.data); // Error +``` + +A call to `revoke()` removes all internal references to the target object from the proxy, so they are no longer connected. + +Initially, `revoke` is separate from `proxy`, so that we can pass `proxy` around while leaving `revoke` in the current scope. + +We can also bind `revoke` method to proxy by setting `proxy.revoke = revoke`. + +Another option is to create a `WeakMap` that has `proxy` as the key and the corresponding `revoke` as the value, that allows to easily find `revoke` for a proxy: + +```js run +*!* +let revokes = new WeakMap(); +*/!* + +let object = { + data: "Valuable data" +}; + +let {proxy, revoke} = Proxy.revocable(object, {}); + +revokes.set(proxy, revoke); + +// ..somewhere else in our code.. +revoke = revokes.get(proxy); +revoke(); + +alert(proxy.data); // Error (revoked) +``` + +We use `WeakMap` instead of `Map` here because it won't block garbage collection. If a proxy object becomes "unreachable" (e.g. no variable references it any more), `WeakMap` allows it to be wiped from memory together with its `revoke` that we won't need any more. + +## References + +- Specification: [Proxy](https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots). +- MDN: [Proxy](mdn:/JavaScript/Reference/Global_Objects/Proxy). + +## Summary + +`Proxy` is a wrapper around an object, that forwards operations on it to the object, optionally trapping some of them. + +It can wrap any kind of object, including classes and functions. + +The syntax is: + +```js +let proxy = new Proxy(target, { + /* traps */ +}); +``` + +...Then we should use `proxy` everywhere instead of `target`. A proxy doesn't have its own properties or methods. It traps an operation if the trap is provided, otherwise forwards it to `target` object. + +We can trap: +- Reading (`get`), writing (`set`), deleting (`deleteProperty`) a property (even a non-existing one). +- Calling a function (`apply` trap). +- The `new` operator (`construct` trap). +- Many other operations (the full list is at the beginning of the article and in the [docs](mdn:/JavaScript/Reference/Global_Objects/Proxy)). + +That allows us to create "virtual" properties and methods, implement default values, observable objects, function decorators and so much more. + +We can also wrap an object multiple times in different proxies, decorating it with various aspects of functionality. + +The [Reflect](mdn:/JavaScript/Reference/Global_Objects/Reflect) API is designed to complement [Proxy](mdn:/JavaScript/Reference/Global_Objects/Proxy). For any `Proxy` trap, there's a `Reflect` call with same arguments. We should use those to forward calls to target objects. + +Proxies have some limitations: + +- Built-in objects have "internal slots", access to those can't be proxied. See the workaround above. +- The same holds true for private class fields, as they are internally implemented using slots. So proxied method calls must have the target object as `this` to access them. +- Object equality tests `===` can't be intercepted. +- Performance: benchmarks depend on an engine, but generally accessing a property using a simplest proxy takes a few times longer. In practice that only matters for some "bottleneck" objects though. 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 new file mode 100644 index 0000000000..3fba64606b --- /dev/null +++ b/1-js/99-js-misc/01-proxy/proxy-inherit-admin.svg @@ -0,0 +1 @@ +_name: "Guest" name: getter_name: "Admin"user (proxied)original useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/99-js-misc/01-proxy/proxy-inherit.svg b/1-js/99-js-misc/01-proxy/proxy-inherit.svg new file mode 100644 index 0000000000..6c34c0f4ed --- /dev/null +++ b/1-js/99-js-misc/01-proxy/proxy-inherit.svg @@ -0,0 +1 @@ +_name: "Guest" name: getteruser (proxied)original user \ No newline at end of file diff --git a/1-js/99-js-misc/01-proxy/proxy.svg b/1-js/99-js-misc/01-proxy/proxy.svg new file mode 100644 index 0000000000..6b2224cfdc --- /dev/null +++ b/1-js/99-js-misc/01-proxy/proxy.svg @@ -0,0 +1 @@ +test: 5proxytargetget proxy.test5 \ No newline at end of file diff --git a/1-js/99-js-misc/02-eval/1-eval-calculator/solution.md b/1-js/99-js-misc/02-eval/1-eval-calculator/solution.md new file mode 100644 index 0000000000..24d40c9b89 --- /dev/null +++ b/1-js/99-js-misc/02-eval/1-eval-calculator/solution.md @@ -0,0 +1,11 @@ +Let's use `eval` to calculate the maths expression: + +```js demo run +let expr = prompt("Type an arithmetic expression?", '2*3+2'); + +alert( eval(expr) ); +``` + +The user can input any text or code though. + +To make things safe, and limit it to arithmetics only, we can check the `expr` using a [regular expression](info:regular-expressions), so that it only may contain digits and operators. diff --git a/1-js/99-js-misc/02-eval/1-eval-calculator/task.md b/1-js/99-js-misc/02-eval/1-eval-calculator/task.md new file mode 100644 index 0000000000..ece43ec9e6 --- /dev/null +++ b/1-js/99-js-misc/02-eval/1-eval-calculator/task.md @@ -0,0 +1,11 @@ +importance: 4 + +--- + +# Eval-calculator + +Create a calculator that prompts for an arithmetic expression and returns its result. + +There's no need to check the expression for correctness in this task. Just evaluate and return the result. + +[demo] diff --git a/1-js/99-js-misc/02-eval/article.md b/1-js/99-js-misc/02-eval/article.md new file mode 100644 index 0000000000..1fdafeeec6 --- /dev/null +++ b/1-js/99-js-misc/02-eval/article.md @@ -0,0 +1,114 @@ +# Eval: run a code string + +The built-in `eval` function allows to execute a string of code. + +The syntax is: + +```js +let result = eval(code); +``` + +For example: + +```js run +let code = 'alert("Hello")'; +eval(code); // Hello +``` + +A string of code may be long, contain line breaks, function declarations, variables and so on. + +The result of `eval` is the result of the last statement. + +For example: +```js run +let value = eval('1+1'); +alert(value); // 2 +``` + +```js run +let value = eval('let i = 0; ++i'); +alert(value); // 1 +``` + +The eval'ed code is executed in the current lexical environment, so it can see outer variables: + +```js run no-beautify +let a = 1; + +function f() { + let a = 2; + +*!* + eval('alert(a)'); // 2 +*/!* +} + +f(); +``` + +It can change outer variables as well: + +```js untrusted refresh run +let x = 5; +eval("x = 10"); +alert(x); // 10, value modified +``` + +In strict mode, `eval` has its own lexical environment. So functions and variables, declared inside eval, are not visible outside: + +```js untrusted refresh run +// reminder: 'use strict' is enabled in runnable examples by default + +eval("let x = 5; function f() {}"); + +alert(typeof x); // undefined (no such variable) +// function f is also not visible +``` + +Without `use strict`, `eval` doesn't have its own lexical environment, so we would see `x` and `f` outside. + +## Using "eval" + +In modern programming `eval` is used very sparingly. It's often said that "eval is evil". + +The reason is simple: long, long time ago JavaScript was a much weaker language, many things could only be done with `eval`. But that time passed a decade ago. + +Right now, there's almost no reason to use `eval`. If someone is using it, there's a good chance they can replace it with a modern language construct or a [JavaScript Module](info:modules). + +Please note that its ability to access outer variables has side-effects. + +Code minifiers (tools used before JS gets to production, to compress it) rename local variables into shorter ones (like `a`, `b` etc) to make the code smaller. That's usually safe, but not if `eval` is used, as local variables may be accessed from eval'ed code string. So minifiers don't do that renaming for all variables potentially visible from `eval`. That negatively affects code compression ratio. + +Using outer local variables inside `eval` is also considered a bad programming practice, as it makes maintaining the code more difficult. + +There are two ways how to be totally safe from such problems. + +**If eval'ed code doesn't use outer variables, please call `eval` as `window.eval(...)`:** + +This way the code is executed in the global scope: + +```js untrusted refresh run +let x = 1; +{ + let x = 5; + window.eval('alert(x)'); // 1 (global variable) +} +``` + +**If eval'ed code needs local variables, change `eval` to `new Function` and pass them as arguments:** + +```js run +let f = new Function('a', 'alert(a)'); + +f(5); // 5 +``` + +The `new Function` construct is explained in the chapter . It creates a function from a string, also in the global scope. So it can't see local variables. But it's so much clearer to pass them explicitly as arguments, like in the example above. + +## Summary + +A call to `eval(code)` runs the string of code and returns the result of the last statement. +- Rarely used in modern JavaScript, as there's usually no need. +- Can access outer local variables. That's considered bad practice. +- Instead, to `eval` the code in the global scope, use `window.eval(code)`. +- Or, if your code needs some data from the outer scope, use `new Function` and pass it as arguments. diff --git a/1-js/99-js-misc/03-currying-partials/article.md b/1-js/99-js-misc/03-currying-partials/article.md new file mode 100644 index 0000000000..d71ac23f87 --- /dev/null +++ b/1-js/99-js-misc/03-currying-partials/article.md @@ -0,0 +1,188 @@ +libs: + - lodash + +--- + +# Currying + +[Currying](https://en.wikipedia.org/wiki/Currying) is an advanced technique of working with functions. It's used not only in JavaScript, but in other languages as well. + +Currying is a transformation of functions that translates a function from callable as `f(a, b, c)` into callable as `f(a)(b)(c)`. + +Currying doesn't call a function. It just transforms it. + +Let's see an example first, to better understand what we're talking about, and then practical applications. + +We'll create a helper function `curry(f)` that performs currying for a two-argument `f`. In other words, `curry(f)` for two-argument `f(a, b)` translates it into a function that runs as `f(a)(b)`: + +```js run +*!* +function curry(f) { // curry(f) does the currying transform + return function(a) { + return function(b) { + return f(a, b); + }; + }; +} +*/!* + +// usage +function sum(a, b) { + return a + b; +} + +let curriedSum = curry(sum); + +alert( curriedSum(1)(2) ); // 3 +``` + +As you can see, the implementation is straightforward: it's just two wrappers. + +- The result of `curry(func)` is a wrapper `function(a)`. +- When it is called like `curriedSum(1)`, the argument is saved in the Lexical Environment, and a new wrapper is returned `function(b)`. +- Then this wrapper is called with `2` as an argument, and it passes the call to the original `sum`. + +More advanced implementations of currying, such as [_.curry](https://lodash.com/docs#curry) from lodash library, return a wrapper that allows a function to be called both normally and partially: + +```js run +function sum(a, b) { + return a + b; +} + +let curriedSum = _.curry(sum); // using _.curry from lodash library + +alert( curriedSum(1, 2) ); // 3, still callable normally +alert( curriedSum(1)(2) ); // 3, called partially +``` + +## Currying? What for? + +To understand the benefits we need a worthy real-life example. + +For instance, we have the logging function `log(date, importance, message)` that formats and outputs the information. In real projects such functions have many useful features like sending logs over the network, here we'll just use `alert`: + +```js +function log(date, importance, message) { + alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); +} +``` + +Let's curry it! + +```js +log = _.curry(log); +``` + +After that `log` works normally: + +```js +log(new Date(), "DEBUG", "some debug"); // log(a, b, c) +``` + +...But also works in the curried form: + +```js +log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) +``` + +Now we can easily make a convenience function for current logs: + +```js +// logNow will be the partial of log with fixed first argument +let logNow = log(new Date()); + +// use it +logNow("INFO", "message"); // [HH:mm] INFO message +``` + +Now `logNow` is `log` with fixed first argument, in other words "partially applied function" or "partial" for short. + +We can go further and make a convenience function for current debug logs: + +```js +let debugNow = logNow("DEBUG"); + +debugNow("message"); // [HH:mm] DEBUG message +``` + +So: +1. We didn't lose anything after currying: `log` is still callable normally. +2. We can easily generate partial functions such as for today's logs. + +## Advanced curry implementation + +In case you'd like to get in to the details, here's the "advanced" curry implementation for multi-argument functions that we could use above. + +It's pretty short: + +```js +function curry(func) { + + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args); + } else { + return function(...args2) { + return curried.apply(this, args.concat(args2)); + } + } + }; + +} +``` + +Usage examples: + +```js +function sum(a, b, c) { + return a + b + c; +} + +let curriedSum = curry(sum); + +alert( curriedSum(1, 2, 3) ); // 6, still callable normally +alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg +alert( curriedSum(1)(2)(3) ); // 6, full currying +``` + +The new `curry` may look complicated, but it's actually easy to understand. + +The result of `curry(func)` call is the wrapper `curried` that looks like this: + +```js +// func is the function to transform +function curried(...args) { + if (args.length >= func.length) { // (1) + return func.apply(this, args); + } else { + return function(...args2) { // (2) + return curried.apply(this, args.concat(args2)); + } + } +}; +``` + +When we run it, there are two `if` execution branches: + +1. If passed `args` count is the same or more than the original function has in its definition (`func.length`) , then just pass the call to it using `func.apply`. +2. Otherwise, get a partial: we don't call `func` just yet. Instead, another wrapper is returned, that will re-apply `curried` providing previous arguments together with the new ones. + +Then, if we call it, again, we'll get either a new partial (if not enough arguments) or, finally, the result. + +```smart header="Fixed-length functions only" +The currying requires the function to have a fixed number of arguments. + +A function that uses rest parameters, such as `f(...args)`, can't be curried this way. +``` + +```smart header="A little more than currying" +By definition, currying should convert `sum(a, b, c)` into `sum(a)(b)(c)`. + +But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant. +``` + +## Summary + +*Currying* is a transform that makes `f(a,b,c)` callable as `f(a)(b)(c)`. JavaScript implementations usually both keep the function callable normally and return the partial if the arguments count is not enough. + +Currying allows us to easily get partials. As we've seen in the logging example, after currying the three argument universal function `log(date, importance, message)` gives us partials when called with one argument (like `log(date)`) or two arguments (like `log(date, importance)`). diff --git a/1-js/99-js-misc/04-reference-type/2-check-syntax/solution.md b/1-js/99-js-misc/04-reference-type/2-check-syntax/solution.md new file mode 100644 index 0000000000..ba5d3bf04b --- /dev/null +++ b/1-js/99-js-misc/04-reference-type/2-check-syntax/solution.md @@ -0,0 +1,37 @@ +**Error**! + +Try it: + +```js run +let user = { + name: "John", + go: function() { alert(this.name) } +} + +(user.go)() // error! +``` + +The error message in most browsers does not give us much of a clue about what went wrong. + +**The error appears because a semicolon is missing after `user = {...}`.** + +JavaScript does not auto-insert a semicolon before a bracket `(user.go)()`, so it reads the code like: + +```js no-beautify +let user = { go:... }(user.go)() +``` + +Then we can also see that such a joint expression is syntactically a call of the object `{ go: ... }` as a function with the argument `(user.go)`. And that also happens on the same line with `let user`, so the `user` object has not yet even been defined, hence the error. + +If we insert the semicolon, all is fine: + +```js run +let user = { + name: "John", + go: function() { alert(this.name) } +}*!*;*/!* + +(user.go)() // John +``` + +Please note that parentheses around `(user.go)` do nothing here. Usually they setup the order of operations, but here the dot `.` works first anyway, so there's no effect. Only the semicolon thing matters. 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 100% 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 diff --git a/1-js/99-js-misc/04-reference-type/3-why-this/solution.md b/1-js/99-js-misc/04-reference-type/3-why-this/solution.md new file mode 100644 index 0000000000..e4ee787489 --- /dev/null +++ b/1-js/99-js-misc/04-reference-type/3-why-this/solution.md @@ -0,0 +1,22 @@ + +Here's the explanations. + +1. That's a regular object method call. + +2. The same, parentheses do not change the order of operations here, the dot is first anyway. + +3. Here we have a more complex call `(expression)()`. The call works as if it were split into two lines: + + ```js no-beautify + f = obj.go; // calculate the expression + f(); // call what we have + ``` + + Here `f()` is executed as a function, without `this`. + +4. The similar thing as `(3)`, to the left of the parentheses `()` we have an expression. + +To explain the behavior of `(3)` and `(4)` we need to recall that property accessors (dot or square brackets) return a value of the Reference Type. + +Any operation on it except a method call (like assignment `=` or `||`) turns it into an ordinary value, which does not carry the information allowing to set `this`. + diff --git a/1-js/99-js-misc/04-reference-type/3-why-this/task.md b/1-js/99-js-misc/04-reference-type/3-why-this/task.md new file mode 100644 index 0000000000..e2c073f62b --- /dev/null +++ b/1-js/99-js-misc/04-reference-type/3-why-this/task.md @@ -0,0 +1,26 @@ +importance: 3 + +--- + +# Explain the value of "this" + +In the code below we intend to call `obj.go()` method 4 times in a row. + +But calls `(1)` and `(2)` works differently from `(3)` and `(4)`. Why? + +```js run no-beautify +let obj, method; + +obj = { + go: function() { alert(this); } +}; + +obj.go(); // (1) [object Object] + +(obj.go)(); // (2) [object Object] + +(method = obj.go)(); // (3) undefined + +(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..894db8fc63 --- /dev/null +++ b/1-js/99-js-misc/04-reference-type/article.md @@ -0,0 +1,108 @@ + +# Reference Type + +```warn header="In-depth language feature" +This article covers an advanced topic, to understand certain edge-cases better. + +It's not important. Many experienced developers live fine without knowing it. Read on if you want to know how things work under the hood. +``` + +A dynamically evaluated method call can lose `this`. + +For instance: + +```js run +let user = { + name: "John", + hi() { alert(this.name); }, + bye() { alert("Bye"); } +}; + +user.hi(); // works + +// now let's call user.hi or user.bye depending on the name +*!* +(user.name == "John" ? user.hi : user.bye)(); // Error! +*/!* +``` + +On the last line there is a conditional operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`. + +Then the method is immediately called with parentheses `()`. But it doesn't work correctly! + +As you can see, the call results in an error, because the value of `"this"` inside the call becomes `undefined`. + +This works (object dot method): +```js +user.hi(); +``` + +This doesn't (evaluated method): +```js +(user.name == "John" ? user.hi : user.bye)(); // Error! +``` + +Why? If we want to understand why it happens, let's get under the hood of how `obj.method()` call works. + +## Reference type explained + +Looking closely, we may notice two operations in `obj.method()` statement: + +1. First, the dot `'.'` retrieves the property `obj.method`. +2. Then parentheses `()` execute it. + +So, how does the information about `this` get passed from the first part to the second one? + +If we put these operations on separate lines, then `this` will be lost for sure: + +```js run +let user = { + name: "John", + hi() { alert(this.name); } +}; + +*!* +// split getting and calling the method in two lines +let hi = user.hi; +hi(); // Error, because this is undefined +*/!* +``` + +Here `hi = user.hi` puts the function into the variable, and then on the last line it is completely standalone, and so there's no `this`. + +**To make `user.hi()` calls work, JavaScript uses a trick -- the dot `'.'` returns not a function, but a value of the special [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).** + +The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language. + +The value of Reference Type is a three-value combination `(base, name, strict)`, where: + +- `base` is the object. +- `name` is the property name. +- `strict` is true if `use strict` is in effect. + +The result of a property access `user.hi` is not a function, but a value of Reference Type. For `user.hi` in strict mode it is: + +```js +// Reference Type value +(user, "hi", true) +``` + +When parentheses `()` are called on the Reference Type, they receive the full information about the object and its method, and can set the right `this` (`user` in this case). + +Reference type is a special "intermediary" internal type, with the purpose to pass information from dot `.` to calling parentheses `()`. + +Any other operation like assignment `hi = user.hi` discards the reference type as a whole, takes the value of `user.hi` (a function) and passes it on. So any further operation "loses" `this`. + +So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj['method']()` syntax (they do the same here). There are various ways to solve this problem such as [func.bind()](/bind#solution-2-bind). + +## Summary + +Reference Type is an internal type of the language. + +Reading a property, such as with dot `.` in `obj.method()` returns not exactly the property value, but a special "reference type" value that stores both the property value and the object it was taken from. + +That's for the subsequent method call `()` to get the object and set `this` to it. + +For all other operations, the reference type automatically becomes the property value (a function in our case). + +The whole mechanics is hidden from our eyes. It only matters in subtle cases, such as when a method is obtained dynamically from the object, using an expression. diff --git a/1-js/99-js-misc/05-bigint/article.md b/1-js/99-js-misc/05-bigint/article.md new file mode 100644 index 0000000000..2a1cfc843f --- /dev/null +++ b/1-js/99-js-misc/05-bigint/article.md @@ -0,0 +1,130 @@ +# BigInt + +[recent caniuse="bigint"] + +`BigInt` is a special numeric type that provides support for integers of arbitrary length. + +A bigint is created by appending `n` to the end of an integer literal or by calling the function `BigInt` that creates bigints from strings, numbers etc. + +```js +const bigint = 1234567890123456789012345678901234567890n; + +const sameBigint = BigInt("1234567890123456789012345678901234567890"); + +const bigintFromNumber = BigInt(10); // same as 10n +``` + +## Math operators + +`BigInt` can mostly be used like a regular number, for example: + +```js run +alert(1n + 2n); // 3 + +alert(5n / 2n); // 2 +``` + +Please note: the division `5/2` returns the result rounded towards zero, without the decimal part. All operations on bigints return bigints. + +We can't mix bigints and regular numbers: + +```js run +alert(1n + 2); // Error: Cannot mix BigInt and other types +``` + +We should explicitly convert them if needed: using either `BigInt()` or `Number()`, like this: + +```js run +let bigint = 1n; +let number = 2; + +// number to bigint +alert(bigint + BigInt(number)); // 3 + +// bigint to number +alert(Number(bigint) + number); // 3 +``` + +The conversion operations are always silent, never give errors, but if the bigint is too huge and won't fit the number type, then extra bits will be cut off, so we should be careful doing such conversion. + +````smart header="The unary plus is not supported on bigints" +The unary plus operator `+value` is a well-known way to convert `value` to a number. + +In order to avoid confusion, it's not supported on bigints: +```js run +let bigint = 1n; + +alert( +bigint ); // error +``` +So we should use `Number()` to convert a bigint to a number. +```` + +## Comparisons + +Comparisons, such as `<`, `>` work with bigints and numbers just fine: + +```js run +alert( 2n > 1n ); // true + +alert( 2n > 1 ); // true +``` + +Please note though, as numbers and bigints belong to different types, they can be equal `==`, but not strictly equal `===`: + +```js run +alert( 1 == 1n ); // true + +alert( 1 === 1n ); // false +``` + +## Boolean operations + +When inside `if` or other boolean operations, bigints behave like numbers. + +For instance, in `if`, bigint `0n` is falsy, other values are truthy: + +```js run +if (0n) { + // never executes +} +``` + +Boolean operators, such as `||`, `&&` and others also work with bigints similar to numbers: + +```js run +alert( 1n || 2 ); // 1 (1n is considered truthy) + +alert( 0n || 2 ); // 2 (0n is considered falsy) +``` + +## Polyfills + +Polyfilling bigints is tricky. The reason is that many JavaScript operators, such as `+`, `-` and so on behave differently with bigints compared to regular numbers. + +For example, division of bigints always returns a bigint (rounded if necessary). + +To emulate such behavior, a polyfill would need to analyze the code and replace all such operators with its functions. But doing so is cumbersome and would cost a lot of performance. + +So, there's no well-known good polyfill. + +Although, the other way around is proposed by the developers of [JSBI](https://github.com/GoogleChromeLabs/jsbi) library. + +This library implements big numbers using its own methods. We can use them instead of native bigints: + +| Operation | native `BigInt` | JSBI | +|-----------|-----------------|------| +| Creation from Number | `a = BigInt(789)` | `a = JSBI.BigInt(789)` | +| Addition | `c = a + b` | `c = JSBI.add(a, b)` | +| Subtraction | `c = a - b` | `c = JSBI.subtract(a, b)` | +| ... | ... | ... | + +...And then use the polyfill (Babel plugin) to convert JSBI calls to native bigints for those browsers that support them. + +In other words, this approach suggests that we write code in JSBI instead of native bigints. But JSBI works with numbers as with bigints internally, emulates them closely following the specification, so the code will be "bigint-ready". + +We can use such JSBI code "as is" for engines that don't support bigints and for those that do support - the polyfill will convert the calls to native bigints. + +## References + +- [MDN docs on BigInt](mdn:/JavaScript/Reference/Global_Objects/BigInt). +- [Specification](https://tc39.es/ecma262/#sec-bigint-objects). diff --git a/1-js/99-js-misc/06-unicode/article.md b/1-js/99-js-misc/06-unicode/article.md new file mode 100644 index 0000000000..4f144f8241 --- /dev/null +++ b/1-js/99-js-misc/06-unicode/article.md @@ -0,0 +1,172 @@ + +# Unicode, String internals + +```warn header="Advanced knowledge" +The section goes deeper into string internals. This knowledge will be useful for you if you plan to deal with emoji, rare mathematical or hieroglyphic characters, or other rare symbols. +``` + +As we already know, JavaScript strings are based on [Unicode](https://en.wikipedia.org/wiki/Unicode): each character is represented by a byte sequence of 1-4 bytes. + +JavaScript allows us to insert a character into a string by specifying its hexadecimal Unicode code with one of these three notations: + +- `\xXX` + + `XX` must be two hexadecimal digits with a value between `00` and `FF`, then `\xXX` is the character whose Unicode code is `XX`. + + Because the `\xXX` notation supports only two hexadecimal digits, it can be used only for the first 256 Unicode characters. + + These first 256 characters include the Latin alphabet, most basic syntax characters, and some others. For example, `"\x7A"` is the same as `"z"` (Unicode `U+007A`). + + ```js run + alert( "\x7A" ); // z + alert( "\xA9" ); // ©, the copyright symbol + ``` + +- `\uXXXX` + `XXXX` must be exactly 4 hex digits with the value between `0000` and `FFFF`, then `\uXXXX` is the character whose Unicode code is `XXXX`. + + Characters with Unicode values greater than `U+FFFF` can also be represented with this notation, but in this case, we will need to use a so called surrogate pair (we will talk about surrogate pairs later in this chapter). + + ```js run + alert( "\u00A9" ); // ©, the same as \xA9, using the 4-digit hex notation + alert( "\u044F" ); // я, the Cyrillic alphabet letter + alert( "\u2191" ); // ↑, the arrow up symbol + ``` + +- `\u{X…XXXXXX}` + + `X…XXXXXX` must be a hexadecimal value of 1 to 6 bytes between `0` and `10FFFF` (the highest code point defined by Unicode). This notation allows us to easily represent all existing Unicode characters. + + ```js run + alert( "\u{20331}" ); // 佫, a rare Chinese character (long Unicode) + alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long Unicode) + ``` + +## Surrogate pairs + +All frequently used characters have 2-byte codes (4 hex digits). Letters in most European languages, numbers, and the basic unified CJK ideographic sets (CJK -- from Chinese, Japanese, and Korean writing systems), have a 2-byte representation. + +Initially, JavaScript was based on UTF-16 encoding that only allowed 2 bytes per character. But 2 bytes only allow 65536 combinations and that's not enough for every possible symbol of Unicode. + +So rare symbols that require more than 2 bytes are encoded with a pair of 2-byte characters called "a surrogate pair". + +As a side effect, the length of such symbols is `2`: + +```js run +alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X +alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY +alert( '𩷶'.length ); // 2, a rare Chinese character +``` + +That's because surrogate pairs did not exist at the time when JavaScript was created, and thus are not correctly processed by the language! + +We actually have a single symbol in each of the strings above, but the `length` property shows a length of `2`. + +Getting a symbol can also be tricky, because most language features treat surrogate pairs as two characters. + +For example, here we can see two odd characters in the output: + +```js run +alert( '𝒳'[0] ); // shows strange symbols... +alert( '𝒳'[1] ); // ...pieces of the surrogate pair +``` + +Pieces of a surrogate pair have no meaning without each other. So the alerts in the example above actually display garbage. + +Technically, surrogate pairs are also detectable by their codes: if a character has the code in the interval of `0xd800..0xdbff`, then it is the first part of the surrogate pair. The next character (second part) must have the code in interval `0xdc00..0xdfff`. These intervals are reserved exclusively for surrogate pairs by the standard. + +So the methods [String.fromCodePoint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint) and [str.codePointAt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt) were added in JavaScript to deal with surrogate pairs. + +They are essentially the same as [String.fromCharCode](mdn:js/String/fromCharCode) and [str.charCodeAt](mdn:js/String/charCodeAt), but they treat surrogate pairs correctly. + +One can see the difference here: + +```js run +// charCodeAt is not surrogate-pair aware, so it gives codes for the 1st part of 𝒳: + +alert( '𝒳'.charCodeAt(0).toString(16) ); // d835 + +// codePointAt is surrogate-pair aware +alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, reads both parts of the surrogate pair +``` + +That said, if we take from position 1 (and that's rather incorrect here), then they both return only the 2nd part of the pair: + +```js run +alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3 +alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3 +// meaningless 2nd half of the pair +``` + +You will find more ways to deal with surrogate pairs later in the chapter . There are probably special libraries for that too, but nothing famous enough to suggest here. + +````warn header="Takeaway: splitting strings at an arbitrary point is dangerous" +We can't just split a string at an arbitrary position, e.g. take `str.slice(0, 4)` and expect it to be a valid string, e.g.: + +```js run +alert( 'hi 😂'.slice(0, 4) ); // hi [?] +``` + +Here we can see a garbage character (first half of the smile surrogate pair) in the output. + +Just be aware of it if you intend to reliably work with surrogate pairs. May not be a big problem, but at least you should understand what happens. +```` + +## Diacritical marks and normalization + +In many languages, there are symbols that are composed of the base character with a mark above/under it. + +For instance, the letter `a` can be the base character for these characters: `àáâäãåā`. + +Most common "composite" characters have their own code in the Unicode table. But not all of them, because there are too many possible combinations. + +To support arbitrary compositions, the Unicode standard allows us to use several Unicode characters: the base character followed by one or many "mark" characters that "decorate" it. + +For instance, if we have `S` followed by the special "dot above" character (code `\u0307`), it is shown as Ṡ. + +```js run +alert( 'S\u0307' ); // Ṡ +``` + +If we need an additional mark above the letter (or below it) -- no problem, just add the necessary mark character. + +For instance, if we append a character "dot below" (code `\u0323`), then we'll have "S with dots above and below": `Ṩ`. + +For example: + +```js run +alert( 'S\u0307\u0323' ); // Ṩ +``` + +This provides great flexibility, but also an interesting problem: two characters may visually look the same, but be represented with different Unicode compositions. + +For instance: + +```js run +let s1 = 'S\u0307\u0323'; // Ṩ, S + dot above + dot below +let s2 = 'S\u0323\u0307'; // Ṩ, S + dot below + dot above + +alert( `s1: ${s1}, s2: ${s2}` ); + +alert( s1 == s2 ); // false though the characters look identical (?!) +``` + +To solve this, there exists a "Unicode normalization" algorithm that brings each string to the single "normal" form. + +It is implemented by [str.normalize()](mdn:js/String/normalize). + +```js run +alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true +``` + +It's funny that in our situation `normalize()` actually brings together a sequence of 3 characters to one: `\u1e68` (S with two dots). + +```js run +alert( "S\u0307\u0323".normalize().length ); // 1 + +alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true +``` + +In reality, this is not always the case. The reason is that the symbol `Ṩ` is "common enough", so Unicode creators included it in the main table and gave it the code. + +If you want to learn more about normalization rules and variants -- they are described in the appendix of the Unicode standard: [Unicode Normalization Forms](https://www.unicode.org/reports/tr15/), but for most practical purposes the information from this section is enough. diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/article.md b/1-js/99-js-misc/07-weakref-finalizationregistry/article.md new file mode 100644 index 0000000000..777bf703ce --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/article.md @@ -0,0 +1,483 @@ + +# WeakRef and FinalizationRegistry + +```warn header="\"Hidden\" features of the language" +This article covers a very narrowly focused topic, that most developers extremely rarely encounter in practice (and may not even be aware of its existence). + +We recommend skipping this chapter if you have just started learning JavaScript. +``` + +Recalling the basic concept of the *reachability principle* from the chapter, +we can note that the JavaScript engine is guaranteed to keep values in memory that are accessible or in use. + +For example: + + +```js +// the user variable holds a strong reference to the object +let user = { name: "John" }; + +// let's overwrite the value of the user variable +user = null; + +// the reference is lost and the object will be deleted from memory + +``` + +Or a similar, but slightly more complicated code with two strong references: + +```js +// the user variable holds a strong reference to the object +let user = { name: "John" }; + +// copied the strong reference to the object into the admin variable +*!* +let admin = user; +*/!* + +// let's overwrite the value of the user variable +user = null; + +// the object is still reachable through the admin variable +``` +The object `{ name: "John" }` would only be deleted from memory if there were no strong references to it (if we also overwrote the value of the `admin` variable). + +In JavaScript, there is a concept called `WeakRef`, which behaves slightly differently in this case. + + +````smart header="Terms: \"Strong reference\", \"Weak reference\"" +**Strong reference** - is a reference to an object or value, that prevents them from being deleted by the garbage collector. Thereby, keeping the object or value in memory, to which it points. + +This means, that the object or value remains in memory and is not collected by the garbage collector as long, as there are active strong references to it. + +In JavaScript, ordinary references to objects are strong references. For example: + +```js +// the user variable holds a strong reference to this object +let user = { name: "John" }; +``` +**Weak reference** - is a reference to an object or value, that does *not* prevent them from being deleted by the garbage collector. +An object or value can be deleted by the garbage collector if, the only remaining references to them are weak references. +```` + +## WeakRef + + +````warn header="Note of caution" +Before we dive into it, it is worth noting that the correct use of the structures discussed in this article requires very careful thought, and they are best avoided if possible. +```` + +`WeakRef` - is an object, that contains a weak reference to another object, called `target` or `referent`. + +The peculiarity of `WeakRef` is that it does not prevent the garbage collector from deleting its referent-object. In other words, a `WeakRef` object does not keep the `referent` object alive. + +Now let's take the `user` variable as the "referent" and create a weak reference from it to the `admin` variable. +To create a weak reference, you need to use the `WeakRef` constructor, passing in the target object (the object you want a weak reference to). + +In our case — this is the `user` variable: + + +```js +// the user variable holds a strong reference to the object +let user = { name: "John" }; + +// the admin variable holds a weak reference to the object +*!* +let admin = new WeakRef(user); +*/!* + +``` + +The diagram below depicts two types of references: a strong reference using the `user` variable and a weak reference using the `admin` variable: + +![](weakref-finalizationregistry-01.svg) + +Then, at some point, we stop using the `user` variable - it gets overwritten, goes out of scope, etc., while keeping the `WeakRef` instance in the `admin` variable: + +```js +// let's overwrite the value of the user variable +user = null; +``` + +A weak reference to an object is not enough to keep it "alive". When the only remaining references to a referent-object are weak references, the garbage collector is free to destroy this object and use its memory for something else. + +However, until the object is actually destroyed, the weak reference may return it, even if there are no more strong references to this object. +That is, our object becomes a kind of "[Schrödinger's cat](https://en.wikipedia.org/wiki/Schr%C3%B6dinger%27s_cat)" – we cannot know for sure whether it's "alive" or "dead": + +![](weakref-finalizationregistry-02.svg) + +At this point, to get the object from the `WeakRef` instance, we will use its `deref()` method. + +The `deref()` method returns the referent-object that the `WeakRef` points to, if the object is still in memory. If the object has been deleted by the garbage collector, then the `deref()` method will return `undefined`: + + +```js +let ref = admin.deref(); + +if (ref) { + // the object is still accessible: we can perform any manipulations with it +} else { + // the object has been collected by the garbage collector +} +``` + +## WeakRef use cases + +`WeakRef` is typically used to create caches or [associative arrays](https://en.wikipedia.org/wiki/Associative_array) that store resource-intensive objects. +This allows one to avoid preventing these objects from being collected by the garbage collector solely based on their presence in the cache or associative array. + +One of the primary examples - is a situation when we have numerous binary image objects (for instance, represented as `ArrayBuffer` or `Blob`), and we want to associate a name or path with each image. +Existing data structures are not quite suitable for these purposes: + +- Using `Map` to create associations between names and images, or vice versa, will keep the image objects in memory since they are present in the `Map` as keys or values. +- `WeakMap` is ineligible for this goal either: because the objects represented as `WeakMap` keys use weak references, and are not protected from deletion by the garbage collector. + +But, in this situation, we need a data structure that would use weak references in its values. + +For this purpose, we can use a `Map` collection, whose values are `WeakRef` instances referring to the large objects we need. +Consequently, we will not keep these large and unnecessary objects in memory longer than they should be. + +Otherwise, this is a way to get the image object from the cache if it is still reachable. +If it has been garbage collected, we will re-generate or re-download it again. + +This way, less memory is used in some situations. + +## Example №1: using WeakRef for caching + +Below is a code snippet that demonstrates the technique of using `WeakRef`. + +In short, we use a `Map` with string keys and `WeakRef` objects as their values. +If the `WeakRef` object has not been collected by the garbage collector, we get it from the cache. +Otherwise, we re-download it again and put it in the cache for further possible reuse: + +```js +function fetchImg() { + // abstract function for downloading images... +} + +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); +``` + +Let's delve into the details of what happened here: +1. `weakRefCache` - is a higher-order function that takes another function, `fetchImg`, as an argument. In this example, we can neglect a detailed description of the `fetchImg` function, since it can be any logic for downloading images. +2. `imgCache` - is a cache of images, that stores cached results of the `fetchImg` function, in the form of string keys (image name) and `WeakRef` objects as their values. +3. Return an anonymous function that takes the image name as an argument. This argument will be used as a key for the cached image. +4. Trying to get the cached result from the cache, using the provided key (image name). +5. If the cache contains a value for the specified key, and the `WeakRef` object has not been deleted by the garbage collector, return the cached result. +6. If there is no entry in the cache with the requested key, or `deref()` method returns `undefined` (meaning that the `WeakRef` object has been garbage collected), the `fetchImg` function downloads the image again. +7. Put the downloaded image into the cache as a `WeakRef` object. + +Now we have a `Map` collection, where the keys - are image names as strings, and values - are `WeakRef` objects containing the images themselves. + +This technique helps to avoid allocating a large amount of memory for resource-intensive objects, that nobody uses anymore. +It also saves memory and time in case of reusing cached objects. + +Here is a visual representation of what this code looks like: + +![](weakref-finalizationregistry-03.svg) + +But, this implementation has its drawbacks: over time, `Map` will be filled with strings as keys, that point to a `WeakRef`, whose referent-object has already been garbage collected: + +![](weakref-finalizationregistry-04.svg) + +One way to handle this problem - is to periodically scavenge the cache and clear out "dead" entries. +Another way - is to use finalizers, which we will explore next. + + +## Example №2: Using WeakRef to track DOM objects + +Another use case for `WeakRef` - is tracking DOM objects. + +Let's imagine a scenario where some third-party code or library interacts with elements on our page as long as they exist in the DOM. +For example, it could be an external utility for monitoring and notifying about the system's state (commonly so-called "logger" – a program that sends informational messages called "logs"). + +Interactive example: + +[codetabs height=420 src="weakref-dom"] + +When the "Start sending messages" button is clicked, in the so-called "logs display window" (an element with the `.window__body` class), messages (logs) start to appear. + +But, as soon as this element is deleted from the DOM, the logger should stop sending messages. +To reproduce the removal of this element, just click the "Close" button in the top right corner. + +In order not to complicate our work, and not to notify third-party code every time our DOM-element is available, and when it is not, it will be enough to create a weak reference to it using `WeakRef`. + +Once the element is removed from the DOM, the logger will notice it and stop sending messages. + +Now let's take a closer look at the source code (*tab `index.js`*): + +1. Get the DOM-element of the "Start sending messages" button. +2. Get the DOM-element of the "Close" button. +3. Get the DOM-element of the logs display window using the `new WeakRef()` constructor. This way, the `windowElementRef` variable holds a weak reference to the DOM-element. +4. Add an event listener on the "Start sending messages" button, responsible for starting the logger when clicked. +5. Add an event listener on the "Close" button, responsible for closing the logs display window when clicked. +6. Use `setInterval` to start displaying a new message every second. +7. If the DOM-element of the logs display window is still accessible and kept in memory, create and send a new message. +8. If the `deref()` method returns `undefined`, it means that the DOM-element has been deleted from memory. In this case, the logger stops displaying messages and clears the timer. +9. `alert`, which will be called, after the DOM-element of the logs display window is deleted from memory (i.e. after clicking the "Close" button). **Note, that deletion from memory may not happen immediately, as it depends only on the internal mechanisms of the garbage collector.** + + We cannot control this process directly from the code. However, despite this, we still have the option to force garbage collection from the browser. + + In Google Chrome, for example, to do this, you need to open the developer tools (`key:Ctrl` + `key:Shift` + `key:J` on Windows/Linux or `key:Option` + `key:⌘` + `key:J` on macOS), go to the "Performance" tab, and click on the bin icon button – "Collect garbage": + + ![](google-chrome-developer-tools.png) + +
+ This functionality is supported in most modern browsers. After the actions are taken, the alert will trigger immediately. + +## FinalizationRegistry + +Now it is time to talk about finalizers. Before we move on, let's clarify the terminology: + +**Cleanup callback (finalizer)** - is a function that is executed, when an object, registered in the `FinalizationRegistry`, is deleted from memory by the garbage collector. + +Its purpose - is to provide the ability to perform additional operations, related to the object, after it has been finally deleted from memory. + +**Registry** (or `FinalizationRegistry`) - is a special object in JavaScript that manages the registration and unregistration of objects and their cleanup callbacks. + +This mechanism allows registering an object to track and associate a cleanup callback with it. +Essentially it is a structure that stores information about registered objects and their cleanup callbacks, and then automatically invokes those callbacks when the objects are deleted from memory. + +To create an instance of the `FinalizationRegistry`, it needs to call its constructor, which takes a single argument - the cleanup callback (finalizer). + +Syntax: + +```js +function cleanupCallback(heldValue) { + // cleanup callback code +} + +const registry = new FinalizationRegistry(cleanupCallback); +``` + +Here: + +- `cleanupCallback` - a cleanup callback that will be automatically called when a registered object is deleted from memory. +- `heldValue` - the value that is passed as an argument to the cleanup callback. If `heldValue` is an object, the registry keeps a strong reference to it. +- `registry` - an instance of `FinalizationRegistry`. + +`FinalizationRegistry` methods: + +- `register(target, heldValue [, unregisterToken])` - used to register objects in the registry. + + `target` - the object being registered for tracking. If the `target` is garbage collected, the cleanup callback will be called with `heldValue` as its argument. + + Optional `unregisterToken` – an unregistration token. It can be passed to unregister an object before the garbage collector deletes it. Typically, the `target` object is used as `unregisterToken`, which is the standard practice. +- `unregister(unregisterToken)` - the `unregister` method is used to unregister an object from the registry. It takes one argument - `unregisterToken` (the unregister token that was obtained when registering the object). + +Now let's move on to a simple example. Let's use the already-known `user` object and create an instance of `FinalizationRegistry`: + +```js +let user = { name: "John" }; + +const registry = new FinalizationRegistry((heldValue) => { + console.log(`${heldValue} has been collected by the garbage collector.`); +}); +``` + +Then, we will register the object, that requires a cleanup callback by calling the `register` method: + +```js +registry.register(user, user.name); +``` + +The registry does not keep a strong reference to the object being registered, as this would defeat its purpose. If the registry kept a strong reference, then the object would never be garbage collected. + +If the object is deleted by the garbage collector, our cleanup callback may be called at some point in the future, with the `heldValue` passed to it: + +```js +// When the user object is deleted by the garbage collector, the following message will be printed in the console: +"John has been collected by the garbage collector." +``` + +There are also situations where, even in implementations that use a cleanup callback, there is a chance that it will not be called. + +For example: +- When the program fully terminates its operation (for example, when closing a tab in a browser). +- When the `FinalizationRegistry` instance itself is no longer reachable to JavaScript code. + If the object that creates the `FinalizationRegistry` instance goes out of scope or is deleted, the cleanup callbacks registered in that registry might also not be invoked. + +## Caching with FinalizationRegistry + +Returning to our *weak* cache example, we can notice the following: +- Even though the values wrapped in the `WeakRef` have been collected by the garbage collector, there is still an issue of "memory leakage" in the form of the remaining keys, whose values have been collected by the garbage collector. + +Here is an improved caching example using `FinalizationRegistry`: + +```js +function fetchImg() { + // abstract function for downloading images... +} + +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. To manage the cleanup of "dead" cache entries, when the associated `WeakRef` objects are collected by the garbage collector, we create a `FinalizationRegistry` cleanup registry. + + The important point here is, that in the cleanup callback, it should be checked, if the entry was deleted by the garbage collector and not re-added, in order not to delete a "live" entry. +2. Once the new value (image) is downloaded and put into the cache, we register it in the finalizer registry to track the `WeakRef` object. + +This implementation contains only actual or "live" key/value pairs. +In this case, each `WeakRef` object is registered in the `FinalizationRegistry`. +And after the objects are cleaned up by the garbage collector, the cleanup callback will delete all `undefined` values. + +Here is a visual representation of the updated code: + +![](weakref-finalizationregistry-05.svg) + +A key aspect of the updated implementation is that finalizers allow parallel processes to be created between the "main" program and cleanup callbacks. +In the context of JavaScript, the "main" program - is our JavaScript-code, that runs and executes in our application or web page. + +Hence, from the moment an object is marked for deletion by the garbage collector, and to the actual execution of the cleanup callback, there may be a certain time gap. +It is important to understand that during this time gap, the main program can make any changes to the object or even bring it back to memory. + +That's why, in the cleanup callback, we must check to see if an entry has been added back to the cache by the main program to avoid deleting "live" entries. +Similarly, when searching for a key in the cache, there is a chance that the value has been deleted by the garbage collector, but the cleanup callback has not been executed yet. + +Such situations require special attention if you are working with `FinalizationRegistry`. + +## Using WeakRef and FinalizationRegistry in practice + +Moving from theory to practice, imagine a real-life scenario, where a user synchronizes their photos on a mobile device with some cloud service +(such as [iCloud](https://en.wikipedia.org/wiki/ICloud) or [Google Photos](https://en.wikipedia.org/wiki/Google_Photos)), +and wants to view them from other devices. In addition to the basic functionality of viewing photos, such services offer a lot of additional features, for example: + +- Photo editing and video effects. +- Creating "memories" and albums. +- Video montage from a series of photos. +- ...and much more. + +Here, as an example, we will use a fairly primitive implementation of such a service. +The main point - is to show a possible scenario of using `WeakRef` and `FinalizationRegistry` together in real life. + +Here is what it looks like: + +![](weakref-finalizationregistry-demo-01.png) + +
+On the left side, there is a cloud library of photos (they are displayed as thumbnails). +We can select the images we need and create a collage, by clicking the "Create collage" button on the right side of the page. +Then, the resulting collage can be downloaded as an image. +

+ +To increase page loading speed, it would be reasonable to download and display photo thumbnails in *compressed* quality. +But, to create a collage from selected photos, download and use them in *full-size* quality. + +Below, we can see, that the intrinsic size of the thumbnails is 240x240 pixels. +The size was chosen on purpose to increase loading speed. +Moreover, we do not need full-size photos in preview mode. + +![](weakref-finalizationregistry-demo-02.png) + +
+Let's assume, that we need to create a collage of 4 photos: we select them, and then click the "Create collage" button. +At this stage, the already known to us weakRefCache function checks whether the required image is in the cache. +If not, it downloads it from the cloud and puts it in the cache for further use. +This happens for each selected image: +

+ +![](weakref-finalizationregistry-demo-03.gif) + +
+ +Paying attention to the output in the console, you can see, which of the photos were downloaded from the cloud - this is indicated by FETCHED_IMAGE. +Since this is the first attempt to create a collage, this means, that at this stage the "weak cache" was still empty, and all the photos were downloaded from the cloud and put in it. + +But, along with the process of downloading images, there is also a process of memory cleanup by the garbage collector. +This means, that the object stored in the cache, which we refer to, using a weak reference, is deleted by the garbage collector. +And our finalizer executes successfully, thereby deleting the key, by which the image was stored in the cache. +CLEANED_IMAGE notifies us about it: + +![](weakref-finalizationregistry-demo-04.jpg) + +
+Next, we realize that we do not like the resulting collage, and decide to change one of the images and create a new one. +To do this, just deselect the unnecessary image, select another one, and click the "Create collage" button again: +

+ +![](weakref-finalizationregistry-demo-05.gif) + +
+But this time not all images were downloaded from the network, and one of them was taken from the weak cache: the CACHED_IMAGE message tells us about it. +This means that at the time of collage creation, the garbage collector had not yet deleted our image, and we boldly took it from the cache, +thereby reducing the number of network requests and speeding up the overall time of the collage creation process: +

+ +![](weakref-finalizationregistry-demo-06.jpg) + +
+Let's "play around" a little more, by replacing one of the images again and creating a new collage: +

+ +![](weakref-finalizationregistry-demo-07.gif) + +
+This time the result is even more impressive. Of the 4 images selected, 3 of them were taken from the weak cache, and only one had to be downloaded from the network. +The reduction in network load was about 75%. Impressive, isn't it? +

+ +![](weakref-finalizationregistry-demo-08.jpg) + +
+ +Of course, it is important to remember, that such behavior is not guaranteed, and depends on the specific implementation and operation of the garbage collector. + +Based on this, a completely logical question immediately arises: why do not we use an ordinary cache, where we can manage its entities ourselves, instead of relying on the garbage collector? +That's right, in the vast majority of cases there is no need to use `WeakRef` and `FinalizationRegistry`. + +Here, we simply demonstrated an alternative implementation of similar functionality, using a non-trivial approach with interesting language features. +Still, we cannot rely on this example, if we need a constant and predictable result. + +You can [open this example in the sandbox](sandbox:weakref-finalizationregistry). + +## Summary + +`WeakRef` - designed to create weak references to objects, allowing them to be deleted from memory by the garbage collector if there are no longer strong references to them. +This is beneficial for addressing excessive memory usage and optimizing the utilization of system resources in applications. + +`FinalizationRegistry` - is a tool for registering callbacks, that are executed when objects that are no longer strongly referenced, are destroyed. +This allows releasing resources associated with the object or performing other necessary operations before deleting the object from memory. \ No newline at end of file diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/google-chrome-developer-tools.png b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/google-chrome-developer-tools.png differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.css b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.css new file mode 100644 index 0000000000..f6df812d07 --- /dev/null +++ b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-dom.view/index.html b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.html new file mode 100644 index 0000000000..7f93af4c7b --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.html @@ -0,0 +1,28 @@ + + + + + + + WeakRef DOM Logger + + + + +
+ +
+
+

Messages:

+ +
+
+ No messages. +
+
+
+ + + + + diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.js b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.js new file mode 100644 index 0000000000..ea55b44781 --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-dom.view/index.js @@ -0,0 +1,24 @@ +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 = `Message: System status OK: ${new Date().toLocaleTimeString()}`; + element.deref().append(payload); + } else { // (8) + alert("The element has been deleted."); // (9) + clearInterval(timerId); + } + }, 1000); +}; \ No newline at end of file diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-01.svg b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-01.svg new file mode 100644 index 0000000000..2a507dbcdb --- /dev/null +++ b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-02.svg b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-02.svg new file mode 100644 index 0000000000..6cc199a128 --- /dev/null +++ b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-03.svg b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-03.svg new file mode 100644 index 0000000000..949a14f9f3 --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-03.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + key + value + image-01.jpg + image-02.jpg + image-03.jpg + + + + + + + + + + + + + + WeakRef object + + + + + + + + + + + + + + + + WeakRef object + + + + + + + + + + + + + + + + + + + WeakRef object + + + + + + + \ No newline at end of file diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-04.svg b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-04.svg new file mode 100644 index 0000000000..1177d6580d --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-04.svg @@ -0,0 +1,77 @@ + + + + + + + name: "John" + Object + + admin + + + + + + + + + key + value + image-01.jpg + image-02.jpg + image-03.jpg + + + + + + + + + + + + + + WeakRef object + + + + + + + + + + + + + + + + WeakRef object + + + + + undefined + undefined + + + + + + + + + + + + + + + WeakRef object + + + \ No newline at end of file diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-05.svg b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-05.svg new file mode 100644 index 0000000000..e738f8e7ed --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-05.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + image-02.jpg + image-03.jpg + + key + value + image-01.jpg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WeakRef object + + + + + + + + + + + + + + + + WeakRef object + + + + + undefined + undefined + Deleted by FinalizationRegistry cleanup callback + + + + + + + + + + + + + + + WeakRef object + + + + \ No newline at end of file diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-01.png b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-01.png differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-02.png b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-02.png differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-03.gif b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-03.gif differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-04.jpg b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-04.jpg differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-05.gif b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-05.gif differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-06.jpg b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-06.jpg differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-07.gif b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-07.gif differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-08.jpg b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry-demo-08.jpg differ diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.css b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.css new file mode 100644 index 0000000000..e6c9e39606 --- /dev/null +++ b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.html b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.html new file mode 100644 index 0000000000..7ce52f927f --- /dev/null +++ b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.html @@ -0,0 +1,49 @@ + + + + + + + Photo Library Collage + + + + +
+
+

+ Photo Library Collage +

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

+
+ +
+
+

Logger:

+
+
+
+
+
+ + + + + diff --git a/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.js b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/index.js new file mode 100644 index 0000000000..983b34d9ab --- /dev/null +++ b/1-js/99-js-misc/07-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/07-weakref-finalizationregistry/weakref-finalizationregistry.view/utils.js b/1-js/99-js-misc/07-weakref-finalizationregistry/weakref-finalizationregistry.view/utils.js new file mode 100644 index 0000000000..f0140c116a --- /dev/null +++ b/1-js/99-js-misc/07-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/index.md b/1-js/99-js-misc/index.md new file mode 100644 index 0000000000..79cd72fe7c --- /dev/null +++ b/1-js/99-js-misc/index.md @@ -0,0 +1,2 @@ + +# Miscellaneous diff --git a/1-js/plan3.txt b/1-js/plan3.txt deleted file mode 100644 index 4d553174f5..0000000000 --- a/1-js/plan3.txt +++ /dev/null @@ -1,5 +0,0 @@ -todo: - -intl? -proxy? -eval? diff --git a/2-ui/1-document/01-browser-environment/article.md b/2-ui/1-document/01-browser-environment/article.md index 2266a2defa..eedc28fb36 100644 --- a/2-ui/1-document/01-browser-environment/article.md +++ b/2-ui/1-document/01-browser-environment/article.md @@ -1,42 +1,44 @@ # Browser environment, specs -The JavaScript language was initially created for web browsers. Since then, it has evolved and become a language with many uses and platforms. +The JavaScript language was initially created for web browsers. Since then, it has evolved into a language with many uses and platforms. -A platform may be a browser, or a web-server, or a washing machine, or another *host*. Each of them provides platform-specific functionality. The JavaScript specification calls that a *host environment*. +A platform may be a browser, or a web-server or another *host*, or even a "smart" coffee machine if it can run JavaScript. Each of these provides platform-specific functionality. The JavaScript specification calls that a *host environment*. -A host environment provides platform-specific objects and functions additional to the language core. Web browsers give a means to control web pages. Node.JS provides server-side features, and so on. +A host environment provides its own objects and functions in addition to the language core. Web browsers give a means to control web pages. Node.js provides server-side features, and so on. -Here's a bird's-eye view of what we have when JavaScript runs in a web-browser: +Here's a bird's-eye view of what we have when JavaScript runs in a web browser: -![](windowObjects.png) +![](windowObjects.svg) There's a "root" object called `window`. It has two roles: 1. First, it is a global object for JavaScript code, as described in the chapter . 2. Second, it represents the "browser window" and provides methods to control it. -For instance, here we use it as a global object: +For instance, we can use it as a global object: -```js run +```js run global function sayHi() { alert("Hello"); } -// global functions are accessible as properties of window +// global functions are methods of the global object: window.sayHi(); ``` -And here we use it as a browser window, to see the window height: +And we can use it as a browser window, to show the window height: ```js run alert(window.innerHeight); // inner window height ``` -There are more window-specific methods and properties, we'll cover them later. +There are more window-specific methods and properties, which we'll cover later. + +## DOM (Document Object Model) -## Document Object Model (DOM) +The Document Object Model, or DOM for short, represents all page content as objects that can be modified. -The `document` object gives access to the page content. We can change or create anything on the page using it. +The `document` object is the main "entry point" to the page. We can change or create anything on the page using it. For instance: ```js run @@ -47,58 +49,44 @@ document.body.style.background = "red"; setTimeout(() => document.body.style.background = "", 1000); ``` -Here we used `document.body.style`, but there's much, much more. Properties and methods are described in the specification. There happen to be two working groups who develop it: - -1. [W3C](https://en.wikipedia.org/wiki/World_Wide_Web_Consortium) -- the documentation is at . -2. [WhatWG](https://en.wikipedia.org/wiki/WHATWG), publishing at . - -As it happens, the two groups don't always agree, so it's like we have two sets of standards. But they are very similar and eventually things merge. The documentation that you can find on the given resources is very similar, with about a 99% match. There are very minor differences that you probably won't notice. - -Personally, I find more pleasant to use. - -In the ancient past, there was no standard at all -- each browser implemented however it wanted. Different browsers had different sets, methods, and properties for the same thing, and developers had to write different code for each of them. Dark, messy times. - -Even now we can sometimes meet old code that uses browser-specific properties and works around incompatibilities. But, in this tutorial we'll use modern stuff: there's no need to learn old things until you really need to (chances are high that you won't). - -Then the DOM standard appeared, in an attempt to bring everyone to an agreement. The first version was "DOM Level 1", then it was extended by DOM Level 2, then DOM Level 3, and now it's reached DOM Level 4. People from WhatWG group got tired of version numbers and are calling it just "DOM", without a number. So we'll do the same. +Here, we used `document.body.style`, but there's much, much more. Properties and methods are described in the specification: [DOM Living Standard](https://dom.spec.whatwg.org). ```smart header="DOM is not only for browsers" -The DOM specification explains the structure of a document and provides objects to manipulate it. There are non-browser instruments that use it too. +The DOM specification explains the structure of a document and provides objects to manipulate it. There are non-browser instruments that use DOM too. -For instance, server-side tools that download HTML pages and process them use the DOM. They may support only a part of the specification though. +For instance, server-side scripts that download HTML pages and process them can also use the DOM. They may support only a part of the specification though. ``` ```smart header="CSSOM for styling" -CSS rules and stylesheets are not structured like HTML. There's a separate specification [CSSOM](https://www.w3.org/TR/cssom-1/) that explains how they are represented as objects, and how to read and write them. +There's also a separate specification, [CSS Object Model (CSSOM)](https://www.w3.org/TR/cssom-1/) for CSS rules and stylesheets, that explains how they are represented as objects, and how to read and write them. -CSSOM is used together with DOM when we modify style rules for the document. In practice though, CSSOM is rarely required, because usually CSS rules are static. We rarely need to add/remove CSS rules from JavaScript, so we won't cover it right now. +The CSSOM is used together with the DOM when we modify style rules for the document. In practice though, the CSSOM is rarely required, because we rarely need to modify CSS rules from JavaScript (usually we just add/remove CSS classes, not modify their CSS rules), but that's also possible. ``` -## BOM (part of HTML spec) +## BOM (Browser Object Model) -Browser Object Model (BOM) are additional objects provided by the browser (host environment) to work with everything except the document. +The Browser Object Model (BOM) represents additional objects provided by the browser (host environment) for working with everything except the document. For instance: -- The [navigator](mdn:api/Window/navigator) object provides background information about the browser and the operating system. There are many properties, but the two most widely known are: `navigator.userAgent` -- about the current browser, and `navigator.platform` -- about the platform (can help to differ between Windows/Linux/Mac etc). +- The [navigator](mdn:api/Window/navigator) object provides background information about the browser and the operating system. There are many properties, but the two most widely known are: `navigator.userAgent` -- about the current browser, and `navigator.platform` -- about the platform (can help to differentiate between Windows/Linux/Mac etc). - The [location](mdn:api/Window/location) object allows us to read the current URL and can redirect the browser to a new one. Here's how we can use the `location` object: ```js run alert(location.href); // shows current URL -if (confirm("Go to wikipedia?")) { +if (confirm("Go to Wikipedia?")) { location.href = "https://wikipedia.org"; // redirect the browser to another URL } ``` -Functions `alert/confirm/prompt` are also a part of BOM: they are directly not related to the document, but represent pure browser methods of communicating with the user. - +The functions `alert/confirm/prompt` are also a part of the BOM: they are not directly related to the document, but represent pure browser methods for communicating with the user. -```smart header="HTML specification" -BOM is the part of the general [HTML specification](https://html.spec.whatwg.org). +```smart header="Specifications" +The BOM is a part of the general [HTML specification](https://html.spec.whatwg.org). -Yes, you heard that right. The HTML spec at is not only about the "HTML language" (tags, attributes), but also covers a bunch of objects, methods and browser-specific DOM extensions. That's "HTML in broad terms". +Yes, you heard that right. The HTML spec at is not only about the "HTML language" (tags, attributes), but also covers a bunch of objects, methods, and browser-specific DOM extensions. That's "HTML in broad terms". Also, some parts have additional specs listed at . ``` ## Summary @@ -106,16 +94,20 @@ Yes, you heard that right. The HTML spec at is no Talking about standards, we have: DOM specification -: Describes the document structure, manipulations and events, see . +: Describes the document structure, manipulations, and events, see . CSSOM specification -: Describes stylesheets and style rules, manipulations with them and their binding to documents, see . +: Describes stylesheets and style rules, manipulations with them, and their binding to documents, see . HTML specification : Describes the HTML language (e.g. tags) and also the BOM (browser object model) -- various browser functions: `setTimeout`, `alert`, `location` and so on, see . It takes the DOM specification and extends it with many additional properties and methods. -Now we'll get down to learning DOM, because the document plays the central role in the UI. +Additionally, some classes are described separately at . + +Please note these links, as there's so much to learn that it's impossible to cover everything and remember it all. + +When you'd like to read about a property or a method, the Mozilla manual at is also a nice resource, but the corresponding spec may be better: it's more complex and longer to read, but will make your fundamental knowledge sound and complete. -Please note the links above, as there's so much stuff to learn it's impossible to cover and remember everything. +To find something, it's often convenient to use an internet search "WHATWG [term]" or "MDN [term]", e.g , . -When you'd like to read about a property or a method, the Mozilla manual at is a nice resource, but reading the corresponding spec may be better: it's more complex and longer to read, but will make your fundamental knowledge sound and complete. +Now, we'll get down to learning the DOM, because the document plays the central role in the UI. diff --git a/2-ui/1-document/01-browser-environment/windowObjects.png b/2-ui/1-document/01-browser-environment/windowObjects.png deleted file mode 100644 index 81803bd09d..0000000000 Binary files a/2-ui/1-document/01-browser-environment/windowObjects.png and /dev/null differ diff --git a/2-ui/1-document/01-browser-environment/windowObjects.svg b/2-ui/1-document/01-browser-environment/windowObjects.svg new file mode 100644 index 0000000000..b7e18bb347 --- /dev/null +++ b/2-ui/1-document/01-browser-environment/windowObjects.svg @@ -0,0 +1 @@ +windowdocumentObjectnavigatorscreenlocationframeshistoryArrayFunctionXMLHttpRequestBOMJavaScriptDOM \ No newline at end of file diff --git a/2-ui/1-document/01-browser-environment/windowObjects@2x.png b/2-ui/1-document/01-browser-environment/windowObjects@2x.png deleted file mode 100644 index e6dae7c3d4..0000000000 Binary files a/2-ui/1-document/01-browser-environment/windowObjects@2x.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/article.md b/2-ui/1-document/02-dom-nodes/article.md index 04fd7a98e0..f7f2be91dc 100644 --- a/2-ui/1-document/02-dom-nodes/article.md +++ b/2-ui/1-document/02-dom-nodes/article.md @@ -6,26 +6,42 @@ libs: # DOM tree -The backbone of an HTML document are tags. +The backbone of an HTML document is tags. -According to Document Object Model (DOM), every HTML-tag is an object. Nested tags are called "children" of the enclosing one. +According to the Document Object Model (DOM), every HTML tag is an object. Nested tags are "children" of the enclosing one. The text inside a tag is an object as well. -The text inside a tag it is an object as well. +All these objects are accessible using JavaScript, and we can use them to modify the page. -All these objects are accessible using JavaScript. +For example, `document.body` is the object representing the `` tag. -## An example of DOM +Running this code will make the `` red for 3 seconds: -For instance, let's explore the DOM for this document: +```js run +document.body.style.background = 'red'; // make the background red + +setTimeout(() => document.body.style.background = '', 3000); // return back +``` + +Here we used `style.background` to change the background color of `document.body`, but there are many other properties, such as: + +- `innerHTML` -- HTML contents of the node. +- `offsetWidth` -- the node width (in pixels) +- ...and so on. + +Soon we'll learn more ways to manipulate the DOM, but first we need to know about its structure. + +## An example of the DOM + +Let's start with the following simple document: ```html run no-beautify - About elks + About elk - The truth about elks. + The truth about elk. ``` @@ -35,7 +51,7 @@ The DOM represents HTML as a tree structure of tags. Here's how it looks:
@@ -44,56 +60,57 @@ drawHtmlTree(node1, 'div.domtree', 690, 320); On the picture above, you can click on element nodes and their children will open/collapse. ``` -Tags are called *element nodes* (or just elements). Nested tags become children of the enclosing ones. As a result we have a tree of elements: `` is at the root, then `` and `` are its children, etc. +Every tree node is an object. + +Tags are *element nodes* (or just elements) and form the tree structure: `` is at the root, then `` and `` are its children, etc. The text inside elements forms *text nodes*, labelled as `#text`. A text node contains only a string. It may not have children and is always a leaf of the tree. -For instance, the `` tag has the text `"About elks"`. +For instance, the `<title>` tag has the text `"About elk"`. Please note the special characters in text nodes: - a newline: `↵` (in JavaScript known as `\n`) - a space: `␣` -Spaces and newlines -- are totally valid characters, they form text nodes and become a part of the DOM. So, for instance, in the example above the `<head>` tag contains some spaces before `<title>`, and that text becomes a `#text` node (it contains a newline and some spaces only). +Spaces and newlines are totally valid characters, like letters and digits. They form text nodes and become a part of the DOM. So, for instance, in the example above the `<head>` tag contains some spaces before `<title>`, and that text becomes a `#text` node (it contains a newline and some spaces only). There are only two top-level exclusions: -1. Spaces and newlines before `<head>` are ignored for historical reasons, -2. If we put something after `</body>`, then that is automatically moved inside the `body`, at the end, as the HTML spec requires that all content must be inside `<body>`. So there may be no spaces after `</body>`. +1. Spaces and newlines before `<head>` are ignored for historical reasons. +2. If we put something after `</body>`, then that is automatically moved inside the `body`, at the end, as the HTML spec requires that all content must be inside `<body>`. So there can't be any spaces after `</body>`. -In other cases everything's straightforward -- if there are spaces (just like any character) in the document, then they become text nodes in DOM, and if we remove them, then there won't be any. +In other cases everything's straightforward -- if there are spaces (just like any character) in the document, then they become text nodes in the DOM, and if we remove them, then there won't be any. Here are no space-only text nodes: ```html no-beautify <!DOCTYPE HTML> -<html><head><title>About elksThe truth about elks. +About elkThe truth about elk. ```
-```smart header="Edge spaces and in-between empty text are usually hidden in tools" +```smart header="Spaces at string start/end and space-only text nodes are usually hidden in tools" Browser tools (to be covered soon) that work with DOM usually do not show spaces at the start/end of the text and empty text nodes (line-breaks) between tags. -That's because they are mainly used to decorate HTML, and do not affect how it is shown (in most cases). +Developer tools save screen space this way. -On further DOM pictures we'll sometimes omit them where they are irrelevant, to keep things short. +On further DOM pictures we'll sometimes omit them when they are irrelevant. Such spaces usually do not affect how the document is displayed. ``` - ## Autocorrection -If the browser encounters malformed HTML, it automatically corrects it when making DOM. +If the browser encounters malformed HTML, it automatically corrects it when making the DOM. -For instance, the top tag is always ``. Even if it doesn't exist in the document -- it will exist in the DOM, the browser will create it. The same goes for ``. +For instance, the top tag is always ``. Even if it doesn't exist in the document, it will exist in the DOM, because the browser will create it. The same goes for ``. -As an example, if the HTML file is a single word `"Hello"`, the browser will wrap it into `` and ``, add the required ``, and the DOM will be: +As an example, if the HTML file is the single word `"Hello"`, the browser will wrap it into `` and ``, and add the required ``, and the DOM will be:
@@ -106,7 +123,7 @@ drawHtmlTree(node3, 'div.domtree', 690, 150); While generating the DOM, browsers automatically process errors in the document, close tags and so on. -Such an "invalid" document: +A document with unclosed tags: ```html no-beautify

Hello @@ -115,7 +132,7 @@ Such an "invalid" document:

  • Dad ``` -...Will become a normal DOM, as the browser reads tags and restores the missing parts: +...will become a normal DOM as the browser reads tags and restores the missing parts:
    @@ -126,7 +143,7 @@ drawHtmlTree(node4, 'div.domtree', 690, 360); ````warn header="Tables always have ``" -An interesting "special case" is tables. By the DOM specification they must have ``, but HTML text may (officially) omit it. Then the browser creates `` in DOM automatically. +An interesting "special case" is tables. By DOM specification they must have `` tag, but HTML text may omit it. Then the browser creates `` in the DOM automatically. For the HTML: @@ -143,18 +160,20 @@ let node5 = {"name":"TABLE","nodeType":1,"children":[{"name":"TBODY","nodeType": drawHtmlTree(node5, 'div.domtree', 600, 200); -You see? The `` appeared out of nowhere. You should keep this in mind while working with tables to avoid surprises. +You see? The `` appeared out of nowhere. We should keep this in mind while working with tables to avoid surprises. ```` ## Other node types -Let's add more tags and a comment to the page: +There are some other node types besides elements and text nodes. + +For example, comments: ```html - The truth about elks. + The truth about elk.
    1. An elk is a smart
    2. *!* @@ -169,18 +188,18 @@ Let's add more tags and a comment to the page:
      -Here we see a new tree node type -- *comment node*, labeled as `#comment`. +We can see here a new tree node type -- *comment node*, labeled as `#comment`, between two text nodes. -We may think -- why a comment is added to the DOM? It doesn't affect the visual representation in any way. But there's a rule -- if something's in HTML, then it also must be in the DOM tree. +We may think -- why is a comment added to the DOM? It doesn't affect the visual representation in any way. But there's a rule -- if something's in HTML, then it also must be in the DOM tree. **Everything in HTML, even comments, becomes a part of the DOM.** -Even the `` directive at the very beginning of HTML is also a DOM node. It's in the DOM tree right before ``. We are not going to touch that node, we even don't draw it on diagrams for that reason, but it's there. +Even the `` directive at the very beginning of HTML is also a DOM node. It's in the DOM tree right before ``. Few people know about that. We are not going to touch that node, we even don't draw it on diagrams, but it's there. The `document` object that represents the whole document is, formally, a DOM node as well. @@ -189,31 +208,29 @@ There are [12 node types](https://dom.spec.whatwg.org/#node). In practice we usu 1. `document` -- the "entry point" into DOM. 2. element nodes -- HTML-tags, the tree building blocks. 3. text nodes -- contain text. -4. comments -- sometimes we can put the information there, it won't be shown, but JS can read it from the DOM. +4. comments -- sometimes we can put information there, it won't be shown, but JS can read it from the DOM. ## See it for yourself -To see the DOM structure in real-time, try [Live DOM Viewer](http://software.hixie.ch/utilities/js/live-dom-viewer/). Just type in the document, and it will show up DOM at an instant. - -## In the browser inspector +To see the DOM structure in real-time, try [Live DOM Viewer](https://software.hixie.ch/utilities/js/live-dom-viewer/). Just type in the document, and it will show up as a DOM at an instant. Another way to explore the DOM is to use the browser developer tools. Actually, that's what we use when developing. -To do so, open the web-page [elks.html](elks.html), turn on the browser developer tools and switch to the Elements tab. +To do so, open the web page [elk.html](elk.html), turn on the browser developer tools and switch to the Elements tab. It should look like this: -![](elks.png) +![](elk.svg) You can see the DOM, click on elements, see their details and so on. Please note that the DOM structure in developer tools is simplified. Text nodes are shown just as text. And there are no "blank" (space only) text nodes at all. That's fine, because most of the time we are interested in element nodes. -Clicking the button in the left-upper corner allows to choose a node from the webpage using a mouse (or other pointer devices) and "inspect" it (scroll to it in the Elements tab). This works great when we have a huge HTML page (and corresponding huge DOM) and would like to see the place of a particual element in it. +Clicking the button in the left-upper corner allows us to choose a node from the webpage using a mouse (or other pointer devices) and "inspect" it (scroll to it in the Elements tab). This works great when we have a huge HTML page (and corresponding huge DOM) and would like to see the place of a particular element in it. Another way to do it would be just right-clicking on a webpage and selecting "Inspect" in the context menu. -![](inspect.png) +![](inspect.svg) At the right part of the tools there are the following subtabs: - **Styles** -- we can see CSS applied to the current element rule by rule, including built-in rules (gray). Almost everything can be edited in-place, including the dimensions/margins/paddings of the box below. @@ -225,22 +242,26 @@ The best way to study them is to click around. Most values are editable in-place ## Interaction with console -As we explore the DOM, we also may want to apply JavaScript to it. Like: get a node and run some code to modify it, to see how it looks. Here are few tips to travel between the Elements tab and the console. +As we work the DOM, we also may want to apply JavaScript to it. Like: get a node and run some code to modify it, to see the result. Here are few tips to travel between the Elements tab and the console. + +For the start: -- Select the first `
    3. ` in the Elements tab. -- Press `key:Esc` -- it will open console right below the Elements tab. +1. Select the first `
    4. ` in the Elements tab. +2. Press `key:Esc` -- it will open console right below the Elements tab. Now the last selected element is available as `$0`, the previously selected is `$1` etc. We can run commands on them. For instance, `$0.style.background = 'red'` makes the selected list item red, like this: -![](domconsole0.png) +![](domconsole0.svg) + +That's how to get a node from Elements in Console. -From the other side, if we're in console and have a variable referencing a DOM node, then we can use the command `inspect(node)` to see it in the Elements pane. +There's also a road back. If there's a variable referencing a DOM node, then we can use the command `inspect(node)` in Console to see it in the Elements pane. -Or we can just output it in the console and explore "at-place", like `document.body` below: +Or we can just output the DOM node in the console and explore "in-place", like `document.body` below: -![](domconsole1.png) +![](domconsole1.svg) That's for debugging purposes of course. From the next chapter on we'll access and modify DOM using JavaScript. @@ -258,4 +279,4 @@ We can use developer tools to inspect DOM and modify it manually. Here we covered the basics, the most used and important actions to start with. There's an extensive documentation about Chrome Developer Tools at . The best way to learn the tools is to click here and there, read menus: most options are obvious. Later, when you know them in general, read the docs and pick up the rest. -DOM nodes have properties and methods that allow to travel between them, modify, move around the page and more. We'll get down to them in the next chapters. +DOM nodes have properties and methods that allow us to travel between them, modify them, move around the page, and more. We'll get down to them in the next chapters. diff --git a/2-ui/1-document/02-dom-nodes/domconsole0.png b/2-ui/1-document/02-dom-nodes/domconsole0.png deleted file mode 100644 index 121c11d75a..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/domconsole0.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/domconsole0.svg b/2-ui/1-document/02-dom-nodes/domconsole0.svg new file mode 100644 index 0000000000..eb99f193fe --- /dev/null +++ b/2-ui/1-document/02-dom-nodes/domconsole0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/2-ui/1-document/02-dom-nodes/domconsole0@2x.png b/2-ui/1-document/02-dom-nodes/domconsole0@2x.png deleted file mode 100644 index a8953395c5..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/domconsole0@2x.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/domconsole1.png b/2-ui/1-document/02-dom-nodes/domconsole1.png deleted file mode 100644 index c04f015cf5..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/domconsole1.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/domconsole1.svg b/2-ui/1-document/02-dom-nodes/domconsole1.svg new file mode 100644 index 0000000000..02ef5f0a65 --- /dev/null +++ b/2-ui/1-document/02-dom-nodes/domconsole1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/2-ui/1-document/02-dom-nodes/domconsole1@2x.png b/2-ui/1-document/02-dom-nodes/domconsole1@2x.png deleted file mode 100644 index ce0fa0fffa..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/domconsole1@2x.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/elk.html b/2-ui/1-document/02-dom-nodes/elk.html new file mode 100644 index 0000000000..dc5d65f541 --- /dev/null +++ b/2-ui/1-document/02-dom-nodes/elk.html @@ -0,0 +1,11 @@ + + + + The truth about elk. +
        +
      1. An elk is a smart
      2. + +
      3. ...and cunning animal!
      4. +
      + + diff --git a/2-ui/1-document/02-dom-nodes/elk.svg b/2-ui/1-document/02-dom-nodes/elk.svg new file mode 100644 index 0000000000..448eea9d13 --- /dev/null +++ b/2-ui/1-document/02-dom-nodes/elk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/2-ui/1-document/02-dom-nodes/elks.html b/2-ui/1-document/02-dom-nodes/elks.html deleted file mode 100644 index 7d29f3d4e3..0000000000 --- a/2-ui/1-document/02-dom-nodes/elks.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - The truth about elks. -
        -
      1. An elk is a smart
      2. - -
      3. ...and cunning animal!
      4. -
      - - diff --git a/2-ui/1-document/02-dom-nodes/elks.png b/2-ui/1-document/02-dom-nodes/elks.png deleted file mode 100644 index 03177c40e8..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/elks.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/elks@2x.png b/2-ui/1-document/02-dom-nodes/elks@2x.png deleted file mode 100644 index e8a15bd5b9..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/elks@2x.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/inspect.png b/2-ui/1-document/02-dom-nodes/inspect.png deleted file mode 100644 index 075cf93080..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/inspect.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/inspect.svg b/2-ui/1-document/02-dom-nodes/inspect.svg new file mode 100644 index 0000000000..60696ec0d5 --- /dev/null +++ b/2-ui/1-document/02-dom-nodes/inspect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/2-ui/1-document/02-dom-nodes/inspect@2x.png b/2-ui/1-document/02-dom-nodes/inspect@2x.png deleted file mode 100644 index 8743dd297d..0000000000 Binary files a/2-ui/1-document/02-dom-nodes/inspect@2x.png and /dev/null differ diff --git a/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg b/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg deleted file mode 100644 index fa37b2cc3d..0000000000 --- a/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg +++ /dev/null @@ -1 +0,0 @@ -! \ No newline at end of file diff --git a/2-ui/1-document/03-dom-navigation/1-dom-children/task.md b/2-ui/1-document/03-dom-navigation/1-dom-children/task.md index 4a9e741a9c..d97f2748a7 100644 --- a/2-ui/1-document/03-dom-navigation/1-dom-children/task.md +++ b/2-ui/1-document/03-dom-navigation/1-dom-children/task.md @@ -4,7 +4,7 @@ importance: 5 # DOM children -For the page: +Look at this page: ```html @@ -18,7 +18,7 @@ For the page: ``` -How to access: +For each of the following, give at least one way of how to access them: - The `
      ` DOM node? - The `