diff --git a/.gitignore b/.gitignore index 6f90fd19..1a71fb7c 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 4822fdb4..2f4f518f 100644 --- a/1-js/01-getting-started/1-intro/article.md +++ b/1-js/01-getting-started/1-intro/article.md @@ -1,10 +1,10 @@ # 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 web pages alive"*. +*JavaScript* was initially created to "make web pages alive". 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. @@ -12,7 +12,7 @@ Scripts are provided and executed as plain text. They don't need special prepara In this aspect, JavaScript is very different from another language called [Java](https://en.wikipedia.org/wiki/Java_(programming_language)). -```smart header="Why JavaScript?" +```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. @@ -24,26 +24,26 @@ The browser has an embedded engine sometimes called a "JavaScript virtual machin 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" and "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 at each step 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. When it's done, scripts run 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? -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. 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. @@ -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. 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 agree for data exchange and contain a special JavaScript code that handles it. We'll cover that in the tutorial. + 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. - 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` and steal information from there. + 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.svg) -Such limits do not exist if JavaScript is used outside of the browser, for example on a server. Modern browsers also allow plugin/extensions which may ask for 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? @@ -86,13 +86,13 @@ There are at least *three* great things about JavaScript: ```compare + Full integration with HTML/CSS. + Simple things are done simply. -+ Support by all major browsers and enabled by default. ++ Supported by all major browsers and enabled by default. ``` JavaScript is the only browser technology that combines these three things. That's what makes JavaScript unique. That's why it's the most widespread tool for creating browser interfaces. -That said, JavaScript also allows to create servers, mobile applications, etc. +That said, JavaScript can be used to create servers, mobile applications, etc. ## Languages "over" JavaScript @@ -100,21 +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 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 us to write clearer and more precise code. Usually, Ruby devs like it. -- [TypeScript](http://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](http://flow.org/) also adds data typing, but in a different way. Developed by Facebook. +- [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 transpiled 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 is now used in many other environments as well. -- Today, 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.svg b/1-js/01-getting-started/1-intro/limitations.svg index a7863c63..76ea43fd 100644 --- a/1-js/01-getting-started/1-intro/limitations.svg +++ b/1-js/01-getting-started/1-intro/limitations.svg @@ -1 +1 @@ -https://javascript.info<script> ... </script>https://gmail.comhttps://javascript.info \ No newline at end of file +https://javascript.info<script> ... </script>https://gmail.comhttps://javascript.info \ No newline at end of file diff --git a/1-js/01-getting-started/2-manuals-specifications/article.md b/1-js/01-getting-started/2-manuals-specifications/article.md index 85a7737c..3fa24333 100644 --- a/1-js/01-getting-started/2-manuals-specifications/article.md +++ b/1-js/01-getting-started/2-manuals-specifications/article.md @@ -1,7 +1,7 @@ # 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 sources. +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 @@ -9,24 +9,19 @@ This book is a *tutorial*. It aims to help you gradually learn the language. But 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. In-between these releases, the latest specification draft is at . +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 in developing for the browser, then there are other specs covered in the [second part](info:browser-environment) of the tutorial. +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 a manual with examples and other information. It's great to get in-depth information about individual language functions, methods etc. +- **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. - One can find it at . + 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 `parseInt` function. - - -- **MSDN** – Microsoft manual with a lot of information, including JavaScript (often referred to as JScript). If one needs something specific to Internet Explorer, better go there: . - - Also, we can use an internet search with phrases such as "RegExp MSDN" or "RegExp MSDN jscript". +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 @@ -34,9 +29,9 @@ 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: . +- - 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. +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 index 6532e54a..5a86f2a7 100644 --- a/1-js/01-getting-started/3-code-editors/article.md +++ b/1-js/01-getting-started/3-code-editors/article.md @@ -13,7 +13,7 @@ An IDE loads the project (which can be many files), allows navigation between fi If you haven't selected an IDE yet, consider the following options: - [Visual Studio Code](https://code.visualstudio.com/) (cross-platform, free). -- [WebStorm](http://www.jetbrains.com/webstorm/) (cross-platform, paid). +- [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/). @@ -31,7 +31,6 @@ In practice, lightweight editors may have a lot of plugins including directory-l The following options deserve your attention: -- [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. diff --git a/1-js/01-getting-started/4-devtools/article.md b/1-js/01-getting-started/4-devtools/article.md index c84d9270..50926d4f 100644 --- a/1-js/01-getting-started/4-devtools/article.md +++ b/1-js/01-getting-started/4-devtools/article.md @@ -29,10 +29,15 @@ The exact look of developer tools depends on your version of Chrome. It changes - 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 (`key:Shift+Enter` to input multi-line commands). +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 @@ -50,12 +55,6 @@ Open Preferences and go to the "Advanced" pane. There's a checkbox at the bottom 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. -```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. -``` - ## Summary - Developer tools allow us to see errors, run commands, examine variables, and much more. 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 00000000..ff1d871b --- /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 e69de29b..81552913 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 489c01ad..35f82bf5 100644 --- a/1-js/02-first-steps/01-hello-world/article.md +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -9,7 +9,7 @@ So first, let's see how we attach a script to a webpage. For server-side environ ## The "script" tag -JavaScript programs can be inserted into any part of an HTML document with the help of the ` ``` - This trick isn't used in modern JavaScript. These comments hid JavaScript code from old browsers that didn't know how to process the ` ``` -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"` 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: ```html - + ``` To attach several scripts, use multiple tags: diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md index b18aab19..e81fd343 100644 --- a/1-js/02-first-steps/02-structure/article.md +++ b/1-js/02-first-steps/02-structure/article.md @@ -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", so the semicolon is not 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,45 +56,41 @@ 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, just remember the result of the code: 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 the code, only the first `alert` is shown and then we have an error! +The difference compared to the code above is only one character: the semicolon at the end of the first line is gone. -But everything is fine again if we add a semicolon after `alert`: -```js run -alert("All fine now"); +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. -[1, 2].forEach(alert) -``` +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. -Now we have the "All fine now" message followed by `1` and `2`. - - -The error in the no-semicolon variant occurs because JavaScript does not assume 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. Here'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 one. Such a merging in this case is just wrong, hence the error. This can happen in other situations. +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. ```` 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, programs become more and more complex. It becomes necessary to add *comments* which describe what the code does and why. @@ -136,7 +132,7 @@ alert('World'); ``` ```smart header="Use hotkeys!" -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`. +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!" 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 573d76bc..9586733c 100644 --- a/1-js/02-first-steps/03-strict-mode/article.md +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -19,8 +19,7 @@ For example: ... ``` -We will learn functions (a way to group commands) soon. Looking ahead, let's note that `"use strict"` can be put at the beginning of the function body instead of the whole script. Doing that enables strict mode 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 at the top of your scripts, otherwise strict mode may not be enabled. @@ -42,16 +41,18 @@ Only comments may appear above `"use strict"`. ```warn header="There's no way to cancel `use strict`" There is no directive like `"no use strict"` that reverts the engine to old behavior. -Once we enter strict mode, there's no return. +Once we enter strict mode, there's no going back. ``` ## Browser console -For the future, when you use a browser console to test features, please note that it doesn't `use strict` by default. +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. -You can try to press `key:Shift+Enter` to input multiple lines, and put `use strict` on top, like this: +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'; @@ -61,25 +62,28 @@ You can try to press `key:Shift+Enter` to input multiple lines, and put `use str It works in most browsers, namely Firefox and Chrome. -If it doesn't, the most reliable way to ensure `use strict` would be to input the code into console like this: +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... + // ...your code here... })() ``` -## Always "use strict" +## Should we "use strict"? -We have yet to cover the differences between strict mode and the "default" mode. +The question may sound obvious, but it's not so. -In the next chapters, as we learn language features, we'll note the differences between the strict and default modes. Luckily, there aren't many and they actually make our lives better. +One could recommend to start scripts with `"use strict"`... But you know what's cool? -For now, it's enough to know about it in general: +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. -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 later in the tutorial. -2. Strict mode is enabled by placing `"use strict"` at the top of a script or function. Several language features, like "classes" and "modules", enable strict mode automatically. -3. Strict mode is supported by all modern browsers. -4. We recommended always starting scripts with `"use strict"`. All examples in this tutorial assume strict mode unless (very rarely) specified otherwise. +**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.** + +As of now, we've got to know about `use strict` 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. + +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 d56e54d2..392f4e26 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 @@ -6,7 +6,7 @@ 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. ## The name of the current visitor 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 5fd18f90..f3c208a7 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 80a36d9c..4c2d09de 100644 --- a/1-js/02-first-steps/04-variables/article.md +++ b/1-js/02-first-steps/04-variables/article.md @@ -24,7 +24,7 @@ Now, we can put some data into it by using the assignment operator `=`: let message; *!* -message = 'Hello'; // store the string +message = 'Hello'; // store the string 'Hello' in the variable named message */!* ``` @@ -64,6 +64,7 @@ let message = 'Hello'; ``` Some people also define multiple variables in this multiline style: + ```js no-beautify let user = 'John', age = 25, @@ -80,7 +81,6 @@ let user = 'John' 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`: @@ -104,6 +104,7 @@ For instance, the variable `message` can be imagined as a box labeled `"message" We can put any value in the box. We can also change it as many times as we want: + ```js run let message; @@ -135,6 +136,20 @@ 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's interesting to note that there exist [functional](https://en.wikipedia.org/wiki/Functional_programming) programming languages, like [Scala](http://www.scala-lang.org/) or [Erlang](http://www.erlang.org/) that forbid changing variable values. @@ -179,7 +194,7 @@ 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-Latin letters are allowed, but not recommended" @@ -190,7 +205,7 @@ 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 some time. ```` ````warn header="Reserved names" @@ -237,7 +252,7 @@ To declare a constant (unchanging) variable, use `const` instead of `let`: const myBirthday = '18.04.1982'; ``` -Variables declared using `const` are called "constants". They cannot be changed. An attempt to do so 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,7 +262,6 @@ myBirthday = '01.01.2001'; // error, can't reassign the constant! When a programmer is sure that a variable will never change, they can declare it with `const` to guarantee and clearly 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. @@ -278,19 +292,20 @@ When should we use capitals for a constant and when should we name it normally? Being a "constant" just means that a variable's value never changes. But there are constants that are known prior to execution (like a hexadecimal value for red) and there are constants that 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. -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. -A variable name should have a clean, obvious meaning, describe the data that it stores. +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 was written by a beginner versus an experienced developer. diff --git a/1-js/02-first-steps/04-variables/variable-change.svg b/1-js/02-first-steps/04-variables/variable-change.svg index 427a6388..1b267923 100644 --- a/1-js/02-first-steps/04-variables/variable-change.svg +++ b/1-js/02-first-steps/04-variables/variable-change.svg @@ -1 +1 @@ -"World!""Hello!"message \ No newline at end of file +"World!""Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/variable.svg b/1-js/02-first-steps/04-variables/variable.svg index 5d15c9e4..1c3d8b0c 100644 --- a/1-js/02-first-steps/04-variables/variable.svg +++ b/1-js/02-first-steps/04-variables/variable.svg @@ -1 +1 @@ -"Hello!"message \ No newline at end of file +"Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md index 0da61737..a697548a 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 at another be a number: +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,11 +12,9 @@ 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 cover them in general and in the next chapters we'll talk about each of them in detail. - -## A number +## Number ```js let n = 123; @@ -44,13 +46,15 @@ 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` returns `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 a `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. @@ -62,14 +66,49 @@ Special numeric values formally belong to the "number" type. Of course they are 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. + +`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. + + +```smart header="Compatibility issues" +Right now, `BigInt` is supported in Firefox/Chrome/Edge/Safari, but not in IE. +``` + +You can check [*MDN* BigInt compatibility table](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#Browser_compatibility) to know which versions of a browser are supported. + +## 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 +117,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: @@ -102,12 +141,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`. @@ -144,7 +183,7 @@ In JavaScript, `null` is not a "reference to a non-existing object" or a "null p It's just a special value which represents "nothing", "empty" or "value unknown". -The code above states that `age` is unknown or empty for some reason. +The code above states that `age` is unknown. ## The "undefined" value @@ -155,49 +194,47 @@ The meaning of `undefined` is "value is not assigned". 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 we don't recommend doing that. Normally, we use `null` to assign an "empty" or "unknown" value to a variable, and we use `undefined` for checks like seeing if a variable has been assigned. +...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 learn more about primitives. +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 `symbol` type is used to create unique identifiers for objects. We mention it here for completeness, but we'll study it after objects. +Being that important, objects deserve a special treatment. We'll deal with them later in the chapter , after we learn more about primitives. + +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. ## The typeof operator [#type-typeof] 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 do a quick check. -It supports two forms of syntax: - -1. As an operator: `typeof x`. -2. As a function: `typeof(x)`. - -In other words, it works with parentheses or without them. The result is the same. - -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" @@ -220,25 +257,37 @@ typeof alert // "function" (3) 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 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, this is an error in the language. -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's not quite correct, but very convenient in practice. +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. +```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 data 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 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"` -- this is an error in the language, it's not actually an object. diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/article.md b/1-js/02-first-steps/06-alert-prompt-confirm/article.md similarity index 79% rename from 1-js/02-first-steps/09-alert-prompt-confirm/article.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/article.md index 8ba414e9..ef0f333c 100644 --- a/1-js/02-first-steps/09-alert-prompt-confirm/article.md +++ b/1-js/02-first-steps/06-alert-prompt-confirm/article.md @@ -1,18 +1,10 @@ # Interaction: alert, prompt, confirm -In this part of the tutorial we cover JavaScript language "as is", without environment-specific tweaks. - -But we'll still be using the browser as our demo environment, so we should know at least a few of its user-interface functions. In this chapter, we'll get familiar with the browser functions `alert`, `prompt` and `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 -Syntax: - -```js -alert(message); -``` - -This shows a message and pauses script execution until the user presses "OK". +This one we've seen already. It shows a message and waits for the user to press "OK". For example: @@ -20,7 +12,7 @@ For example: 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". +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 @@ -38,7 +30,11 @@ It shows a modal window with a text message, an input field for the visitor, and `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 Cancel or hitting the `key:Esc` key. +```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. diff --git a/1-js/02-first-steps/06-type-conversions/article.md b/1-js/02-first-steps/07-type-conversions/article.md similarity index 79% rename from 1-js/02-first-steps/06-type-conversions/article.md rename to 1-js/02-first-steps/07-type-conversions/article.md index 20d093ea..ea8e0f81 100644 --- a/1-js/02-first-steps/06-type-conversions/article.md +++ b/1-js/02-first-steps/07-type-conversions/article.md @@ -1,13 +1,15 @@ # Type Conversions -Most of the time, operators and functions automatically convert the values given to them to the right type. +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. Instead, we'll study primitives first. Later, after we learn about objects, we'll see how object conversion works in the chapter . +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 @@ -68,7 +70,7 @@ Numeric conversion rules: |`undefined`|`NaN`| |`null`|`0`| |true and false | `1` and `0` | -| `string` | Whitespaces 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`. | +| `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: @@ -81,18 +83,7 @@ alert( Number(false) ); // 0 Please note that `null` and `undefined` behave differently here: `null` becomes zero while `undefined` becomes `NaN`. -````smart header="Addition '+' concatenates strings" -Almost all mathematical operations convert values to numbers. A notable exception is addition `+`. If one of the added values is a string, the other 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) -``` - -This only happens when at least one of the arguments is a string. Otherwise, values are converted to numbers. -```` +Most mathematical operators also perform such conversion, we'll see that in the next chapter. ## Boolean Conversion @@ -139,7 +130,7 @@ The conversion follows the rules: |`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`. | +| `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)`. 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/06-type-conversions/1-primitive-conversions-questions/solution.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md similarity index 70% rename from 1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md rename to 1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md index 4964a623..7370b66a 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md @@ -9,9 +9,8 @@ true + false = 1 "$" + 4 + 5 = "$45" "4" - 2 = 2 "4px" - 2 = NaN -7 / 0 = Infinity -" -9 " + 5 = " -9 5" // (3) -" -9 " - 5 = -14 // (4) +" -9 " + 5 = " -9 5" // (3) +" -9 " - 5 = -14 // (4) null + 1 = 1 // (5) undefined + 1 = NaN // (6) " \t \n" - 2 = -2 // (7) @@ -23,4 +22,4 @@ undefined + 1 = NaN // (6) 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`. +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/06-type-conversions/1-primitive-conversions-questions/task.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md similarity index 98% rename from 1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md rename to 1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md index 930c7151..068420c7 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md @@ -16,7 +16,6 @@ true + false "$" + 4 + 5 "4" - 2 "4px" - 2 -7 / 0 " -9 " + 5 " -9 " - 5 null + 1 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 00000000..209a0702 --- /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 00000000..b3ea4a3a --- /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/07-operators/article.md b/1-js/02-first-steps/08-operators/article.md similarity index 74% rename from 1-js/02-first-steps/07-operators/article.md rename to 1-js/02-first-steps/08-operators/article.md index a1373ead..882b6cdb 100644 --- a/1-js/02-first-steps/07-operators/article.md +++ b/1-js/02-first-steps/08-operators/article.md @@ -1,8 +1,8 @@ -# Operators +# Basic operators, maths We know many operators from school. They are things like addition `+`, multiplication `*`, subtraction `-`, and so on. -In this chapter, we'll concentrate on aspects of operators that are not covered by school arithmetic. +In this chapter, we’ll start with simple operators, then concentrate on JavaScript-specific aspects, not covered by school arithmetic. ## Terms: "unary", "binary", "operand" @@ -28,9 +28,59 @@ Before we move on, let's grasp some common terminology. 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. -## String concatenation, binary + +## Maths -Now, let's see special features of JavaScript operators that are beyond school arithmetics. +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, a remainder of 5 divided by 2 +alert( 8 % 3 ); // 2, a remainder of 8 divided by 3 +``` + +### 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 features of JavaScript operators that are beyond school arithmetics. Usually, the plus operator `+` sums numbers. @@ -41,7 +91,7 @@ let s = "my" + "string"; alert(s); // mystring ``` -Note that if one of the operands is a string, the other one is converted to a string too. +Note that if any of the operands is a string, then the other one is converted to a string too. For example: @@ -50,22 +100,28 @@ 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, the other one is converted 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: +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" ``` -String concatenation and conversion is a special feature of the binary plus `+`. Other arithmetic operators work only with numbers and always convert their operands to numbers. - -For instance, subtraction and division: +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( 2 - '1' ); // 1 -alert( '6' / '2' ); // 3 +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 + @@ -133,26 +189,27 @@ Parentheses override any precedence, so if we're not satisfied with the default 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/JavaScript/Reference/operators/operator_precedence) (you don't need to remember this, but note that unary operators are higher than corresponding binary ones): +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 | |------------|------|------| | ... | ... | ... | -| 16 | unary plus | `+` | -| 16 | unary negation | `-` | -| 14 | multiplication | `*` | -| 14 | division | `/` | -| 13 | addition | `+` | -| 13 | subtraction | `-` | +| 14 | unary plus | `+` | +| 14 | unary negation | `-` | +| 13 | exponentiation | `**` | +| 12 | multiplication | `*` | +| 12 | division | `/` | +| 11 | addition | `+` | +| 11 | subtraction | `-` | | ... | ... | ... | -| 3 | assignment | `=` | +| 2 | assignment | `=` | | ... | ... | ... | -As we can see, the "unary plus" has a priority of `16` which is higher than the `13` of "addition" (binary plus). That's why, in the expression `"+apples + +oranges"`, unary pluses work before the addition. +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 `3`. +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`. @@ -162,24 +219,11 @@ let x = 2 * 2 + 1; alert( x ); // 5 ``` -It is possible to chain assignments: +### Assignment = returns a value -```js run -let a, b, c; +The fact of `=` being an operator, not a "magical" language construct has an interesting implication. -*!* -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. - -````smart header="The assignment operator `\"=\"` returns a value" -An operator always returns a value. That's obvious for most of them like addition `+` or multiplication `*`. But the assignment operator follows this rule too. +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*. @@ -199,49 +243,74 @@ 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, but shouldn't write anything like that ourselves. Such tricks definitely don't make code clearer or readable. -```` +Funny code, isn't it? We should understand how it works, because sometimes we see it in JavaScript libraries. -## Remainder % +Although, please don't write the code like that. Such tricks definitely don't make code clearer or readable. -The remainder operator `%`, despite its appearance, is not related to percents. +### Chaining assignments -The result of `a % b` is the remainder of the integer division of `a` by `b`. - -For instance: +Another interesting feature is the ability to chain assignments: ```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 +let a, b, c; + +*!* +a = b = c = 2 + 2; +*/!* + +alert( a ); // 4 +alert( b ); // 4 +alert( c ); // 4 ``` -## Exponentiation ** +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. -The exponentiation operator `**` is a recent addition to the language. +Once again, for the purposes of readability it's better to split such code into few lines: -For a natural number `b`, the result of `a ** b` is `a` multiplied by itself `b` times. +```js +c = 2 + 2; +b = c; +a = c; +``` +That's easier to read, especially when eye-scanning the code fast. -For instance: +## Modify-in-place -```js run -alert( 2 ** 2 ); // 4 (2 * 2) -alert( 2 ** 3 ); // 8 (2 * 2 * 2) -alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2) +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; ``` -The operator works for non-integer numbers as well. - -For instance: +This notation can be shortened using the operators `+=` and `*=`: ```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) +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. @@ -368,41 +437,7 @@ The list of operators: - RIGHT SHIFT ( `>>` ) - ZERO-FILL RIGHT SHIFT ( `>>>` ) -These operators are used very rarely. To understand them, we need to delve into low-level number representation and it would not be optimal to do that right now, especially since 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 on 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 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; - -alert( n ); // 16 (right part evaluated first, same as n *= 8) -``` +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 diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md similarity index 75% rename from 1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md rename to 1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md index 6437b512..632b1cf4 100644 --- a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md +++ b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md @@ -13,8 +13,8 @@ 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"`. +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`. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/task.md similarity index 100% rename from 1-js/02-first-steps/08-comparison/1-comparison-questions/task.md rename to 1-js/02-first-steps/09-comparison/1-comparison-questions/task.md diff --git a/1-js/02-first-steps/08-comparison/article.md b/1-js/02-first-steps/09-comparison/article.md similarity index 85% rename from 1-js/02-first-steps/08-comparison/article.md rename to 1-js/02-first-steps/09-comparison/article.md index d889b132..a69317fe 100644 --- a/1-js/02-first-steps/08-comparison/article.md +++ b/1-js/02-first-steps/09-comparison/article.md @@ -1,15 +1,21 @@ # Comparisons -We know many comparison operators from maths: +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 equals sign `=`. A single symbol `a = b` would mean an assignment). -- Not equals. In maths the notation is , but in JavaScript it's written as an assignment with an exclamation sign before it: 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 -Like all other operators, a comparison returns a value. In this case, the value is a boolean. +All comparison operators return a boolean value: - `true` -- means "yes", "correct" or "the truth". - `false` -- means "no", "wrong" or "not the truth". @@ -51,7 +57,9 @@ The algorithm to compare two strings is simple: 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 examples above, the comparison `'Z' > 'A'` gets to a result at the first step while the strings `"Glow"` and `"Glee"` are compared character-by-character: +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`. @@ -192,13 +200,12 @@ 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. -### Evade problems +### 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 evade problems with them: +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: -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're really sure of what you're doing. If a variable can have these values, check for them separately. +- 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 diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg index ea4d122f..47b020aa 100644 --- a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg @@ -1 +1 @@ -BeginYou don't know? “ECMAScript”!Right!What's the “official” name of JavaScript?OtherECMAScript \ No newline at end of file +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/task.md b/1-js/02-first-steps/10-ifelse/2-check-standard/task.md index a4d94324..4305584f 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,7 +6,7 @@ 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.svg) diff --git a/1-js/02-first-steps/10-ifelse/article.md b/1-js/02-first-steps/10-ifelse/article.md index 30287ccb..51514062 100644 --- a/1-js/02-first-steps/10-ifelse/article.md +++ b/1-js/02-first-steps/10-ifelse/article.md @@ -1,4 +1,4 @@ -# Conditional operators: if, '?' +# Conditional branching: if, '?' Sometimes, we need to perform different actions based on different conditions. @@ -68,7 +68,7 @@ if (cond) { ## The "else" clause -The `if` statement may contain an optional "else" block. It executes when the condition is false. +The `if` statement may contain an optional "else" block. It executes when the condition is falsy. For example: ```js run 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 8f4d664e..f85b5636 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/3-alert-1-null-2/solution.md b/1-js/02-first-steps/11-logical-operators/3-alert-1-null-2/solution.md index 5c2455ef..368b5940 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/6-check-if-in-range/task.md b/1-js/02-first-steps/11-logical-operators/6-check-if-in-range/task.md index cc00ca9f..fc9e336c 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 7c22d6ad..9b947d00 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/9-check-login/ifelse_task.svg b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg index 010a53a4..d22b518a 100644 --- 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 @@ -1 +1 @@ -BeginCanceledCanceledWelcome!I don't know youWrong passwordWho's there?Password?CancelCancelAdminTheMasterOtherOther \ No newline at end of file +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 index a30db7aa..60460625 100644 --- 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 @@ -3,19 +3,19 @@ ```js run demo let userName = prompt("Who's there?", ''); -if (userName == 'Admin') { +if (userName === 'Admin') { let pass = prompt('Password?', ''); - if (pass == 'TheMaster') { + if (pass === 'TheMaster') { alert( 'Welcome!' ); - } else if (pass == '' || pass == null) { + } else if (pass === '' || pass === null) { alert( 'Canceled' ); } else { alert( 'Wrong password' ); } -} else if (userName == '' || userName == null) { +} else if (userName === '' || userName === null) { alert( 'Canceled' ); } else { alert( "I don't know you" ); 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 25f8ff7f..78c4fd2f 100644 --- a/1-js/02-first-steps/11-logical-operators/article.md +++ b/1-js/02-first-steps/11-logical-operators/article.md @@ -1,6 +1,6 @@ # 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. Their result can also be of any type. @@ -64,7 +64,7 @@ if (hour < 10 || hour > 18 || isWeekend) { } ``` -## OR "||" finds 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. @@ -84,16 +84,16 @@ The OR `||` operator does the following: 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 truthy 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) ``` @@ -101,53 +101,40 @@ This leads to some interesting usage compared to a "pure, classical, boolean-onl 1. **Getting the first truthy value from a list of variables or expressions.** - Imagine we have a list of variables which can either contain data or be `null/undefined`. How can we find 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 `||`: + 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, `"unnamed"` would be the result. + If all variables were falsy, `"Anonymous"` would show up. + 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. This process is called "a short-circuit evaluation" because it goes as short as possible from left to right. + Another feature of OR `||` operator is the so-called "short-circuit" evaluation. - This is clearly seen when the expression given as the second argument has a side effect like a variable assignment. + 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. - In the example below, `x` does not get assigned: + 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. + + In the example below, only the second message is printed: ```js run no-beautify - let x; - - *!*true*/!* || (x = 1); - - alert(x); // undefined, because (x = 1) not evaluated + *!*true*/!* || alert("not printed"); + *!*false*/!* || alert("printed"); ``` - If, instead, the first argument is `false`, `||` evaluates the second one, thus running the assignment: + In the first line, the OR `||` operator stops the evaluation immediately upon seeing `true`, so the `alert` isn't run. - ```js run no-beautify - let x; - - *!*false*/!* || (x = 1); - - alert(x); // 1 - ``` - - An assignment is a simple case. There may be side effects, that won't show up if the evaluation doesn't reach them. - - As we can see, such a use case is a "shorter way of doing `if`". The first operand is converted to boolean. If it's false, 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 this can be handy. + Sometimes, people use this feature to execute commands only if the condition on the left part is falsy. ## && (AND) @@ -236,7 +223,8 @@ The precedence of AND `&&` operator is higher than OR `||`. 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: @@ -253,14 +241,12 @@ 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 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 we recommend using every construct for its purpose: use `if` if we want if and use `&&` if we want AND. ## ! (NOT) 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 00000000..ec0ffe78 --- /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 not defined) +``` + +Here's the example with `user` assigned to a name: + +```js run +let user = "John"; + +alert(user ?? "Anonymous"); // John (user defined) +``` + +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 exists 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/1-loop-last-value/solution.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/task.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/task.md diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/2-which-value-while/solution.md rename to 1-js/02-first-steps/13-while-for/2-which-value-while/solution.md diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/task.md b/1-js/02-first-steps/13-while-for/2-which-value-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/2-which-value-while/task.md rename to 1-js/02-first-steps/13-while-for/2-which-value-while/task.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md b/1-js/02-first-steps/13-while-for/3-which-value-for/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/solution.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/solution.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/task.md b/1-js/02-first-steps/13-while-for/3-which-value-for/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/task.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/task.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/solution.md b/1-js/02-first-steps/13-while-for/4-for-even/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/solution.md rename to 1-js/02-first-steps/13-while-for/4-for-even/solution.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/task.md b/1-js/02-first-steps/13-while-for/4-for-even/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/task.md rename to 1-js/02-first-steps/13-while-for/4-for-even/task.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/task.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/task.md diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md similarity index 80% rename from 1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md rename to 1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md index 2e04a78c..c7de5f09 100644 --- a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md +++ b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md @@ -10,6 +10,6 @@ do { 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. +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/12-while-for/7-list-primes/solution.md b/1-js/02-first-steps/13-while-for/7-list-primes/solution.md similarity index 53% rename from 1-js/02-first-steps/12-while-for/7-list-primes/solution.md rename to 1-js/02-first-steps/13-while-for/7-list-primes/solution.md index 9ff0663d..b4b64b6f 100644 --- a/1-js/02-first-steps/12-while-for/7-list-primes/solution.md +++ b/1-js/02-first-steps/13-while-for/7-list-primes/solution.md @@ -26,4 +26,4 @@ for (let i = 2; i <= n; i++) { // for each i... } ``` -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 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. +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/12-while-for/article.md b/1-js/02-first-steps/13-while-for/article.md similarity index 89% rename from 1-js/02-first-steps/12-while-for/article.md rename to 1-js/02-first-steps/13-while-for/article.md index 580ac3e1..d1b74988 100644 --- a/1-js/02-first-steps/12-while-for/article.md +++ b/1-js/02-first-steps/13-while-for/article.md @@ -6,6 +6,19 @@ For example, outputting goods from a list one after another or just running the *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: @@ -106,7 +119,7 @@ Let's examine the `for` statement part-by-part: | part | | | |-------|----------|----------------------------------------------------------------------------| -| begin | `i = 0` | Executes once upon entering the loop. | +| 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. | @@ -162,10 +175,8 @@ for (i = 0; i < 3; i++) { // use an existing variable alert(i); // 3, visible, because declared outside of the loop ``` - ```` - ### Skipping parts Any part of `for` can be skipped. @@ -212,7 +223,7 @@ 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 +```js run let sum = 0; while (true) { @@ -256,7 +267,7 @@ For even values of `i`, the `continue` directive stops executing the body and pa ````smart header="The `continue` directive helps decrease nesting" A loop that shows odd values could look like this: -```js +```js run for (let i = 0; i < 10; i++) { if (i % 2) { @@ -268,7 +279,7 @@ for (let i = 0; i < 10; 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. +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 '?'" @@ -286,7 +297,6 @@ if (i > 5) { ...and rewrite it using a question mark: - ```js no-beautify (i > 5) ? alert(i) : *!*continue*/!*; // continue isn't allowed here ``` @@ -318,9 +328,10 @@ 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! +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 (...) { ... @@ -342,6 +353,7 @@ The `break ` statement in the loop below breaks out to the label: // do something with the value... } } + alert('Done!'); ``` @@ -362,13 +374,26 @@ The `continue` directive can also be used with a label. In this case, code execu 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; // doesn't jumps to the label below +break label; // jump to the label below (doesn't work) label: for (...) ``` -A call to `break/continue` is only possible from inside a loop and the label must be somewhere above the directive. +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 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 e41c8041..00000000 --- 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/13-switch/1-rewrite-switch-if-else/solution.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md diff --git a/1-js/02-first-steps/13-switch/article.md b/1-js/02-first-steps/14-switch/article.md similarity index 97% rename from 1-js/02-first-steps/13-switch/article.md rename to 1-js/02-first-steps/14-switch/article.md index dec40a53..d86babce 100644 --- a/1-js/02-first-steps/13-switch/article.md +++ b/1-js/02-first-steps/14-switch/article.md @@ -47,7 +47,7 @@ switch (a) { break; */!* case 5: - alert( 'Too large' ); + alert( 'Too big' ); break; default: alert( "I don't know such values" ); @@ -117,7 +117,7 @@ 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; +let a = 3; switch (a) { case 4: @@ -139,7 +139,7 @@ switch (a) { 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`. +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 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 00000000..e3a0df77 --- /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/14-function-basics/2-rewrite-function-question-or/solution.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md similarity index 89% rename from 1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md rename to 1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md index c8ee9618..e4850264 100644 --- a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md +++ b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md @@ -14,4 +14,4 @@ function checkAge(age) { } ``` -Note that the parentheses around `age > 18` are not required here. They exist for better readabilty. +Note that the parentheses around `age > 18` are not required here. They exist for better readability. diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md rename to 1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/solution.md b/1-js/02-first-steps/15-function-basics/3-min/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/solution.md rename to 1-js/02-first-steps/15-function-basics/3-min/solution.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/task.md b/1-js/02-first-steps/15-function-basics/3-min/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/task.md rename to 1-js/02-first-steps/15-function-basics/3-min/task.md diff --git a/1-js/02-first-steps/14-function-basics/4-pow/solution.md b/1-js/02-first-steps/15-function-basics/4-pow/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/4-pow/solution.md rename to 1-js/02-first-steps/15-function-basics/4-pow/solution.md 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/14-function-basics/article.md b/1-js/02-first-steps/15-function-basics/article.md similarity index 79% rename from 1-js/02-first-steps/14-function-basics/article.md rename to 1-js/02-first-steps/15-function-basics/article.md index b1881e31..d82e8677 100644 --- a/1-js/02-first-steps/14-function-basics/article.md +++ b/1-js/02-first-steps/15-function-basics/article.md @@ -20,11 +20,11 @@ function showMessage() { } ``` -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) and finally the code of the function, also named "the function body", between curly braces. +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(parameters) { - ...body... +function name(parameter1, parameter2, ... parameterN) { + // body } ``` @@ -137,26 +137,23 @@ It's a good practice to minimize the use of global variables. Modern code has fe ## Parameters -We can pass arbitrary data to functions using parameters (also called *function arguments*) . +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*/!*) { // arguments: from, text +function showMessage(*!*from, text*/!*) { // parameters: from, text alert(from + ': ' + text); } -*!* -showMessage('Ann', 'Hello!'); // Ann: Hello! (*) -showMessage('Ann', "What's up?"); // Ann: What's up? (**) -*/!* +*!*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) { @@ -175,9 +172,21 @@ showMessage(from, "Hello"); // *Ann*: Hello 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 parameter is not provided, then its value becomes `undefined`. +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: @@ -185,9 +194,9 @@ For instance, the aforementioned function `showMessage(from, text)` can be calle 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`. +That's not an error. Such a call would output `"*Ann*: undefined"`. As the value for `text` isn't passed, it becomes `undefined`. -If we want to use a "default" `text` in this case, then we can specify it after `=`: +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"*/!*) { @@ -197,7 +206,13 @@ function showMessage(from, *!*text = "no text given"*/!*) { showMessage("Ann"); // Ann: no text given ``` -Now if the `text` parameter is not passed, it will get the value `"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: @@ -211,13 +226,17 @@ function showMessage(from, text = anotherFunction()) { ```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()` is called every time `showMessage()` is called without the `text` 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 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. +````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. -For instance, an explicit check for being `undefined`: +Nowadays, we can come across them in old scripts. + +For example, an explicit check for `undefined`: ```js function showMessage(from, text) { @@ -231,20 +250,64 @@ function showMessage(from, text) { } ``` -...Or the `||` operator: +...Or using the `||` operator: ```js function showMessage(from, text) { - // if text is falsy then text gets the "default" value + // 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. @@ -266,7 +329,7 @@ There may be many occurrences of `return` in a single function. For instance: ```js run function checkAge(age) { - if (age > 18) { + if (age >= 18) { *!* return true; */!* @@ -399,7 +462,7 @@ Functions that are used *very often* sometimes have ultrashort names. For example, the [jQuery](http://jquery.com) framework defines a function with `$`. The [Lodash](http://lodash.com/) library has its core function named `_`. -These are exceptions. Generally functions names should be concise and descriptive. +These are exceptions. Generally function names should be concise and descriptive. ``` ## Functions == Comments @@ -465,7 +528,7 @@ function name(parameters, delimited, by, comma) { 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. +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: diff --git a/1-js/02-first-steps/15-function-expressions-arrows/article.md b/1-js/02-first-steps/16-function-expressions/article.md similarity index 67% rename from 1-js/02-first-steps/15-function-expressions-arrows/article.md rename to 1-js/02-first-steps/16-function-expressions/article.md index d7f0f99c..b952d594 100644 --- a/1-js/02-first-steps/15-function-expressions-arrows/article.md +++ b/1-js/02-first-steps/16-function-expressions/article.md @@ -1,4 +1,4 @@ -# Function expressions and arrows +# Function expressions In JavaScript, a function is not a "magical language structure", but a special kind of value. @@ -12,7 +12,9 @@ function sayHi() { There is another syntax for creating a function that is called a *Function Expression*. -It looks like this: +It allows us to create a new function in the middle of any expression. + +For example: ```js let sayHi = function() { @@ -20,9 +22,19 @@ let sayHi = function() { }; ``` -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`. +Here we can see a variable `sayHi` getting a value, the new function, created as `function() { alert("Hello"); }`. -The meaning of these code samples is the same: "create a function and put it into the variable `sayHi`". +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`: @@ -40,7 +52,7 @@ Please note that the last line does not run the function, because there are no p 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 values, in the sense that we can call it like `sayHi()`. +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. @@ -63,10 +75,10 @@ Here's what happens above in detail: 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: +We could also have used a Function Expression to declare `sayHi`, in the first line: ```js -let sayHi = function() { +let sayHi = function() { // (1) create alert( "Hello" ); }; @@ -78,7 +90,7 @@ Everything would work the same. ````smart header="Why is there a semicolon at the end?" -You might wonder, why does Function Expression have a semicolon `;` at the end, but Function Declaration does not: +You might wonder, why do Function Expressions have a semicolon `;` at the end, but Function Declarations do not: ```js function sayHi() { @@ -90,9 +102,9 @@ 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, but rather an assignment. The semicolon `;` is recommended at the end of statements, no matter what the value is. So the semicolon here is not related to the Function Expression itself, it just terminates the statement. +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 @@ -132,13 +144,13 @@ function showCancel() { 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 function usually draws a nice-looking question window. But that's another story. +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 the same function much shorter: +We can use Function Expressions to write an equivalent, shorter function: ```js run no-beautify function ask(question, yes, no) { @@ -174,7 +186,7 @@ Let's formulate the key differences between Function Declarations and Expression 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. +- *Function Declaration:* a function, declared as a separate statement, in the main code flow: ```js // Function Declaration @@ -182,7 +194,7 @@ First, the syntax: how to differentiate between them in the code. 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" `=`: +- *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 @@ -279,7 +291,7 @@ if (age < 18) { welcome(); // \ (runs) */!* // | - function welcome() { // | + function welcome() { // | alert("Hello!"); // | Function Declaration is available } // | everywhere in the block where it's declared // | @@ -289,7 +301,7 @@ if (age < 18) { } else { - function welcome() { + function welcome() { alert("Greetings!"); } } @@ -348,114 +360,13 @@ 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. It gives more freedom in how to organize our code, because we can call such functions before they are declared. +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". +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. ``` - -## 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 around parameters 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 arrow functions for one-line actions and callbacks. -``` - ## Summary - Functions are values. They can be assigned, copied or declared in any place of the code. @@ -467,8 +378,3 @@ For now, we can already use arrow functions for one-line actions and callbacks. 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/15-function-expressions-arrows/1-rewrite-arrow/solution.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md similarity index 86% rename from 1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md rename to 1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md index 3ea11247..041db18b 100644 --- a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/solution.md +++ b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md @@ -1,7 +1,7 @@ ```js run function ask(question, yes, no) { - if (confirm(question)) yes() + if (confirm(question)) yes(); else no(); } diff --git a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md similarity index 68% rename from 1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md rename to 1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md index a888ac15..e18c08a8 100644 --- a/1-js/02-first-steps/15-function-expressions-arrows/1-rewrite-arrow/task.md +++ b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md @@ -1,11 +1,11 @@ # Rewrite with arrow functions -Replace Function Expressions with arrow functions in the code: +Replace Function Expressions with arrow functions in the code below: ```js run function ask(question, yes, no) { - if (confirm(question)) yes() + if (confirm(question)) yes(); else no(); } 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 00000000..50c0d475 --- /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/16-javascript-specials/article.md b/1-js/02-first-steps/18-javascript-specials/article.md similarity index 91% rename from 1-js/02-first-steps/16-javascript-specials/article.md rename to 1-js/02-first-steps/18-javascript-specials/article.md index b246a841..016214e3 100644 --- a/1-js/02-first-steps/16-javascript-specials/article.md +++ b/1-js/02-first-steps/18-javascript-specials/article.md @@ -55,7 +55,7 @@ To fully enable all features of modern JavaScript, we should start scripts with 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-fashion, "compatible" way. We'd generally prefer the modern behavior. +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. @@ -81,9 +81,10 @@ let x = 5; x = "John"; ``` -There are 7 data types: +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", @@ -143,7 +144,7 @@ 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](mdn:/JavaScript/Reference/Operators/Bitwise_Operators) when they are needed. +: Bitwise operators work with 32-bit integers at the lowest, bit-level: see the [docs](mdn:/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`. @@ -151,6 +152,9 @@ Conditional 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: @@ -170,7 +174,7 @@ Comparisons Other operators : There are few others, like a comma operator. -More in: , , . +More in: , , , . ## Loops @@ -212,6 +216,7 @@ 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!"); @@ -251,7 +256,7 @@ We covered three ways to create a function in JavaScript: 3. Arrow functions: ```js - // expression at the right side + // expression on the right side let sum = (a, b) => a + b; // or multi-line syntax with { ... }, need return here: @@ -268,11 +273,11 @@ We covered three ways to create a function in JavaScript: ``` -- Functions may have local variables: those declared inside its body. Such variables are only visible inside the function. +- 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 , . +Details: see , . ## More to come 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 c777ae69..4f50fb42 100644 --- a/1-js/03-code-quality/01-debugging-chrome/article.md +++ b/1-js/03-code-quality/01-debugging-chrome/article.md @@ -1,10 +1,10 @@ -# Debugging in Chrome +# Debugging in the browser Before writing more complex code, let's talk about debugging. [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 has enough features, most other browsers have a similar process`. +We'll be using Chrome here, because it has enough features, most other browsers have a similar process. ## The "Sources" panel @@ -18,19 +18,19 @@ Here's what you should see if you are doing it for the first time: ![](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 `hello.js` in the tree view. Here's what should show up: ![](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 @@ -38,7 +38,7 @@ If we press `key:Esc`, then a console opens below. We can type commands there an 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.svg) @@ -63,12 +63,12 @@ We can always find a list of breakpoints in the right panel. That's useful when - ...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 in it, like this: @@ -84,8 +84,7 @@ 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 @@ -99,7 +98,7 @@ Please open the informational dropdowns to the right (labeled with arrows). They 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.** @@ -119,8 +118,8 @@ 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 panel. Let's engage them. - - -- continue the execution, hotkey `key:F8`. + + -- "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: @@ -129,20 +128,33 @@ There are buttons for it at the top of the right panel. Let's engage them. 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. - -- 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": run the next command, hotkey `key:F9`. +: Run the next statement. If we click it now, `alert` will be shown. - -- 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. + Clicking this again and again will step through all script statements one by one. - -- 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 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). - -- enable/disable all breakpoints. + 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. + + That's good if we're not interested to see what happens inside the function call. + + -- "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. + + 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. + + -- "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. : 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". @@ -172,9 +184,9 @@ 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"). -When paused, we can debug - examine variables and trace the code 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 . diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg index 73bcc484..5fc6dce3 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg @@ -1 +1 @@ -open sources \ No newline at end of file +open sources \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg index 580bf638..63bf4966 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg @@ -1 +1 @@ -here's the listbreakpoints \ No newline at end of file +here's the listbreakpoints \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg index 39f4f917..3fe5f124 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg index 40df82a3..0147c2e0 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg @@ -1 +1 @@ -213jump to the outer functionwatch expressionscurrent variables \ No newline at end of file +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-trace-1.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg index dc96ea4f..9fa1b3b8 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg @@ -1 +1 @@ -nested calls \ No newline at end of file +nested calls \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg index 4c762923..01670825 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg @@ -1 +1 @@ -213 \ No newline at end of file +213 \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/head.html b/1-js/03-code-quality/01-debugging-chrome/head.html index f219b0af..615326c0 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 00000000..83303365 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/largeIcons.svg @@ -0,0 +1,1472 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + a + b + c + d + e + f + g + h + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + + + + + + + + + + + + + + + + + diff --git a/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg b/1-js/03-code-quality/01-debugging-chrome/toolbarButtonGlyphs.svg deleted file mode 100644 index 5bdf20a8..00000000 --- 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 764e36c6..4facc8b2 100644 --- a/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md +++ b/1-js/03-code-quality/02-coding-style/1-style-errors/solution.md @@ -12,7 +12,7 @@ function pow(x,n) // <- no space between arguments let x=prompt("x?",''), n=prompt("n?",'') // <-- technically possible, // but better make it 2 lines, also there's no spaces and missing ; -if (n<0) // <- no spaces inside (n < 0), and should be extra line above it +if (n<=0) // <- no spaces inside (n <= 0), and should be extra line above it { // <- figure bracket on a separate line // below - long lines can be split into multiple lines for improved readability alert(`Power ${n} is not supported, please enter an integer number greater than zero`); @@ -39,7 +39,7 @@ function pow(x, n) { let x = prompt("x?", ""); let n = prompt("n?", ""); -if (n < 0) { +if (n <= 0) { alert(`Power ${n} is not supported, please enter an integer number greater than zero`); } else { diff --git a/1-js/03-code-quality/02-coding-style/article.md b/1-js/03-code-quality/02-coding-style/article.md index bdcfec54..904f0a93 100644 --- a/1-js/03-code-quality/02-coding-style/article.md +++ b/1-js/03-code-quality/02-coding-style/article.md @@ -86,7 +86,7 @@ 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, + 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. `; @@ -116,7 +116,7 @@ There are two types of indents: 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, @@ -285,7 +285,7 @@ Of course, a team can always write their own style guide, but usually there's no 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/) @@ -301,11 +301,11 @@ The great thing about them is that style-checking can also find some bugs, like 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 many popular editors: just enable the plugin in the editor and configure the style. @@ -328,14 +328,14 @@ Here's an example of an `.eslintrc` file: }, "rules": { "no-console": 0, - "indent": ["warning", 2] + "indent": 2 } } ``` Here the directive `"extends"` denotes that the configuration is based on the "eslint:recommended" set of settings. After that, we specify our own. -It is also possible to download style rule sets from the web and extend them instead. See for more details about installation. +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 have built-in linting, which is convenient but not as customizable as ESLint. diff --git a/1-js/03-code-quality/02-coding-style/code-style.svg b/1-js/03-code-quality/02-coding-style/code-style.svg index 4cb4a893..739d9f1e 100644 --- a/1-js/03-code-quality/02-coding-style/code-style.svg +++ b/1-js/03-code-quality/02-coding-style/code-style.svg @@ -1 +1 @@ -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 parametersA space between parameters \ No newline at end of file +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/03-comments/article.md b/1-js/03-code-quality/03-comments/article.md index 29ba701f..af3a06c8 100644 --- a/1-js/03-code-quality/03-comments/article.md +++ b/1-js/03-code-quality/03-comments/article.md @@ -125,25 +125,25 @@ Describe the architecture 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. 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 7846f6e2..96fdf414 100644 --- a/1-js/03-code-quality/04-ninja-code/article.md +++ b/1-js/03-code-quality/04-ninja-code/article.md @@ -1,7 +1,7 @@ # Ninja code -```quote author="Confucius" +```quote author="Confucius (Analects)" Learning without thought is labor lost; thought without learning is perilous. ``` @@ -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. @@ -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. 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 ca639fb6..55826337 100644 --- a/1-js/03-code-quality/05-testing-mocha/article.md +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -2,7 +2,7 @@ Automated testing will be used in further tasks, and it's also widely used in real projects. -## 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. @@ -51,7 +51,7 @@ describe("pow", function() { A spec has three main building blocks that you can see above: `describe("title", function() { ... })` -: What functionality we're describing. In our case we're describing the function `pow`. Used to group "workers" -- the `it` blocks. +: What functionality we're describing? In our case we're describing the function `pow`. Used to group "workers" -- the `it` blocks. `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. @@ -79,7 +79,7 @@ So, the development is *iterative*. We write the spec, implement it, make sure t 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 implementaton, let's use few JavaScript libraries to run the tests, just to see that they are working (they will all fail). +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 @@ -159,8 +159,8 @@ We can select one of two ways to organize the test here: 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); }); }); @@ -182,7 +182,7 @@ The result: [iframe height=250 src="pow-2" edit border="1"] -As we could expect, the second test failed. Sure, our function always returns `8`, while the `assert` expects `27`. +As we could expect, the second test failed. Sure, our function always returns `8`, while the `assert` expects `81`. ## Improving the implementation diff --git a/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js b/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js index cad51d3e..d3de8254 100644 --- a/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js @@ -1,5 +1,11 @@ describe("test", function() { + + // Mocha usually waits for the tests for 2 seconds before considering them wrong + + this.timeout(200000); // With this code we increase this - in this case to 200,000 milliseconds + // This is because of the "alert" function, because if you delay pressing the "OK" button the tests will not pass! + before(() => alert("Testing started – before all tests")); after(() => alert("Testing finished – after all tests")); 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 9a2f8fde..c803f0e6 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/06-polyfills/article.md b/1-js/03-code-quality/06-polyfills/article.md index b399fd42..83a12fb1 100644 --- a/1-js/03-code-quality/06-polyfills/article.md +++ b/1-js/03-code-quality/06-polyfills/article.md @@ -1,54 +1,92 @@ -# 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). 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). -## 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 systems like [webpack](http://webpack.github.io/) provide means to run transpiler automatically on every code change, so that very easy to integrate into development process. +## 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. - New language features may include new built-in functions and syntax constructs. - The transpiler rewrites the code, transforming syntax constructs into older ones. But as for new built-in functions, we need to implement them. JavaScript is a highly dynamic language, scripts may add/modify any functions, 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`. - A script that updates/adds new functions is called "polyfill". It "fills in" the gap and adds missing implementations. +A transpiler would analyze our code and rewrite `height ?? 100` into `(height !== undefined && height !== null) ? height : 100`. - Two interesting polyfills are: - - [core js](https://github.com/zloirock/core-js) that supports a lot, allows to include only needed features. - - [polyfill.io](http://polyfill.io) service that provides a script with polyfills, depending on the features and user's browser. +```js +// before running the transpiler +height = height ?? 100; -So, if we're going to use modern language features, a transpiler and a polyfill are necessary. - -## Examples in the tutorial - - -````online -Most examples are runnable at-place, like this: - -```js run -alert('Press the "Play" button in the upper-right corner to run'); +// after running the transpiler +height = (height !== undefined && height !== null) ? height : 100; ``` -Examples that use modern JS will work only if your browser supports it. -```` +Now the rewritten code is suitable for older JavaScript engines. -```offline -As you're reading the offline version, in PDF examples are not runnable. In EPUB some of them can run. +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. + +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. + +## 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`. + +In some (very outdated) JavaScript engines, there's no `Math.trunc`, so such code will fail. + +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); + }; +} ``` -Google Chrome is usually the most up-to-date with language features, good to run bleeding-edge demos without any transpilers, but other modern browsers also work fine. +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. +- [polyfill.io](http://polyfill.io) service that provides a script with polyfills, depending on the features and user's browser. + + +## 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. + 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 f73c2f92..00000000 --- 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 a9aada63..00000000 --- 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`? What 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 33eb8922..6878ca08 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 120e8dde..0fe5979f 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 data 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. @@ -44,7 +44,7 @@ The resulting `user` object can be imagined as a cabinet with two signed files l ![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: @@ -62,7 +62,7 @@ user.isAdmin = true; ![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; @@ -101,7 +101,9 @@ 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`. + +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: @@ -159,7 +161,7 @@ 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: @@ -199,50 +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 vulnerabilities if we intend 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), 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: @@ -250,7 +215,7 @@ For instance: function makeUser(name, age) { return { name: name, - age: age + age: age, // ...other properties }; } @@ -268,7 +233,7 @@ function makeUser(name, age) { *!* return { name, // same as name: name - age // same as age: age + age, // same as age: age // ... }; */!* @@ -284,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 = {}; @@ -294,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 @@ -312,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 will 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 the property existance just 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`: @@ -336,13 +350,12 @@ 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. @@ -375,7 +388,6 @@ Note that all "for" constructs allow us to declare the looping variable inside t 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 Are objects ordered? In other words, if we loop over an object, do we get all properties in the same order they were added? Can we rely on this? @@ -400,7 +412,7 @@ for (let code in codes) { */!* ``` -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: @@ -412,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 @@ -459,262 +472,6 @@ for (let code in codes) { 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.svg) - -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.svg) - -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.svg) - -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, if 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 only 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 independent 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. @@ -725,17 +482,13 @@ 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). - What we've studied in this chapter is called a "plain object", or just `Object`. There are many other kinds of objects in JavaScript: diff --git a/1-js/04-object-basics/01-object/object-user-delete.svg b/1-js/04-object-basics/01-object/object-user-delete.svg index 87753bb1..c5af7e7a 100644 --- a/1-js/04-object-basics/01-object/object-user-delete.svg +++ b/1-js/04-object-basics/01-object/object-user-delete.svg @@ -1 +1 @@ -nameisAdminuser \ No newline at end of file +nameisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-empty.svg b/1-js/04-object-basics/01-object/object-user-empty.svg index df684a0a..99edb026 100644 --- a/1-js/04-object-basics/01-object/object-user-empty.svg +++ b/1-js/04-object-basics/01-object/object-user-empty.svg @@ -1 +1 @@ -emptyuser \ No newline at end of file +emptyuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.svg b/1-js/04-object-basics/01-object/object-user-isadmin.svg index c7a24cc9..e2cc0eaf 100644 --- a/1-js/04-object-basics/01-object/object-user-isadmin.svg +++ b/1-js/04-object-basics/01-object/object-user-isadmin.svg @@ -1 +1 @@ -nameageisAdminuser \ No newline at end of file +nameageisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-props.svg b/1-js/04-object-basics/01-object/object-user-props.svg index cb9afc1a..b3d5c9b7 100644 --- a/1-js/04-object-basics/01-object/object-user-props.svg +++ b/1-js/04-object-basics/01-object/object-user-props.svg @@ -1 +1 @@ -nameagelikes birdsuser \ No newline at end of file +nameagelikes birdsuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user.svg b/1-js/04-object-basics/01-object/object-user.svg index 69f7efd9..f499fbc1 100644 --- a/1-js/04-object-basics/01-object/object-user.svg +++ b/1-js/04-object-basics/01-object/object-user.svg @@ -1 +1 @@ -nameageuser \ No newline at end of file +nameageuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object.svg b/1-js/04-object-basics/01-object/object.svg index 5a4a49fb..47431a6e 100644 --- a/1-js/04-object-basics/01-object/object.svg +++ b/1-js/04-object-basics/01-object/object.svg @@ -1 +1 @@ -key1key2key3 \ No newline at end of file +key1key2key3 \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-copy-value.svg b/1-js/04-object-basics/01-object/variable-copy-value.svg deleted file mode 100644 index e09f521f..00000000 --- a/1-js/04-object-basics/01-object/variable-copy-value.svg +++ /dev/null @@ -1 +0,0 @@ -"Hello!"message"Hello!"phrase \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg deleted file mode 100644 index e30bf7e5..00000000 --- a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg +++ /dev/null @@ -1 +0,0 @@ -<global variable>ObjectObjectwifefamilyname: "John"name: "Ann"motherObjectfatherhusband \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg b/1-js/04-object-basics/02-garbage-collection/family-no-family.svg deleted file mode 100644 index cb945855..00000000 --- a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg +++ /dev/null @@ -1 +0,0 @@ -<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg b/1-js/04-object-basics/02-garbage-collection/family-no-father.svg deleted file mode 100644 index 8e7f3025..00000000 --- a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg +++ /dev/null @@ -1 +0,0 @@ -ObjectObjectwifefamilyname: "John"name: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family.svg b/1-js/04-object-basics/02-garbage-collection/family.svg deleted file mode 100644 index 37613260..00000000 --- a/1-js/04-object-basics/02-garbage-collection/family.svg +++ /dev/null @@ -1 +0,0 @@ -ObjectObjectfatherwifefamilyname: "John"name: "Ann"motherObjecthusband<global variable> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg deleted file mode 100644 index 50697c4b..00000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg deleted file mode 100644 index dc086832..00000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg deleted file mode 100644 index d5faadb3..00000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg deleted file mode 100644 index 37364776..00000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg +++ /dev/null @@ -1 +0,0 @@ -<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg deleted file mode 100644 index 49e9c62b..00000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg +++ /dev/null @@ -1 +0,0 @@ -<global>unreachables \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/article.md b/1-js/04-object-basics/02-object-copy/article.md new file mode 100644 index 00000000..e80f748a --- /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/01-object/variable-contains-reference.svg b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg similarity index 63% rename from 1-js/04-object-basics/01-object/variable-contains-reference.svg rename to 1-js/04-object-basics/02-object-copy/variable-contains-reference.svg index dedb7eaa..267f0457 100644 --- a/1-js/04-object-basics/01-object/variable-contains-reference.svg +++ b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg @@ -1 +1 @@ -username \ No newline at end of file +username \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.svg b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg similarity index 73% rename from 1-js/04-object-basics/01-object/variable-copy-reference.svg rename to 1-js/04-object-basics/02-object-copy/variable-copy-reference.svg index f212c085..a847fb20 100644 --- a/1-js/04-object-basics/01-object/variable-copy-reference.svg +++ b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg @@ -1 +1 @@ -useradminname \ No newline at end of file +useradminname \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/variable-copy-value.svg b/1-js/04-object-basics/02-object-copy/variable-copy-value.svg new file mode 100644 index 00000000..0d6ca67b --- /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/02-garbage-collection/article.md b/1-js/04-object-basics/03-garbage-collection/article.md similarity index 66% rename from 1-js/04-object-basics/02-garbage-collection/article.md rename to 1-js/04-object-basics/03-garbage-collection/article.md index 620cbc21..50ada671 100644 --- a/1-js/04-object-basics/02-garbage-collection/article.md +++ b/1-js/04-object-basics/03-garbage-collection/article.md @@ -14,8 +14,8 @@ Simply put, "reachable" values are those that are accessible or usable somehow. For instance: - - Local variables and parameters of the current function. - - Variables and parameters for other functions on the current chain of nested calls. + - 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) @@ -23,7 +23,7 @@ Simply put, "reachable" values are those that are accessible or usable somehow. 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. + 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. @@ -74,7 +74,7 @@ Now if we do the same: 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. +...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 @@ -156,7 +156,7 @@ 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). +- ...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: @@ -169,11 +169,11 @@ The first step marks the roots: ![](garbage-collection-2.svg) -Then their references are marked: +Then we follow their references and mark referenced objects: ![](garbage-collection-3.svg) -...And their references, while possible: +...And continue to follow further references, while possible: ![](garbage-collection-4.svg) @@ -181,17 +181,17 @@ Now the objects that could not be visited in the process are considered unreacha ![](garbage-collection-5.svg) -That's the concept of how garbage collection works. +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. -JavaScript engines apply many optimizations to make it run faster and not affect the execution. +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". 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. +- **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 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. +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 @@ -199,14 +199,14 @@ 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. +- 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, 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). +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](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). -[V8 blog](https://v8.dev/) 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. +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](http://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. +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 00000000..a582ca64 --- /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 00000000..c73dd6a4 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg @@ -0,0 +1 @@ +<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg similarity index 57% rename from 1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg rename to 1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg index 37255e57..6bd13c0e 100644 --- a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg +++ b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg @@ -1 +1 @@ -Objectfamilyname: "Ann"motherObject<global> \ No newline at end of file +Objectfamilyname: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/family-no-father.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg new file mode 100644 index 00000000..fd1f2060 --- /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 00000000..fd053487 --- /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 00000000..5cac52e9 --- /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 00000000..7dd3a693 --- /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 00000000..10605778 --- /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 00000000..bd485ade --- /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 00000000..2d85432b --- /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/02-garbage-collection/memory-user-john-admin.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg similarity index 60% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg index 9ad1d88b..19132435 100644 --- a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg @@ -1 +1 @@ -username: "John"Objectadmin<global> \ No newline at end of file +username: "John"Objectadmin<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg similarity index 75% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg index e75b8d46..07914a9c 100644 --- a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg @@ -1 +1 @@ -name: "John"Objectuser: null<global> \ No newline at end of file +name: "John"Objectuser: null<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg similarity index 66% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john.svg index f051391d..15bd51af 100644 --- a/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg +++ b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg @@ -1 +1 @@ -username: "John"Object<global> \ No newline at end of file +username: "John"Object<global> \ No newline at end of file diff --git a/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md b/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md index ea00c970..f33c9310 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,7 +7,7 @@ function makeUser() { name: "John", ref: this }; -}; +} let user = makeUser(); @@ -22,6 +22,17 @@ The value of `this` is one for the whole function, code blocks and object litera 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(); 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 4784b082..c6f8f965 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 1f71eda4..4decb76d 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/task.md b/1-js/04-object-basics/04-object-methods/7-calculator/task.md index aa22608e..82d0da03 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 e98fe641..a35c009c 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 a2b17fcc..b4f2459b 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 2b47873f..f215461d 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 @@ -21,9 +21,9 @@ 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: @@ -33,7 +33,7 @@ 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 eca9f4e9..a2a19c62 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 @@ -28,12 +28,14 @@ ladder.up(); ladder.up(); ladder.down(); ladder.showStep(); // 1 +ladder.down(); +ladder.showStep(); // 0 ``` 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. 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 3d83a224..cea2b6a7 100644 --- a/1-js/04-object-basics/04-object-methods/article.md +++ b/1-js/04-object-basics/04-object-methods/article.md @@ -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. +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 @@ -81,7 +81,7 @@ user = { // method shorthand looks better, right? user = { *!* - sayHi() { // same as "sayHi: function()" + sayHi() { // same as "sayHi: function(){...}" */!* alert("Hello"); } @@ -90,7 +90,7 @@ 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 @@ -160,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. 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() { @@ -220,111 +222,19 @@ In this case `this` is `undefined` in strict mode. If we try to access `this.nam 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. -Usually such call is an programming error. If there's `this` inside a function, it expects to be called in an object context. +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 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. - -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). 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. 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 ca449e5f..00000000 --- a/1-js/04-object-basics/05-object-toprimitive/article.md +++ /dev/null @@ -1,221 +0,0 @@ - -# Object to primitive conversion - -What happens when objects are added `obj1 + obj2`, subtracted `obj1 - obj2` or printed using `alert(obj)`? - -In that case, objects are auto-converted to primitives, and then the operation is carried out. - -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. All objects are `true` in a boolean context. There are 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 like `alert(obj)` and in similar contexts. - -## ToPrimitive - -We can fine-tune string and numeric conversion, using special object methods. - -There are three variants of type conversion, so-called "hints", 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; - ``` - -`"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, it's also unclear which conversion should be done. - - ```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)` - the method with the symbolic key `Symbol.toPrimitive` (system symbol), if such 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) { - // must 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 -``` - -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. - -## Return types - -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, 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` *must* return a primitive, otherwise there will be an error. -``` - -## Further operations - -An operation that initiated the conversion gets the primitive, and then continues to work with it, applying further conversions if necessary. - -For instance: - -- Mathematical operations, except binary plus, convert the primitive to a number: - - ```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 - ``` - -- Binary plus will concatenate strings in the same situation: - ```js run - let obj = { - toString() { - return "2"; - } - }; - - alert(obj + 2); // 22 (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) - -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/task.md b/1-js/04-object-basics/06-constructor-new/1-two-functions-one-object/task.md index 8c1fea8e..e932a201 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 03605392..bba80e5c 100644 --- a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js +++ b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/_js.view/test.js @@ -10,6 +10,11 @@ describe("calculator", function() { calculator = new Calculator(); calculator.read(); }); + + it("the read method asks for two values using prompt and remembers them in object properties", function() { + assert.equal(calculator.a, 2); + assert.equal(calculator.b, 3); + }); it("when 2 and 3 are entered, the sum is 5", function() { assert.equal(calculator.sum(), 5); diff --git a/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md b/1-js/04-object-basics/06-constructor-new/2-calculator-constructor/task.md index 60e7c373..c862bec4 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/article.md b/1-js/04-object-basics/06-constructor-new/article.md index c4ccca66..a335464f 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. @@ -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,7 +81,7 @@ 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. ```` ## Constructor mode test: new.target @@ -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() { @@ -136,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. @@ -170,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 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 00000000..4c602942 --- /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/03-symbol/article.md b/1-js/04-object-basics/08-symbol/article.md similarity index 78% rename from 1-js/04-object-basics/03-symbol/article.md rename to 1-js/04-object-basics/08-symbol/article.md index a17f85fe..10a98af0 100644 --- a/1-js/04-object-basics/03-symbol/article.md +++ b/1-js/04-object-basics/08-symbol/article.md @@ -1,9 +1,16 @@ # 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. +By specification, only two primitive types may serve as object property keys: -Till now we've been using only strings. Now let's see the benefits that symbols can give us. +- 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 @@ -12,18 +19,17 @@ A "symbol" represents a unique identifier. A value of this type can be created using `Symbol()`: ```js -// id is a new symbol let id = Symbol(); ``` -Upon creation, we can give symbol a description (also called a symbol name), mostly useful for debugging purposes: +Upon creation, we can give symbols a description (also called a symbol name), mostly useful for debugging purposes: -```js run +```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. +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: @@ -38,6 +44,8 @@ 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. @@ -53,6 +61,7 @@ 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"); *!* @@ -61,6 +70,7 @@ alert(id.toString()); // Symbol(id), now it works ``` Or get `symbol.description` property to show the description only: + ```js run let id = Symbol("id"); *!* @@ -72,6 +82,7 @@ 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. @@ -92,9 +103,9 @@ 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 belongs to another code, and that code also works with them, we shouldn't just add any fields to it. That's unsafe. But a symbol cannot be accessed accidentally, the third-party code probably won't even see it, so it's probably all right to do. +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. That may be another JavaScript library, so that the scripts are completely unaware of each other. +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: @@ -109,7 +120,7 @@ There will be no conflict between our and their identifiers, because symbols are ...But if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: -```js run +```js let user = { name: "John" }; // Our script uses "id" property @@ -121,7 +132,7 @@ user.id = "Their id value" // Boom! overwritten by another script! ``` -### Symbols in a literal +### Symbols in an object literal If we want to use a symbol in an object literal `{...}`, we need square brackets around it. @@ -133,7 +144,7 @@ let id = Symbol("id"); let user = { name: "John", *!* - [id]: 123 // not "id: 123" + [id]: 123 // not "id": 123 */!* }; ``` @@ -158,10 +169,10 @@ for (let key in user) alert(key); // name, age (no symbols) */!* // the direct access by the symbol works -alert( "Direct: " + user[id] ); +alert( "Direct: " + user[id] ); // Direct: 123 ``` -`Object.keys(user)` 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. +[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: @@ -178,22 +189,6 @@ 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 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. @@ -222,12 +217,12 @@ Symbols inside the registry are called *global symbols*. If we want an applicati ```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. +In JavaScript, as we can see, that's true 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. +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: @@ -241,9 +236,9 @@ 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`. +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, any symbols have `description` property. +That said, all symbols have the `description` property. For instance: @@ -284,10 +279,11 @@ Symbols are always different values, even if they have the same name. If we want 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. So they are not really hidden. But most libraries, built-in functions and syntax constructs don't use these methods. +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 00000000..8b0008b1 --- /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 the 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/task.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/task.md index 50c781ea..208f84cc 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 06138a00..69e7196e 100644 --- a/1-js/05-data-types/01-primitives-methods/article.md +++ b/1-js/05-data-types/01-primitives-methods/article.md @@ -7,7 +7,7 @@ Let's look at the key distinctions between primitives and objects. A primitive - Is a value of a primitive type. -- There are 6 primitive types: `string`, `number`, `boolean`, `symbol`, `null` and `undefined`. +- There are 7 primitive types: `string`, `number`, `bigint`, `boolean`, `symbol`, `null` and `undefined`. An object @@ -39,7 +39,7 @@ Objects are "heavier" than primitives. They require additional resources to supp 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: @@ -48,7 +48,7 @@ The solution looks a little bit awkward, but here it is: 2. The language allows access to methods and properties of strings, numbers, booleans and symbols. 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 string method [str.toUpperCase()](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) that returns a capitalized `str`. @@ -104,9 +104,10 @@ if (zero) { // zero is true, because it's an object } ``` -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 a17a4671..4bcd7451 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/article.md b/1-js/05-data-types/02-number/article.md index 634baf15..c704bd98 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](https://en.wikipedia.org/wiki/IEEE_754-2008_revision), also known as "double precision floating point numbers". +In modern JavaScript, there are two types of numbers: -Let's 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 few special areas, we devote them 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: -In JavaScript, we shorten a number by appending the letter `"e"` to the number and specifying the zeroes count: +```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 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): ```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 @@ -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`, the middle case: `3.5` rounds up to `4` too. `Math.trunc` (not supported by Internet Explorer) : Removes anything after the decimal point without rounding: `3.1` becomes `3`, `-1.1` becomes `-1`. @@ -149,7 +164,7 @@ 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. @@ -166,20 +181,20 @@ 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 ``` - 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](https://en.wikipedia.org/wiki/IEEE_754-2008_revision), 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 @@ -187,7 +202,7 @@ 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*/!* @@ -201,13 +216,13 @@ 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 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)`. +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)`. 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. @@ -234,7 +249,7 @@ Can we work around the problem? Sure, the most reliable method is to round the r ```js run let sum = 0.1 + 0.2; -alert( sum.toFixed(2) ); // 0.30 +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: @@ -271,13 +286,11 @@ 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. ``` - - ## Tests: isFinite and isNaN Remember these two special numeric values? @@ -295,7 +308,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 @@ -319,18 +332,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="Compare with `Object.is`" +````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. -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: +- `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 + + // 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 + ``` + +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. +```` + +```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: 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 true, because internally the number has a sign bit that may be different even if all other bits are zeroes. +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. In all other cases, `Object.is(a, b)` is the same as `a === b`. -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)). +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)). ``` @@ -382,7 +423,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 @@ -390,8 +431,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 @@ -399,27 +440,34 @@ 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. 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 f7a332d0..be5dd2aa 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 @@ -8,12 +8,7 @@ let newStr = str[0].toUpperCase() + str.slice(1); 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: - -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: +The easiest way out is to add a test for an empty string, like this: ```js run demo function ucFirst(str) { @@ -24,4 +19,3 @@ function ucFirst(str) { alert( ucFirst("john") ); // John ``` - 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 5546c47e..d51672ae 100644 --- a/1-js/05-data-types/03-string/3-truncate/solution.md +++ b/1-js/05-data-types/03-string/3-truncate/solution.md @@ -1,6 +1,6 @@ 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 demo function truncate(str, maxlength) { diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md index 3bde8bd5..618f8ef3 100644 --- a/1-js/05-data-types/03-string/article.md +++ b/1-js/05-data-types/03-string/article.md @@ -48,9 +48,9 @@ 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. +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. +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 @@ -59,10 +59,10 @@ It is still possible to create multiline strings with single and double quotes b ```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 are equal, just written differently: +As a simpler example, these two lines are equal, just written differently: ```js run let str1 = "Hello\nWorld"; // two lines using a "newline symbol" @@ -74,33 +74,26 @@ World`; alert(str1 == str2); // true ``` -There are other, less common "special" characters. - -Here's the full list: +There are other, less common special characters: | Character | Description | |-----------|-------------| |`\n`|New line| -|`\r`|Carriage return: not used alone. Windows text files use a combination of two characters `\r\n` to represent a line break. | -|`\'`, `\"`|Quotes| +|`\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| -|`\b`, `\f`, `\v`| Backspace, Form Feed, Vertical Tab -- kept for compatibility, not used nowadays. | -|`\xXX`|Unicode character with the given hexadecimal unicode `XX`, e.g. `'\x7A'` is the same as `'z'`.| -|`\uXXXX`|A unicode symbol with the hex code `XXXX` in UTF-16 encoding, for instance `\u00A9` -- is a unicode for the copyright symbol `©`. It must be exactly 4 hex digits. | -|`\u{X…XXXXXX}` (1 to 6 hex characters)|A unicode symbol with the given UTF-32 encoding. Some rare characters are encoded with two unicode symbols, taking 4 bytes. This way we can insert long codes. | +|`\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 might also use it if we wanted to insert a quote into the string. +So-called "escaped" quotes `\'`, `\"`, \\` are used to insert a quote into the same-quoted string. For instance: @@ -110,21 +103,13 @@ 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, only to 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: +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 @@ -139,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 parenthesis 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`: @@ -214,7 +202,7 @@ alert( 'Interface'.toLowerCase() ); // interface Or, if we want a single character lowercased: -```js +```js run alert( 'Interface'[0].toLowerCase() ); // 'i' ``` @@ -239,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`: @@ -310,45 +298,6 @@ if (str.indexOf("Widget") != -1) { } ``` -#### 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. - -In practice, that means a simple thing: for 32-bit integers `~n` equals `-(n+1)`. - -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` (that's for any 32-bit signed integer `n`). - -So, the test `if ( ~str.indexOf("...") )` is truthy only if 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". - -To be precise though, as big numbers are truncated to 32 bits by `~` operator, there exist other numbers that give `0`, the smallest is `~4294967295=0`. That makes such check is correct only if a string is not that long. - -Right now we can see this trick only in the old code, as modern JavaScript provides `.includes` method (see below). - ### 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. @@ -371,8 +320,8 @@ 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 @@ -394,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: @@ -403,13 +352,13 @@ 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: @@ -435,28 +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. -Of the other two variants, `slice` is a little bit more flexible, it allows negative arguments and shorter to write. So, it's enough to remember solely `slice` of these three methods. +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 @@ -479,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)` @@ -497,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: @@ -515,6 +463,7 @@ for (let i = 65; i <= 220; i++) { str += String.fromCodePoint(i); } alert( str ); +// Output: // ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„ // ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ ``` @@ -526,15 +475,15 @@ 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. 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. @@ -552,118 +501,10 @@ 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, letter order depends on the language) 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 or hieroglyphic characters or other rare symbols. - -You can skip the section if you don't plan to support them. -``` - -### Surrogate pairs - -All frequently used characters have 2-byte codes. 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. - -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 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 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. - ## 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...`. +- We can use special characters, such as a line break `\n`. - To get a character, use: `[]`. - To get a substring, use: `slice` or `substring`. - To lowercase/uppercase a string, use: `toLowerCase/toUpperCase`. @@ -677,3 +518,5 @@ There are several other helpful methods in strings: - ...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 . + +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 . \ No newline at end of file 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 daadf494..7e1ca3bd 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 @@ -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 serious sluggishness. +For big arrays (1000, 10000 or more items) such algorithms can lead to serious sluggishness. # Fast solution @@ -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 e63c4e62..f1a1d9f9 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 @@ -10,15 +10,15 @@ The task is: find the contiguous subarray of `arr` with the maximal sum of items 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: 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 16d14071..d4551c79 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. 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 e994ae07..3cb0317c 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 340c5fee..f1e13499 100644 --- a/1-js/05-data-types/04-array/3-call-array-this/task.md +++ b/1-js/05-data-types/04-array/3-call-array-this/task.md @@ -11,7 +11,7 @@ let arr = ["a", "b"]; arr.push(function() { alert( this ); -}) +}); arr[2](); // ? ``` diff --git a/1-js/05-data-types/04-array/array-pop.svg b/1-js/05-data-types/04-array/array-pop.svg index 35191605..82b112b4 100644 --- a/1-js/05-data-types/04-array/array-pop.svg +++ b/1-js/05-data-types/04-array/array-pop.svg @@ -1 +1 @@ -0123"Apple""Orange""Pear""Lemon"length = 4clear012"Apple""Orange""Pear"length = 3 \ No newline at end of file +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-shift.svg b/1-js/05-data-types/04-array/array-shift.svg index 09236b9d..9485a3c9 100644 --- a/1-js/05-data-types/04-array/array-shift.svg +++ b/1-js/05-data-types/04-array/array-shift.svg @@ -1 +1 @@ -123"Orange""Pear""Lemon"length = 423"Orange""Pear""Lemon"length = 3clearmove elements to the left0"Apple"012"Orange""Pear""Lemon"11 \ No newline at end of file +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-speed.svg b/1-js/05-data-types/04-array/array-speed.svg index 5660cd5e..41f7d998 100644 --- a/1-js/05-data-types/04-array/array-speed.svg +++ b/1-js/05-data-types/04-array/array-speed.svg @@ -1 +1 @@ -0123popunshiftpushshift \ No newline at end of file +0123popunshiftpushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/article.md b/1-js/05-data-types/04-array/article.md index 7dc54bd4..4bcab0bc 100644 --- a/1-js/05-data-types/04-array/article.md +++ b/1-js/05-data-types/04-array/article.md @@ -92,6 +92,38 @@ 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 to use 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 @@ -121,9 +153,9 @@ A stack is usually illustrated as a pack of cards: new cards are added to the to 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 you 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'); @@ -193,7 +227,7 @@ An array is a special kind of object. The square brackets used to access a prope 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: @@ -209,7 +243,7 @@ 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. @@ -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: @@ -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,9 +413,7 @@ 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 @@ -429,25 +461,77 @@ 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 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. - 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. @@ -460,4 +544,8 @@ To loop over the elements of the array: - `for (let item of arr)` -- the modern syntax for items only, - `for (let i in arr)` -- never use. -We will return to arrays and study more methods to add, remove, extract elements and sort arrays in the chapter . +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.svg b/1-js/05-data-types/04-array/queue.svg index 0ed2f1cd..e89a3dcd 100644 --- a/1-js/05-data-types/04-array/queue.svg +++ b/1-js/05-data-types/04-array/queue.svg @@ -1 +1 @@ -pushshift \ No newline at end of file +pushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/stack.svg b/1-js/05-data-types/04-array/stack.svg index dcc600e7..ae0c474d 100644 --- a/1-js/05-data-types/04-array/stack.svg +++ b/1-js/05-data-types/04-array/stack.svg @@ -1 +1 @@ -pushpop \ No newline at end of file +pushpop \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/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 00000000..8dea23a0 --- /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 00000000..e48ba138 --- /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/11-async/01-callbacks/01-animate-circle-callback/solution.md b/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md similarity index 100% rename from 1-js/11-async/01-callbacks/01-animate-circle-callback/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 00000000..7f008235 --- /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/task.md b/1-js/05-data-types/05-array-methods/2-filter-range/task.md index 18b2c1d9..46e47c93 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/test.js b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js index db32d9a1..241b74c6 100644 --- a/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js +++ b/1-js/05-data-types/05-array-methods/3-filter-range-in-place/_js.view/test.js @@ -4,13 +4,13 @@ describe("filterRangeInPlace", function() { let arr = [5, 3, 8, 1]; - filterRangeInPlace(arr, 1, 4); + filterRangeInPlace(arr, 2, 5); - assert.deepEqual(arr, [3, 1]); + assert.deepEqual(arr, [5, 3]); }); it("doesn't return anything", function() { assert.isUndefined(filterRangeInPlace([1,2,3], 1, 4)); }); -}); \ No newline at end of file +}); diff --git a/1-js/05-data-types/05-array-methods/4-sort-back/task.md b/1-js/05-data-types/05-array-methods/4-sort-back/task.md index 05a08aad..0e3eeab7 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 index 45ef1619..f62452a5 100644 --- 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 @@ -10,14 +10,14 @@ function Calculator() { let split = str.split(' '), a = +split[0], op = split[1], - b = +split[2] + 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/05-array-methods/6-calculator-extendable/solution.md b/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md index 982d3f9e..ebe0714c 100644 --- a/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md +++ b/1-js/05-data-types/05-array-methods/6-calculator-extendable/solution.md @@ -1,5 +1,3 @@ - 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. - -[js src="_js/solution.js"] 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 5d8bf4a1..2d8d4fb0 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 9f1ade70..cfaf9761 100644 --- a/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md +++ b/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md @@ -1,6 +1,6 @@ ```js run no-beautify function sortByAge(arr) { - arr.sort((a, b) => a.age > b.age ? 1 : -1); + arr.sort((a, b) => a.age - b.age); } let john = { name: "John", age: 25 }; diff --git a/1-js/05-data-types/05-array-methods/article.md b/1-js/05-data-types/05-array-methods/article.md index 8e5b4efd..feb626f9 100644 --- a/1-js/05-data-types/05-array-methods/article.md +++ b/1-js/05-data-types/05-array-methods/article.md @@ -36,15 +36,15 @@ That's natural, because `delete obj.key` removes a value by the `key`. It's all 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: insert, remove and replace 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. @@ -160,16 +160,16 @@ For instance: let arr = [1, 2]; // create an array from: arr and [3,4] -alert( arr.concat([3, 4])); // 1,2,3,4 +alert( arr.concat([3, 4]) ); // 1,2,3,4 // create an array from: arr and [3,4] and [5,6] -alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6 +alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6 // 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 +alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6 ``` -Normally, it only copies elements from arrays. 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]; @@ -180,10 +180,9 @@ let arrayLike = { }; alert( arr.concat(arrayLike) ); // 1,2,[object Object] -//[1, 2, arrayLike] ``` -...But if an array-like object has a special property `Symbol.isConcatSpreadable` property, the it's treated as array by `concat`: 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]; @@ -235,12 +234,13 @@ 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 for from right to left. - `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 @@ -253,23 +253,35 @@ 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 for inclusion, and don't want to know the exact index, then `arr.includes` is preferred. +If we want to check if `item` exists in the array, and don't need the exact index, then `arr.includes` is preferred. -Also, a very minor difference of `includes` is that it correctly handles `NaN`, unlike `indexOf/lastIndexOf`: +The method [arr.lastIndexOf](mdn:js/Array/lastIndexOf) is the same as `indexOf`, but looks for from right to left. + +```js run +let fruits = ['Apple', 'Orange', 'Apple'] + +alert( fruits.indexOf('Apple') ); // 0 (first Apple) +alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple) +``` + +````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? -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 @@ -305,7 +317,28 @@ In real life arrays of objects is a common thing, so the `find` method is very u 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 and `-1` is returned when nothing is found. +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 @@ -385,11 +418,12 @@ The order became `1, 15, 2`. Incorrect. But why? **The items are sorted as strings by default.** -Literally, all elements are converted to strings for comparisons. For strings, 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()`. The function should compare two arbitrary values and return: + ```js function compare(a, b) { if (a > b) return 1; // if the first value is greater than the second @@ -420,19 +454,19 @@ 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 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 implements a generic sorting algorithm. We don't need to care how it internally 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: ```js run [1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); + return a - b; }); ``` 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". @@ -448,7 +482,7 @@ alert(arr); // *!*1, 2, 15*/!* ```` ````smart header="Arrow functions for the best" -Remember [arrow functions](info:function-expressions-arrows#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 ); @@ -457,6 +491,22 @@ arr.sort( (a, b) => a - b ); 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 The method [arr.reverse](mdn:js/Array/reverse) reverses the order of elements in `arr`. @@ -531,7 +581,7 @@ 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, array) { +let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]); ``` @@ -540,14 +590,16 @@ The function is applied to all array elements one after another and "carries on" Arguments: -- `previousValue` -- is the result of the previous function call, equals `initial` the first time (if `initial` is provided). +- `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. - `array` -- is the array. As function is applied, the result of the previous function call is passed to the next one as the first argument. -Sounds complicated, but it's not if you think about the first argument as the "accumulator" that stores the combined result of all previous execution. And at the end it becomes the result of `reduce`. +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`. + +Sounds complicated? The easiest way to grasp that is by example. @@ -575,7 +627,7 @@ The calculation flow: 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`| @@ -612,12 +664,10 @@ 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. - ## Array.isArray Arrays do not form a separate language type. They are based on objects. @@ -626,7 +676,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. @@ -655,31 +705,37 @@ 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` helps with that: +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 @@ -690,8 +746,8 @@ A cheat sheet of array methods: - `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: @@ -708,23 +764,37 @@ A cheat sheet of array methods: - `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. + - `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. diff --git a/1-js/05-data-types/05-array-methods/reduce.svg b/1-js/05-data-types/05-array-methods/reduce.svg index 47797bf7..180941dc 100644 --- a/1-js/05-data-types/05-array-methods/reduce.svg +++ b/1-js/05-data-types/05-array-methods/reduce.svg @@ -1 +1 @@ -1sum 0 current 12sum 0+1 current 23sum 0+1+2 current 34sum 0+1+2+3 current 45sum 0+1+2+3+4 current 50+1+2+3+4+5 = 15 \ No newline at end of file +1sum 0 current 12sum 0+1 current 23sum 0+1+2 current 34sum 0+1+2+3 current 45sum 0+1+2+3+4 current 50+1+2+3+4+5 = 15 \ No newline at end of file diff --git a/1-js/05-data-types/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md index 80f067ca..76f74036 100644 --- a/1-js/05-data-types/06-iterable/article.md +++ b/1-js/05-data-types/06-iterable/article.md @@ -1,7 +1,7 @@ # 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. Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well. For instance, strings are also iterable. @@ -12,7 +12,7 @@ If an object isn't technically an array, but represents a collection (list, set) 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: @@ -26,12 +26,12 @@ 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). 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 iteration is finished, otherwise `value` is the next value. +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` with remarks: @@ -45,10 +45,10 @@ let range = { range[Symbol.iterator] = function() { // ...it returns the iterator object: - // 2. Onward, for..of works only with this iterator, asking it for next values + // 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() { @@ -140,9 +140,9 @@ for (let char of str) { ## Calling an iterator explicitly -For deeper understanding let's see how to use an iterator explicitly. +For deeper understanding, let's see how to use an iterator explicitly. -We'll iterate over a string in exactlly the same way as `for..of`, but with direct calls. This code creates a string iterator and gets values from 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"; @@ -150,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(); @@ -163,12 +165,12 @@ 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. -When we use JavaScript for practical tasks in browser or other environments, we may meet objects that are iterables or array-likes, or both. +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. For instance, strings are both iterable (`for..of` works on them) and array-like (they have numeric indexes and `length`). @@ -212,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 optional second argument `mapFn` can be a function that will be applied 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 @@ -268,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: @@ -291,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. @@ -302,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/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md index 4c8af1f2..16067518 100644 --- a/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md +++ b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md @@ -36,7 +36,7 @@ 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 +let sorted = word // PAN .toLowerCase() // pan .split('') // ['p','a','n'] .sort() // ['a','n','p'] 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 index 25c74bfc..81507647 100644 --- 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 @@ -4,7 +4,7 @@ importance: 5 # Iterable keys -We'd like to get an array of `map.keys()` in a variable and then do apply array-specific methods to it, e.g. `.push`. +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: diff --git a/1-js/05-data-types/07-map-set/article.md b/1-js/05-data-types/07-map-set/article.md index c4d7c21a..35407088 100644 --- a/1-js/05-data-types/07-map-set/article.md +++ b/1-js/05-data-types/07-map-set/article.md @@ -1,10 +1,10 @@ # Map and Set -Now we've learned about the following complex data structures: +Till now, we've learned about the following complex data structures: -- Objects for storing keyed collections. -- Arrays for storing ordered collections. +- 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. @@ -15,12 +15,12 @@ But that's not enough for real life. That's why `Map` and `Set` also exist. Methods and properties 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()` -- removes everything from the map. -- `map.size` -- returns the current element count. +- [`map.set(key, value)`](mdn:js/Map/set) -- stores the value by the key. +- [`map.get(key)`](mdn:js/Map/get) -- returns the value by the key, `undefined` if `key` doesn't exist in map. +- [`map.has(key)`](mdn:js/Map/has) -- returns `true` if the `key` exists, `false` otherwise. +- [`map.delete(key)`](mdn:js/Map/delete) -- removes the value by the key. +- [`map.clear()`](mdn:js/Map/clear) -- removes everything from the map. +- [`map.size`](mdn:js/Map/size) -- returns the current element count. For instance: @@ -41,6 +41,12 @@ 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: @@ -57,24 +63,26 @@ 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 not for object keys. +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[john] = 123; // try to use john object as the key +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 +alert( visitsCountObj["[object Object]"] ); // 123 */!* ``` -As `visitsCountObj` is an object, it converts all keys, such as `john` to strings, so we've got the string key `"[object Object]"`. Definitely not what we want. +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. @@ -97,9 +105,9 @@ map.set('1', 'str1') 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`. +- [`map.keys()`](mdn:js/Map/keys) -- returns an iterable for keys, +- [`map.values()`](mdn:js/Map/values) -- returns an iterable for values, +- [`map.entries()`](mdn:js/Map/entries) -- returns an iterable for entries `[key, value]`, it's used by default in `for..of`. For instance: @@ -192,7 +200,7 @@ let prices = Object.fromEntries([ alert(prices.orange); // 2 ``` -We can use `Object.fromEntries` to get an plain object from `Map`. +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. @@ -214,7 +222,7 @@ let obj = Object.fromEntries(map.entries()); // make a plain object (*) alert(obj.orange); // 2 ``` -A call to `map.entries()` returns an array of key/value pairs, exactly in the right format for `Object.fromEntries`. +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 @@ -230,11 +238,11 @@ A `Set` is a special type collection - "set of values" (without keys), where eac Its main methods are: - `new Set(iterable)` -- creates the set, and if an `iterable` object is provided (usually an array), copies values from it into the set. -- `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. +- [`set.add(value)`](mdn:js/Set/add) -- adds a value, returns the set itself. +- [`set.delete(value)`](mdn:js/Set/delete) -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. +- [`set.has(value)`](mdn:js/Set/has) -- returns `true` if the value exists in the set, otherwise `false`. +- [`set.clear()`](mdn:js/Set/clear) -- removes everything from the set. +- [`set.size`](mdn:js/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. @@ -283,13 +291,13 @@ set.forEach((value, valueAgain, set) => { 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 may help to replace `Map` with `Set` in certain cases with ease, and vice versa. +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()` -- 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`. +- [`set.keys()`](mdn:js/Set/keys) -- returns an iterable object for values, +- [`set.values()`](mdn:js/Set/values) -- same as `set.keys()`, for compatibility with `Map`, +- [`set.entries()`](mdn:js/Set/entries) -- returns an iterable object for entries `[value, value]`, exists for compatibility with `Map`. ## Summary @@ -298,12 +306,12 @@ The same methods `Map` has for iterators are also supported: Methods and properties: - `new Map([iterable])` -- creates the map, with optional `iterable` (e.g. array) of `[key,value]` pairs for initialization. -- `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()` -- removes everything from the map. -- `map.size` -- returns the current element count. +- [`map.set(key, value)`](mdn:js/Map/set) -- stores the value by the key, returns the map itself. +- [`map.get(key)`](mdn:js/Map/get) -- returns the value by the key, `undefined` if `key` doesn't exist in map. +- [`map.has(key)`](mdn:js/Map/has) -- returns `true` if the `key` exists, `false` otherwise. +- [`map.delete(key)`](mdn:js/Map/delete) -- removes the value by the key, returns `true` if `key` existed at the moment of the call, otherwise `false`. +- [`map.clear()`](mdn:js/Map/clear) -- removes everything from the map. +- [`map.size`](mdn:js/Map/size) -- returns the current element count. The differences from a regular `Object`: @@ -315,10 +323,10 @@ The differences from a regular `Object`: Methods and properties: - `new Set([iterable])` -- creates the set, with optional `iterable` (e.g. array) of values for initialization. -- `set.add(value)` -- adds a value (does nothing if `value` exists), 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. +- [`set.add(value)`](mdn:js/Set/add) -- adds a value (does nothing if `value` exists), returns the set itself. +- [`set.delete(value)`](mdn:js/Set/delete) -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. +- [`set.has(value)`](mdn:js/Set/has) -- returns `true` if the value exists in the set, otherwise `false`. +- [`set.clear()`](mdn:js/Set/clear) -- removes everything from the set. +- [`set.size`](mdn:js/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-weakmap-weakset/01-recipients-read/solution.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md index f0c6ed45..e2147ccf 100644 --- 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 @@ -1,6 +1,6 @@ Let's store read messages in `WeakSet`: -```js +```js run let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, @@ -25,7 +25,7 @@ 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. +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. diff --git a/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md index c3d3bbc0..fd31a891 100644 --- a/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md +++ b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md @@ -16,7 +16,7 @@ let messages = [ 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. +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. diff --git a/1-js/05-data-types/08-weakmap-weakset/article.md b/1-js/05-data-types/08-weakmap-weakset/article.md index 11ff9d5e..8d5a8698 100644 --- a/1-js/05-data-types/08-weakmap-weakset/article.md +++ b/1-js/05-data-types/08-weakmap-weakset/article.md @@ -1,6 +1,6 @@ # WeakMap and WeakSet -As we know from the chapter , JavaScript engine stores a value in memory while it is reachable (and can potentially be used). +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 @@ -30,7 +30,8 @@ let array = [ john ]; john = null; // overwrite the reference *!* -// john is stored inside the array, so it won't be garbage-collected +// 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] */!* ``` @@ -59,7 +60,7 @@ Let's see what it means on examples. ## WeakMap -The first difference from `Map` is that `WeakMap` keys must be objects, not primitive values: +The first difference between `Map` and `WeakMap` is that keys must be objects, not primitive values: ```js run let weakMap = new WeakMap(); @@ -100,9 +101,9 @@ Compare it with the regular `Map` example above. Now if `john` only exists as th 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. +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 data structure? +Now, where do we need such a data structure? ## Use case: additional data @@ -141,13 +142,12 @@ And here's another part of the code, maybe another file using it: let john = { name: "John" }; countUser(john); // count his visits -countUser(john); // later john leaves us john = null; ``` -Now `john` object should be garbage collected, but remains in memory, as it's a key in `visitsCountMap`. +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. @@ -164,13 +164,13 @@ function countUser(user) { } ``` -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`. +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: when a function result should be remembered ("cached"), so that future calls on the same object reuse it. +Another common example is caching. We can store ("cache") results from a function, so that future calls on the same object can reuse it. -We can use `Map` to store results, like this: +To achieve that, we can use `Map` (not optimal scenario): ```js run // 📁 cache.js @@ -207,7 +207,7 @@ 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. +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 @@ -248,7 +248,7 @@ obj = null; - An object exists in the set while it is reachable from somewhere else. - Like `Set`, it supports `add`, `has` and `delete`, but not `size`, `keys()` and no iterations. -Being "weak", it also serves as an additional storage. But not for an arbitrary data, but rather for "yes/no" facts. A membership in `WeakSet` may mean something about the object. +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: @@ -276,7 +276,7 @@ john = null; // visitedSet will be cleaned automatically ``` -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 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. +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 @@ -284,6 +284,8 @@ The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterati `WeakSet` is `Set`-like collection that stores only objects and removes them once they become inaccessible by other means. -Both of them do not support methods and properties that refer to all keys or their count. Only individual operations are allowed. +Their main advantages are that they have weak reference to objects, so they can easily be removed by garbage collector. -`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, if it is only found as the key of `WeakMap` or in a `WeakSet`, it will be cleaned up automatically. +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-keys-values-entries/article.md b/1-js/05-data-types/09-keys-values-entries/article.md index ca0be768..bef678f5 100644 --- a/1-js/05-data-types/09-keys-values-entries/article.md +++ b/1-js/05-data-types/09-keys-values-entries/article.md @@ -11,7 +11,7 @@ They are supported for: - `Map` - `Set` -- `Array` (except `arr.values()`) +- `Array` Plain objects also support similar methods, but the syntax is a bit different. @@ -74,10 +74,10 @@ Usually that's convenient. But if we want symbolic keys too, then there's a sepa 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 `Object.fromEntries`: +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`. +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: @@ -91,14 +91,13 @@ let prices = { *!* let doublePrices = Object.fromEntries( - // convert to array, map, and then fromEntries gives back the object - Object.entries(prices).map(([key, value]) => [key, value * 2]) + // 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 from the first sight, but becomes easy to understand after you use it once or twice. - -We can make powerful one-liners for more complex transforms this way. It's only important to keep balance, so that the code is still simple enough to understand it. +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-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 index f4bd5c76..6538af42 100644 --- 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 @@ -1,16 +1,14 @@ function topSalary(salaries) { - let max = 0; + let maxSalary = 0; let maxName = null; for(const [name, salary] of Object.entries(salaries)) { - if (max < salary) { - max = salary; + if (maxSalary < salary) { + maxSalary = salary; maxName = name; } } return maxName; -} - - +} \ No newline at end of file diff --git a/1-js/05-data-types/10-destructuring-assignment/article.md b/1-js/05-data-types/10-destructuring-assignment/article.md index c2288847..41e36db2 100644 --- a/1-js/05-data-types/10-destructuring-assignment/article.md +++ b/1-js/05-data-types/10-destructuring-assignment/article.md @@ -2,19 +2,22 @@ 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, and arrays allow us to gather data items into an ordered collection. +- 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. -But when we pass those to a function, it may need not an object/array as a whole, but rather individual pieces. +Although, when we pass those to a function, it may need not be an object/array as a whole. It may need individual pieces. -*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 great with complex functions that have a lot of parameters, default values, and so on. +*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 great with complex functions that have a lot of parameters, default values, and so on. Soon we'll see that. ## Array destructuring -An example of how the array is destructured into variables: +Here's an example of how an array is destructured into variables: ```js // we have an array with the name and surname -let arr = ["Ilya", "Kantor"] +let arr = ["John", "Smith"] *!* // destructuring assignment @@ -23,18 +26,22 @@ let arr = ["Ilya", "Kantor"] let [firstName, surname] = arr; */!* -alert(firstName); // Ilya -alert(surname); // Kantor +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 -let [firstName, surname] = "Ilya Kantor".split(' '); +```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 better understand it. + ````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. @@ -69,26 +76,25 @@ In the code above, the second element of the array is skipped, the third one is 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" 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] = "Ilya Kantor".split(' '); +[user.name, user.surname] = "John Smith".split(' '); -alert(user.name); // Ilya +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 keys-and-values of an object: @@ -107,7 +113,7 @@ for (let [key, value] of Object.entries(user)) { } ``` -...And the same for a map: +The similar code for a `Map` is simpler, as it's iterable: ```js run let user = new Map(); @@ -115,35 +121,73 @@ 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 '...' -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 `"..."`: +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"*/!*]; -alert(name1); // Julius -alert(name2); // Caesar - *!* -// Note that type of `rest` is Array. +// rest is 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. +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 there are fewer values in the array than variables in the assignment, there will be no error. Absent values are considered undefined: +If the array is shorter than the list of variables at the left, there'll be no errors. Absent values are considered undefined: ```js run *!* @@ -168,7 +212,7 @@ 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: +For instance, here we use the `prompt` function for two defaults: ```js run // runs only prompt for surname @@ -178,7 +222,7 @@ alert(name); // Julius (from array) alert(surname); // whatever prompt gets ``` - +Please note: the `prompt` will run only for the missing value (`surname`). ## Object destructuring @@ -187,10 +231,10 @@ The destructuring assignment also works with objects. The basic syntax is: ```js -let {var1, var2} = {var1:…, var2…} +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 `{...}`. +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: @@ -210,7 +254,9 @@ 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: +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 {...} @@ -219,7 +265,7 @@ 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: +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 = { @@ -356,7 +402,7 @@ The problem is that JavaScript treats `{...}` in the main code flow (not inside } ``` -So here JavaScript assumes that we have a code block, that's why there's an error. We have destructuring instead. +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 `(...)`: @@ -374,7 +420,7 @@ alert( title ); // Menu If an object or an array contain 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 at the left side of the assignment has the same structure to extract values from them: +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 = { @@ -383,7 +429,7 @@ let options = { height: 200 }, items: ["Cake", "Donut"], - extra: true + extra: true }; // destructuring assignment split in multiple lines for clarity diff --git a/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg index 0b650020..8a1ff1a9 100644 --- a/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg +++ b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/05-data-types/11-date/1-new-date/solution.md b/1-js/05-data-types/11-date/1-new-date/solution.md index 9bb1d749..18286c33 100644 --- 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 @@ -2,7 +2,17 @@ The `new Date` constructor uses the local time zone. So the only important thing So February has number 1. +Here's an example with numbers as date components: + ```js run -let d = new Date(2012, 1, 20, 3, 12); -alert( d ); +//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/11-date/6-get-seconds-today/solution.md b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md index a483afe9..8f8e52b6 100644 --- 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 @@ -23,4 +23,6 @@ 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 index 3fbe1328..45679092 100644 --- 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 @@ -2,11 +2,11 @@ importance: 5 --- -# How many seconds has passed today? +# 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 `10:00 am`, and there was no daylight savings shift, then: +For instance, if now were `10:00 am`, and there was no daylight savings shift, then: ```js getSecondsToday() == 36000 // (3600 * 10) diff --git a/1-js/05-data-types/11-date/8-format-date-relative/solution.md b/1-js/05-data-types/11-date/8-format-date-relative/solution.md index 2507c840..37248568 100644 --- a/1-js/05-data-types/11-date/8-format-date-relative/solution.md +++ b/1-js/05-data-types/11-date/8-format-date-relative/solution.md @@ -40,7 +40,7 @@ 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 +// yesterday's date like 31.12.2016 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); ``` @@ -62,6 +62,8 @@ function formatDate(date) { year = year.toString().slice(-2); month = month < 10 ? '0' + month : month; dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth; + hour = hour < 10 ? '0' + hour : hour; + minutes = minutes < 10 ? '0' + minutes : minutes; if (diffSec < 1) { return 'right now'; 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 index 4dc06737..9651b305 100644 --- 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 @@ -20,6 +20,6 @@ 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 +// 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 index 4f80f752..2266c077 100644 --- a/1-js/05-data-types/11-date/article.md +++ b/1-js/05-data-types/11-date/article.md @@ -33,6 +33,13 @@ To create a new `Date` object call `new Date()` with one of the following argume 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. @@ -50,7 +57,7 @@ To create a new `Date` object call `new Date()` with one of the following argume `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` must have 4 digits: `2013` is okay, `98` is not. + - 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`. @@ -58,11 +65,11 @@ To create a new `Date` object call `new Date()` with one of the following argume For instance: ```js - new Date(2011, 0, 1, 0, 0, 0, 0); // // 1 Jan 2011, 00:00:00 + 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): + The maximal precision is 1 ms (1/1000 sec): ```js run let date = new Date(2011, 0, 1, 2, 3, 4, 567); @@ -117,7 +124,7 @@ Besides the given methods, there are two special ones that do not have a UTC-var : 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: +: Returns the difference between UTC and the local time zone, in minutes: ```js run // if you are in timezone UTC-1, outputs 60 @@ -341,7 +348,7 @@ let time1 = 0; let time2 = 0; *!* -// run bench(upperSlice) and bench(upperLoop) each 10 times alternating +// run bench(diffSubtract) and bench(diffGetTime) each 10 times alternating for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); @@ -381,7 +388,7 @@ 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. +- 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`. @@ -400,7 +407,7 @@ 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); +alert(date); ``` ## Summary @@ -420,7 +427,7 @@ Sometimes we need more precise time measurements. JavaScript itself does not hav 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 +// 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/12-json/article.md b/1-js/05-data-types/12-json/article.md index 2942cdd8..50374535 100644 --- a/1-js/05-data-types/12-json/article.md +++ b/1-js/05-data-types/12-json/article.md @@ -27,7 +27,7 @@ Luckily, there's no need to write the code to handle all this. The task has been ## 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. +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: @@ -41,7 +41,7 @@ let student = { age: 30, isAdmin: false, courses: ['html', 'css', 'js'], - wife: null + spouse: null }; *!* @@ -58,7 +58,7 @@ alert(json); "age": 30, "isAdmin": false, "courses": ["html", "css", "js"], - "wife": null + "spouse": null } */ */!* @@ -105,7 +105,7 @@ JSON is data-only language-independent specification, so some JavaScript-specifi Namely: - Function properties (methods). -- Symbolic properties. +- Symbolic keys and values. - Properties that store `undefined`. ```js run @@ -276,6 +276,7 @@ name: John name: Alice place: [object Object] number: 23 +occupiedBy: [object Object] */ ``` @@ -328,6 +329,8 @@ alert(JSON.stringify(user, null, 2)); */ ``` +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" @@ -425,9 +428,9 @@ alert( numbers[1] ); // 1 Or for nested objects: ```js run -let user = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; +let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; -user = JSON.parse(user); +let user = JSON.parse(userData); alert( user.friends[1] ); // 1 ``` diff --git a/1-js/05-data-types/12-json/json-meetup.svg b/1-js/05-data-types/12-json/json-meetup.svg index 5dbb4a9a..3fa32a26 100644 --- a/1-js/05-data-types/12-json/json-meetup.svg +++ b/1-js/05-data-types/12-json/json-meetup.svg @@ -1 +1 @@ -number: 23title: "Conference"...placeoccupiedByparticipants \ No newline at end of file +number: 23title: "Conference"...placeoccupiedByparticipants \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md index 3a281ef3..11667f94 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. Some engines support the "tail call" optimization: if a 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, so the engine doesn't need to remember its execution context. That removes the burden on memory, so counting `sumTo(100000)` becomes possible. 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. +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 59040a2b..09e511db 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.svg b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg index 224735b6..1313837f 100644 --- a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg @@ -1 +1 @@ -fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file +fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/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 4357ff20..0eb76ea1 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,7 +33,7 @@ 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". diff --git a/1-js/06-advanced-functions/01-recursion/article.md b/1-js/06-advanced-functions/01-recursion/article.md index 9d217653..5ae89447 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) ``` @@ -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: { @@ -344,7 +350,7 @@ 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,7 +372,7 @@ 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. @@ -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: @@ -528,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/linked-list-0.svg b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg index f95e1bfc..5d23c7a4 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-0.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg @@ -1 +1 @@ -value1nextvalue"new item"nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value1nextvalue"new item"nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg index 3828ec08..2f37449c 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg @@ -1 +1 @@ -value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.svg b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg index 3db14be2..6c307213 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-split.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg @@ -1 +1 @@ -value1nextvalue2nextvalue3nextvalue4nextnullnullsecondListlist \ No newline at end of file +value1nextvalue2nextvalue3nextvalue4nextnullnullsecondListlist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.svg b/1-js/06-advanced-functions/01-recursion/linked-list.svg index 6dd22939..c02744f3 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list.svg @@ -1 +1 @@ -value1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg index 8354cd15..2b970a04 100644 --- a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg +++ b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg @@ -1 +1 @@ -pow(x,n)xx * pow(x, n-1)n == 1 ?YesNorecursive call until n==1 \ No newline at end of file +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/recursive-salaries.svg b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg index 207271cb..bd874c5b 100644 --- a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg +++ b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md b/1-js/06-advanced-functions/02-rest-parameters-spread/article.md similarity index 73% rename from 1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md rename to 1-js/06-advanced-functions/02-rest-parameters-spread/article.md index a14f0fb7..c63fe70c 100644 --- a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md +++ b/1-js/06-advanced-functions/02-rest-parameters-spread/article.md @@ -1,4 +1,4 @@ -# Rest parameters and spread operator +# Rest parameters and spread syntax Many JavaScript built-in functions support an arbitrary number of arguments. @@ -122,7 +122,7 @@ As we remember, arrow functions don't have their own `this`. Now we know they do ```` -## Spread operator [#spread-operator] +## Spread syntax [#spread-syntax] We've just seen how to get an array from the list of parameters. @@ -148,7 +148,7 @@ 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. +*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. @@ -169,7 +169,7 @@ let arr2 = [8, 3, -8, 1]; alert( Math.max(...arr1, ...arr2) ); // 8 ``` -We can even combine the spread operator with normal values: +We can even combine the spread syntax with normal values: ```js run @@ -179,7 +179,7 @@ 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: +Also, the spread syntax can be used to merge arrays: ```js run let arr = [3, 5, 1]; @@ -192,9 +192,9 @@ 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. +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 operator to turn the string into array of characters: +For instance, here we use the spread syntax to turn the string into array of characters: ```js run let str = "Hello"; @@ -202,7 +202,7 @@ 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. +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]`. @@ -220,24 +220,75 @@ 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. +- 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 operator. +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 operator" and expands an array into a list. +- 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 operator is used to pass an array to functions that normally require a list of many 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. 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 00000000..7cbd85ab --- /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 00000000..81918977 --- /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 00000000..f8c7bd6a --- /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 00000000..7611d0ef --- /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 00000000..d83ecbe7 --- /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 00000000..9d99aa71 --- /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 00000000..f50c7dc2 --- /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 00000000..8dfd8bd6 --- /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 00000000..0a522132 --- /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 00000000..d12a385c --- /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/2-counter-object-independent/solution.md b/1-js/06-advanced-functions/03-closure/4-counter-object-independent/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md rename to 1-js/06-advanced-functions/03-closure/4-counter-object-independent/solution.md diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md b/1-js/06-advanced-functions/03-closure/4-counter-object-independent/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md rename to 1-js/06-advanced-functions/03-closure/4-counter-object-independent/task.md diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md rename to 1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md similarity index 93% rename from 1-js/06-advanced-functions/03-closure/3-function-in-if/task.md rename to 1-js/06-advanced-functions/03-closure/5-function-in-if/task.md index d02c53b9..4e386eec 100644 --- a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md +++ b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md @@ -1,4 +1,6 @@ +importance: 5 +--- # Function in if Look at the code. What will be the result of the call at the last line? diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md rename to 1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/4-closure-sum/task.md rename to 1-js/06-advanced-functions/03-closure/6-closure-sum/task.md diff --git a/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md b/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md new file mode 100644 index 00000000..b16b3529 --- /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 00000000..fb7445e6 --- /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 bd57085e..00000000 --- a/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md +++ /dev/null @@ -1,22 +0,0 @@ - - -```js run -let users = [ - { name: "John", age: 20, surname: "Johnson" }, - { name: "Pete", age: 18, surname: "Peterson" }, - { name: "Ann", age: 19, surname: "Hathaway" } -]; - -*!* -function byField(field) { - return (a, b) => a[field] > b[field] ? 1 : -1; -} -*/!* - -users.sort(byField('name')); -users.forEach(user => alert(user.name)); // Ann, John, Pete - -users.sort(byField('age')); -users.forEach(user => alert(user.name)); // Pete, Ann, John -``` - diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/solution.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/solution.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/source.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/source.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/test.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/test.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg deleted file mode 100644 index 5549a6a0..00000000 --- a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg +++ /dev/null @@ -1 +0,0 @@ -outeri: 0i: 1i: 2i: 10...makeArmy() LexicalEnvironmentfor block LexicalEnvironment \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md b/1-js/06-advanced-functions/03-closure/8-make-army/solution.md deleted file mode 100644 index 0fb0b4a4..00000000 --- a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md +++ /dev/null @@ -1,120 +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`. - -We can fix it by moving the variable definition into the loop: - -```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 -``` - -Now it works correctly, because every time the code block in `for (let i=0...) {...}` is executed, a new Lexical Environment is created for it, with the corresponding variable `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. That's why now it works. - -![](lexenv-makearmy.svg) - -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 ede8fd04..00000000 --- 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 00000000..8a71c869 --- /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 00000000..23b43383 --- /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 00000000..802f28c4 --- /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 00000000..8b137891 --- /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 a39dfc95..cb43a796 100644 --- a/1-js/06-advanced-functions/03-closure/article.md +++ b/1-js/06-advanced-functions/03-closure/article.md @@ -1,203 +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 dynamically, 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. +- In this article we'll use `let` variables in examples. +- Variables, declared with `const`, behave the same, so this article is about `const` too. +- The old `var` has some notable differences, they will be covered in the article . +``` -1. The function `sayHi` uses an external variable `name`. When the function runs, which value is it going to use? +## Code blocks - ```js - let name = "John"; +If a variable is declared inside a code block `{...}`, it's only visible inside that block. - 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 internal (hidden) associated object known as the *Lexical Environment*. - -The Lexical Environment object consists of two parts: - -1. *Environment Record* -- an object that stores all local variables as its properties (and some other information like the value of `this`). -2. A reference to the *outer lexical environment*, the one associated with the outer code. - -**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 that object".** - -For instance, in this simple code, there is only one Lexical Environment: - -![lexical environment](lexical-environment-global.svg) - -This is a so-called global Lexical Environment, associated with the whole script. - -On the picture above, the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical Environment has no outer reference, so it points to `null`. - -And that's how it changes when a variable is defined and assigned: - -![lexical environment](lexical-environment-global-2.svg) - -Rectangles on the right-hand side demonstrate how the global Lexical Environment changes during the execution: - -1. When the script starts, the Lexical Environment is empty. -2. The `let phrase` definition appears. It has been assigned no value, so `undefined` is stored. -3. `phrase` is assigned a value. -4. `phrase` changes value. - -Everything looks simple for now, right? - -To summarize: - -- A variable is a property of a special internal object, associated with the currently executing block/function/script. -- Working with variables is actually working with the properties of that object. - -### Function Declaration - -Till now, we only observed variables. Now enter Function Declarations. - -**Unlike `let` variables, they are fully initialized not when the execution reaches them, but earlier, when a Lexical Environment is created.** - -For top-level functions, it means the moment when the script is started. - -That is why we can call a function declaration before it is defined. - -The code below demonstrates that the Lexical Environment is non-empty from the beginning. It has `say`, because that's a Function Declaration. And later it gets `phrase`, declared with `let`: - -![lexical environment](lexical-environment-global-3.svg) - - -### Inner and outer Lexical Environment - -Now let's go on and explore what happens when a function accesses an outer variable. - -During the call, `say()` uses the outer variable `phrase`, let's look at the details of what's going on. - -When a function runs, a new Lexical Environment is created automatically to store local variables and parameters of the call. - -For instance, for `say("John")`, it looks like this (the execution is at the line, labelled with an arrow): - - - -![lexical environment](lexical-environment-simple.svg) - -So, during the function call we have two Lexical Environments: the inner one (for the function call) and the outer one (global): - -- The inner Lexical Environment corresponds to the current execution of `say`. - - It has a single property: `name`, the function argument. We called `say("John")`, so the value of `name` is `"John"`. -- The outer Lexical Environment is the global Lexical Environment. - - It has `phrase` variable and the function itself. - -The inner Lexical Environment has a reference to the `outer` one. - -**When the code wants to access a variable -- the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.** - -If a variable is not found anywhere, that's an error in strict mode (without `use strict`, an assignment to a non-existing variable, like `user = "John"` creates a new global variable `user`, that's for backwards compatibility). - -Let's see how the search proceeds in our example: - -- When the `alert` inside `say` wants to access `name`, it finds it immediately in the function Lexical Environment. -- When it wants to access `phrase`, then there is no `phrase` locally, so it follows the reference to the enclosing Lexical Environment and finds it there. - -![lexical environment lookup](lexical-environment-simple-lookup.svg) - -Now we can give the answer to the first question from the beginning of the chapter. - -**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. - -So the answer to the first question is `Pete`: +For example: ```js run -let name = "John"; +{ + // do some job with local variables that should not be seen outside -function sayHi() { - alert("Hi, " + name); + let message = "Hello"; // only visible in this block + + alert(message); // Hello } -name = "Pete"; // (*) +alert(message); // Error: message is not defined +``` +We can use this to isolate a piece of code that does its own task, with variables that only belong to it: + +```js run +{ + // show message + let message = "Hello"; + alert(message); +} + +{ + // show another message + let message = "Goodbye"; + alert(message); +} +``` + +````smart header="There'd be an error without blocks" +Please note, without separate blocks there would be an error, if we use `let` with the existing variable name: + +```js run +// show message +let message = "Hello"; +alert(message); + +// show another message *!* -sayHi(); // Pete +let message = "Goodbye"; // Error: variable already declared */!* +alert(message); +``` +```` + +For `if`, `for`, `while` and so on, variables declared in `{...}` are also only visible inside: + +```js run +if (true) { + let phrase = "Hello!"; + + alert(phrase); // Hello! +} + +alert(phrase); // Error, no such variable! ``` +Here, after `if` finishes, the `alert` below won't see the `phrase`, hence the error. -The execution flow of the code above: +That's great, as it allows us to create block-local variables, specific to an `if` branch. -1. The global Lexical Environment has `name: "John"`. -2. At the line `(*)` the global variable is changed, now it has `name: "Pete"`. -3. When the function `sayHi()`, is executed and takes `name` from outside. Here that's from the global Lexical Environment where it's already `"Pete"`. +The similar thing holds true for `for` and `while` loops: +```js run +for (let i = 0; i < 3; i++) { + // the variable i is only visible inside this for + alert(i); // 0, then 1, then 2 +} -```smart header="One call -- one Lexical Environment" -Please note that a new function Lexical Environment is created each time a function runs. - -And if a function is called multiple times, then each invocation will have its own Lexical Environment, with local variables and parameters specific for that very run. -``` - -```smart header="Lexical Environment is a specification object" -"Lexical Environment" is a specification object: it only exists "theoretically" in the [language specification](https://tc39.es/ecma262/#sec-lexical-environments) to describe how things work. We can't get this object in our code and manipulate it directly. JavaScript engines also may optimize it, discard variables that are unused to save memory and perform other internal tricks, as long as the visible behavior remains as described. +alert(i); // Error, no such variable ``` +Visually, `let i` is outside of `{...}`. But the `for` construct is special here: the variable, declared inside it, is considered a part of the block. ## Nested functions @@ -223,32 +122,16 @@ function sayHiBye(firstName, lastName) { Here the *nested* function `getFullName()` is made for convenience. It can access the outer variables and so can return the full name. Nested functions are quite common in JavaScript. -What's much more interesting, a nested function can be returned: either as a property of a new object (if the outer function creates an object with methods) or as a result by itself. It can then be used somewhere else. No matter where, it still has access to the same outer variables. +What's much more interesting, a nested function can be returned: either as a property of a new object or as a result by itself. It can then be used somewhere else. No matter where, it still has access to the same outer variables. -For instance, here the nested function is assigned to the new object by the [constructor function](info:constructor-new): - -```js run -// constructor function returns a new object -function User(name) { - - // the object method is created as a nested function - this.sayHi = function() { - alert(name); - }; -} - -let user = new User("John"); -user.sayHi(); // the method "sayHi" code has access to the outer "name" -``` - -And here we just create and return a "counting" function: +Below, `makeCounter` creates the "counter" function that returns the next number on each invocation: ```js run function makeCounter() { let count = 0; return function() { - return count++; // has access to the outer "count" + return count++; }; } @@ -259,316 +142,198 @@ alert( counter() ); // 1 alert( counter() ); // 2 ``` -Let's go on with the `makeCounter` example. It creates the "counter" function that returns the next number on each invocation. Despite being simple, slightly modified variants of that code have practical uses, for instance, as a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator), and more. +Despite being simple, slightly modified variants of that code have practical uses, for instance, as a [random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) to generate random values for automated tests. -How does the counter work internally? +How does this work? If we create multiple counters, will they be independent? What's going on with the variables here? -When the inner function runs, the variable in `count++` is searched from inside out. For the example above, the order will be: +Understanding such things is great for the overall knowledge of JavaScript and beneficial for more complex scenarios. So let's go a bit in-depth. -![](lexical-search-order.svg) +## Lexical Environment -1. The locals of the nested function... -2. The variables of the outer function... -3. And so on until it reaches global variables. +```warn header="Here be dragons!" +The in-depth technical explanation lies ahead. -In this example `count` is found on step `2`. When an outer variable is modified, it's changed where it's found. So `count++` finds the outer variable and increases it in the Lexical Environment where it belongs. Like if we had `let count = 1`. +As far as I'd like to avoid low-level language details, any understanding without them would be lacking and incomplete, so get ready. +``` -Here are two questions to consider: +For clarity, the explanation is split into multiple steps. -1. Can we somehow reset the counter `count` from the code that doesn't belong to `makeCounter`? E.g. after `alert` calls in the example above. -2. If we call `makeCounter()` multiple times -- it returns many `counter` functions. Are they independent or do they share the same `count`? +### Step 1. Variables -Try to answer them before you continue reading. +In JavaScript, every running function, code block `{...}`, and the script as a whole have an internal (hidden) associated object known as the *Lexical Environment*. -... +The Lexical Environment object consists of two parts: -All done? +1. *Environment Record* -- an object that stores all local variables as its properties (and some other information like the value of `this`). +2. A reference to the *outer lexical environment*, the one associated with the outer code. -Okay, let's go over the answers. +**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 that object".** -1. There is no way: `count` is a local function variable, we can't access it from the outside. -2. For every call to `makeCounter()` a new function Lexical Environment is created, with its own `count`. So the resulting `counter` functions are independent. +In this simple code without functions, there is only one Lexical Environment: -Here's the demo: +![lexical environment](lexical-environment-global.svg) -```js run +This is the so-called *global* Lexical Environment, associated with the whole script. + +On the picture above, the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical Environment has no outer reference, that's why the arrow points to `null`. + +As the code starts executing and goes on, the Lexical Environment changes. + +Here's a little bit longer code: + +![lexical environment](closure-variable-phrase.svg) + +Rectangles on the right-hand side demonstrate how the global Lexical Environment changes during the execution: + +1. When the script starts, the Lexical Environment is pre-populated with all declared variables. + - Initially, they are in the "Uninitialized" state. That's a special internal state, it means that the engine knows about the variable, but it cannot be referenced until it has been declared with `let`. It's almost the same as if the variable didn't exist. +2. Then `let phrase` definition appears. There's no assignment yet, so its value is `undefined`. We can use the variable from this point forward. +3. `phrase` is assigned a value. +4. `phrase` changes the value. + +Everything looks simple for now, right? + +- A variable is a property of a special internal object, associated with the currently executing block/function/script. +- Working with variables is actually working with the properties of that object. + +```smart header="Lexical Environment is a specification object" +"Lexical Environment" is a specification object: it only exists "theoretically" in the [language specification](https://tc39.es/ecma262/#sec-lexical-environments) to describe how things work. We can't get this object in our code and manipulate it directly. + +JavaScript engines also may optimize it, discard variables that are unused to save memory and perform other internal tricks, as long as the visible behavior remains as described. +``` + +### Step 2. Function Declarations + +A function is also a value, like a variable. + +**The difference is that a Function Declaration is instantly fully initialized.** + +When a Lexical Environment is created, a Function Declaration immediately becomes a ready-to-use function (unlike `let`, that is unusable till the declaration). + +That's why we can use a function, declared as Function Declaration, even before the declaration itself. + +For example, here's the initial state of the global Lexical Environment when we add a function: + +![](closure-function-declaration.svg) + +Naturally, this behavior only applies to Function Declarations, not Function Expressions where we assign a function to a variable, such as `let say = function(name)...`. + +### Step 3. Inner and outer Lexical Environment + +When a function runs, at the beginning of the call, a new Lexical Environment is created automatically to store local variables and parameters of the call. + +For instance, for `say("John")`, it looks like this (the execution is at the line, labelled with an arrow): + + + +![](lexical-environment-simple.svg) + +During the function call we have two Lexical Environments: the inner one (for the function call) and the outer one (global): + +- The inner Lexical Environment corresponds to the current execution of `say`. It has a single property: `name`, the function argument. We called `say("John")`, so the value of the `name` is `"John"`. +- The outer Lexical Environment is the global Lexical Environment. It has the `phrase` variable and the function itself. + +The inner Lexical Environment has a reference to the `outer` one. + +**When the code wants to access a variable -- the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.** + +If a variable is not found anywhere, that's an error in strict mode (without `use strict`, an assignment to a non-existing variable creates a new global variable, for compatibility with old code). + +In this example the search proceeds as follows: + +- For the `name` variable, the `alert` inside `say` finds it immediately in the inner Lexical Environment. +- When it wants to access `phrase`, then there is no `phrase` locally, so it follows the reference to the outer Lexical Environment and finds it there. + +![lexical environment lookup](lexical-environment-simple-lookup.svg) + + +### Step 4. Returning a function + +Let's return to the `makeCounter` example. + +```js function makeCounter() { let count = 0; + return function() { return count++; }; } -let counter1 = makeCounter(); -let counter2 = makeCounter(); - -alert( counter1() ); // 0 -alert( counter1() ); // 1 - -alert( counter2() ); // 0 (independent) +let counter = makeCounter(); ``` +At the beginning of each `makeCounter()` call, a new Lexical Environment object is created, to store variables for this `makeCounter` run. -Hopefully, the situation with outer variables is clear now. For most situations such understanding is enough. There are few details in the specification that we omitted for brevity. So in the next section we cover even more details. +So we have two nested Lexical Environments, just like in the example above: -## Environments in detail +![](closure-makecounter.svg) -Here's what's going on in the `makeCounter` example step-by-step, follow it to make sure that you understand how it works in detail. +What's different is that, during the execution of `makeCounter()`, a tiny nested function is created of only one line: `return count++`. We don't run it yet, only create. -Please note the additional `[[Environment]]` property is covered here. We didn't mention it before for simplicity. +All functions remember the Lexical Environment in which they were made. Technically, there's no magic here: all functions have the hidden property named `[[Environment]]`, that keeps the reference to the Lexical Environment where the function was created: -1. When the script has just started, there is only global Lexical Environment: +![](closure-makecounter-environment.svg) - ![](lexenv-nested-makecounter-1.svg) +So, `counter.[[Environment]]` has the reference to `{count: 0}` Lexical Environment. That's how the function remembers where it was created, no matter where it's called. The `[[Environment]]` reference is set once and forever at function creation time. - At that starting moment there is only `makeCounter` function, because it's a Function Declaration. It did not run yet. +Later, when `counter()` is called, a new Lexical Environment is created for the call, and its outer Lexical Environment reference is taken from `counter.[[Environment]]`: - **All functions "on birth" receive a hidden property `[[Environment]]` with a reference to the Lexical Environment of their creation.** +![](closure-makecounter-nested-call.svg) - We didn't talk about it yet, that's how the function knows where it was made. +Now when the code inside `counter()` looks for `count` variable, it first searches its own Lexical Environment (empty, as there are no local variables there), then the Lexical Environment of the outer `makeCounter()` call, where it finds and changes it. - Here, `makeCounter` is created in the global Lexical Environment, so `[[Environment]]` keeps a reference to it. +**A variable is updated in the Lexical Environment where it lives.** - In other words, a function is "imprinted" with a reference to the Lexical Environment where it was born. And `[[Environment]]` is the hidden function property that has that reference. +Here's the state after the execution: -2. The code runs on, the new global variable `counter` is declared and gets the result of `makeCounter()` call. Here's a snapshot of the moment when the execution is on the first line inside `makeCounter()`: +![](closure-makecounter-nested-call-2.svg) - ![](lexenv-nested-makecounter-2.svg) +If we call `counter()` multiple times, the `count` variable will be increased to `2`, `3` and so on, at the same place. - At the moment of the call of `makeCounter()`, the Lexical Environment is created, to hold its variables and arguments. - - As all Lexical Environments, it stores two things: - 1. An Environment Record with local variables. In our case `count` is the only local variable (appearing when the line with `let count` is executed). - 2. The outer lexical reference, which is set to the value of `[[Environment]]` of the function. Here `[[Environment]]` of `makeCounter` references the global Lexical Environment. - - So, now we have two Lexical Environments: the first one is global, the second one is for the current `makeCounter` call, with the outer reference to global. - -3. During the execution of `makeCounter()`, a tiny nested function is created. - - It doesn't matter whether the function is created using Function Declaration or Function Expression. All functions get the `[[Environment]]` property that references the Lexical Environment in which they were made. So our new tiny nested function gets it as well. - - For our new nested function the value of `[[Environment]]` is the current Lexical Environment of `makeCounter()` (where it was born): - - ![](lexenv-nested-makecounter-3.svg) - - Please note that on this step the inner function was created, but not yet called. The code inside `function() { return count++; }` is not running. - -4. As the execution goes on, the call to `makeCounter()` finishes, and the result (the tiny nested function) is assigned to the global variable `counter`: - - ![](lexenv-nested-makecounter-4.svg) - - That function has only one line: `return count++`, that will be executed when we run it. - -5. When `counter()` is called, a new Lexical Environment is created for the call. It's empty, as `counter` has no local variables by itself. But the `[[Environment]]` of `counter` is used as the `outer` reference for it, that provides access to the variables of the former `makeCounter()` call where it was created: - - ![](lexenv-nested-makecounter-5.svg) - - Now when the call looks for `count` variable, it first searches its own Lexical Environment (empty), then the Lexical Environment of the outer `makeCounter()` call, where finds it. - - Please note how memory management works here. Although `makeCounter()` call finished some time ago, its Lexical Environment was retained in memory, because there's a nested function with `[[Environment]]` referencing it. - - Generally, a Lexical Environment object lives as long as there is a function which may use it. And only when there are none remaining, it is cleared. - -6. The call to `counter()` not only returns the value of `count`, but also increases it. Note that the modification is done "in place". The value of `count` is modified exactly in the environment where it was found. - - ![](lexenv-nested-makecounter-6.svg) - -7. Next `counter()` invocations do the same. - -The answer to the second question from the beginning of the chapter should now be obvious. - -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 it would be `"John"`. - -```smart header="Closures" +```smart header="Closure" There is a general programming term "closure", that developers generally should know. -A [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) is a function that remembers its outer variables and can access them. In some languages, that's not possible, or a function should be written in a special way to make it happen. But as explained above, in JavaScript, all functions are naturally closures (there is only one exclusion, to be covered in ). +A [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) is a function that remembers its outer variables and can access them. In some languages, that's not possible, or a function should be written in a special way to make it happen. But as explained above, in JavaScript, all functions are naturally closures (there is only one exception, to be covered in ). -That is: they automatically remember where they were created using a hidden `[[Environment]]` property, and all of them can access outer variables. +That is: they automatically remember where they were created using a hidden `[[Environment]]` property, and then their code can access outer variables. -When on an interview, a frontend developer gets a question about "what's a closure?", a valid answer would be a definition of the closure and an explanation that all functions in JavaScript are closures, and maybe few more words about technical details: the `[[Environment]]` property and how Lexical Environments work. +When on an interview, a frontend developer gets a question about "what's a closure?", a valid answer would be a definition of the closure and an explanation that all functions in JavaScript are closures, and maybe a few more words about technical details: the `[[Environment]]` property and how Lexical Environments work. ``` -## Code blocks and loops, IIFE - -The examples above concentrated on functions. But a Lexical Environment exists for any code block `{...}`. - -A Lexical Environment is created when a code block runs and contains block-local variables. Here are a couple of examples. - -### If - -In the example below, the `user` variable exists only in the `if` block: - - - -![](lexenv-if.svg) - -When the execution gets into the `if` block, the new "if-only" Lexical Environment is created for it. - -It has the reference to the outer one, so `phrase` can be found. But all variables and Function Expressions, declared inside `if`, reside in that Lexical Environment and can't be seen from the outside. - -For instance, after `if` finishes, the `alert` below won't see the `user`, hence the error. - -### For, while - -For a loop, every iteration has a separate Lexical Environment. If a variable is declared in `for(let ...)`, then it's also in there: - -```js run -for (let i = 0; i < 10; i++) { - // Each loop has its own Lexical Environment - // {i: value} -} - -alert(i); // Error, no such variable -``` - -Please note: `let i` is visually outside of `{...}`. The `for` construct is special here: each iteration of the loop has its own Lexical Environment with the current `i` in it. - -Again, similarly to `if`, after the loop `i` is not visible. - -### Code blocks - -We also can use a "bare" code block `{…}` to isolate variables into a "local scope". - -For instance, in a web browser all scripts (except with `type="module"`) share the same global area. So if we create a global variable in one script, it becomes available to others. But that becomes a source of conflicts if two scripts use the same variable name and overwrite each other. - -That may happen if the variable name is a widespread word, and script authors are unaware of each other. - -If we'd like to avoid that, we can use a code block to isolate the whole script or a part of it: - -```js run -{ - // do some job with local variables that should not be seen outside - - let message = "Hello"; - - alert(message); // Hello -} - -alert(message); // Error: message is not defined -``` - -The code outside of the block (or inside another script) doesn't see variables inside the block, because the block has its own Lexical Environment. - -### IIFE - -In the past, there were no block-level lexical environment in JavaScript. - -So programmers had to invent something. And what they did is called "immediately-invoked function expressions" (abbreviated as IIFE). - -That's not a thing we should use nowadays, but you can find them in old scripts, so it's better to understand them. - -IIFE looks like this: - -```js run -(function() { - - let message = "Hello"; - - alert(message); // Hello - -})(); -``` - -Here a Function Expression is created and immediately called. So the code executes right away and has its own private variables. - -The Function Expression is wrapped with parenthesis `(function {...})`, because when JavaScript meets `"function"` in the main code flow, it understands it as the start of a Function Declaration. But a Function Declaration must have a name, so this kind of code will give an error: - -```js run -// Try to declare and immediately call a function -function() { // <-- Error: Unexpected token ( - - let message = "Hello"; - - alert(message); // Hello - -}(); -``` - -Even if we say: "okay, let's add a name", that won't work, as JavaScript does not allow Function Declarations to be called immediately: - -```js run -// syntax error because of parentheses below -function go() { - -}(); // <-- can't call Function Declaration immediately -``` - -So, parentheses around the function is a trick to show JavaScript that the function is created in the context of another expression, and hence it's a Function Expression: it needs no name and can be called immediately. - -There exist other ways besides parentheses to tell JavaScript that we mean a Function Expression: - -```js run -// Ways to create IIFE - -(function() { - alert("Parentheses around the function"); -}*!*)*/!*(); - -(function() { - alert("Parentheses around the whole thing"); -}()*!*)*/!*; - -*!*!*/!*function() { - alert("Bitwise NOT operator starts the expression"); -}(); - -*!*+*/!*function() { - alert("Unary plus starts the expression"); -}(); -``` - -In all the above cases we declare a Function Expression and run it immediately. Let's note again: nowadays there's no reason to write such code. - ## Garbage collection -Usually, a Lexical Environment is cleaned up and deleted after the function run. For instance: +Usually, a Lexical Environment is removed from memory with all the variables after the function call finishes. That's because there are no references to it. As any JavaScript object, it's only kept in memory while it's reachable. -```js -function f() { - let value1 = 123; - let value2 = 456; -} +However, if there's a nested function that is still reachable after the end of a function, then it has `[[Environment]]` property that references the lexical environment. -f(); -``` +In that case the Lexical Environment is still reachable even after the completion of the function, so it stays alive. -Here two values are technically the properties of the Lexical Environment. But after `f()` finishes that Lexical Environment becomes unreachable, so it's deleted from the memory. - -...But if there's a nested function that is still reachable after the end of `f`, then it has `[[Environment]]` property that references the outer lexical environment, so it's also reachable and alive: +For example: ```js function f() { let value = 123; - function g() { alert(value); } - -*!* - return g; -*/!* + return function() { + alert(value); + } } -let g = f(); // g is reachable, and keeps the outer lexical environment in memory +let g = f(); // g.[[Environment]] stores a reference to the Lexical Environment +// of the corresponding f() call ``` -Please note that if `f()` is called many times, and resulting functions are saved, then all corresponding Lexical Environment objects will also be retained in memory. All 3 of them in the code below: +Please note that if `f()` is called many times, and resulting functions are saved, then all corresponding Lexical Environment objects will also be retained in memory. In the code below, all 3 of them: ```js function f() { @@ -584,19 +349,18 @@ let arr = [f(), f(), f()]; A Lexical Environment object dies when it becomes unreachable (just like any other object). In other words, it exists only while there's at least one nested function referencing it. -In the code below, after `g` becomes unreachable, enclosing Lexical Environment (and hence the `value`) is cleaned from memory; +In the code below, after the nested function is removed, its enclosing Lexical Environment (and hence the `value`) is cleaned from memory: ```js function f() { let value = 123; - function g() { alert(value); } - - return g; + return function() { + alert(value); + } } -let g = f(); // while g is alive -// their corresponding Lexical Environment lives +let g = f(); // while g function exists, the value stays in memory g = null; // ...and now the memory is cleaned up ``` @@ -607,7 +371,7 @@ As we've seen, in theory while a function is alive, all outer variables are also But in practice, JavaScript engines try to optimize that. They analyze variable usage and if it's obvious from the code that an outer variable is not used -- it is removed. -**An important side effect in V8 (Chrome, Opera) is that such variable will become unavailable in debugging.** +**An important side effect in V8 (Chrome, Edge, Opera) is that such variable will become unavailable in debugging.** Try running the example below in Chrome with the Developer Tools open. @@ -649,9 +413,6 @@ let g = f(); g(); ``` -```warn header="See ya!" -This feature of V8 is good to know. If you are debugging with Chrome/Opera, sooner or later you will meet it. +This feature of V8 is good to know. If you are debugging with Chrome/Edge/Opera, sooner or later you will meet it. -That is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be changed sometime. -You always can check for it by running the examples on this page. -``` +That is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be changed sometime. You can always check for it by running the examples on this page. diff --git a/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg b/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg new file mode 100644 index 00000000..3ef78787 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg @@ -0,0 +1 @@ +outernullexecution startphrase: <uninitialized> say: function... \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-environment.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-environment.svg new file mode 100644 index 00000000..f7844171 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-environment.svg @@ -0,0 +1 @@ +null[[Environment]]makeCounter: function counter: undefinedcount: 0outerouter \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg new file mode 100644 index 00000000..3950a8fa --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg @@ -0,0 +1 @@ +count: 1<empty>nullouterouteroutermakeCounter: function counter: functionmodified here \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg new file mode 100644 index 00000000..24315bf2 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg @@ -0,0 +1 @@ +count: 0<empty>nullouterouteroutermakeCounter: function counter: function \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter.svg new file mode 100644 index 00000000..2ca06455 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter.svg @@ -0,0 +1 @@ +makeCounter: function counter: undefinedcount: 0nullglobal LexicalEnvironmentLexicalEnvironment of makeCounter() callouterouter \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg b/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg new file mode 100644 index 00000000..b9bb12ff --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg @@ -0,0 +1 @@ +phrase: "Bye"phrase: "Hello"phrase: undefinedphrase: <uninitialized>outernullexecution start \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-if.svg b/1-js/06-advanced-functions/03-closure/lexenv-if.svg index e2b8d6f6..3d4d6d7c 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-if.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-if.svg @@ -1 +1 @@ -phrase: "Hello"outerouternulluser: "John" \ No newline at end of file +phrase: "Hello"outerouternulluser: "John" \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg index 70649ffd..f15e77a8 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-1.svg @@ -1 +1 @@ -makeCounter: function[[Environment]]outernull \ No newline at end of file +makeCounter: function[[Environment]]outernull \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg index b6a8bd48..f3748853 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg @@ -1 +1 @@ -makeCounter: functioncounter: undefinedcount: 0outerouternullglobal LexicalEnvironmentLexicalEnvironment of makeCounter() call \ No newline at end of file +makeCounter: functioncounter: undefinedcount: 0outerouternullglobal LexicalEnvironmentLexicalEnvironment of makeCounter() call \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg index aa0eee4e..54f1d97b 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg @@ -1 +1 @@ -makeCounter: functioncounter: undefinedcount: 0outerouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: undefinedcount: 0outerouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg index 216658f5..fb60a785 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 0outerouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: functioncount: 0outerouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg index eff2c7dd..79c440da 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 0<empty>outerouterouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: functioncount: 0<empty>outerouterouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg index 4eb3a6e3..06d5b506 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 1outerouternull[[Environment]]modified here \ No newline at end of file +makeCounter: functioncounter: functioncount: 1outerouternull[[Environment]]modified here \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg deleted file mode 100644 index ae115aa2..00000000 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg +++ /dev/null @@ -1 +0,0 @@ -makeWorker: function name: "John"<empty>outerouterouternullname: "Pete" \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg index f077d5a8..b6e576f0 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global-2.svg @@ -1 +1 @@ -phrase: "Bye"phrase: "Hello"phrase: undefined<empty>outernullexecution start \ No newline at end of file +phrase: "Bye"phrase: "Hello"phrase: undefined<empty>outernullexecution start \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg index 4eaa7de7..1942a7e3 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global-3.svg @@ -1 +1 @@ -say: function phrase: "Hello"say: functionouternullexecution start \ No newline at end of file +say: function phrase: "Hello"say: functionouternullexecution start \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg index f974122f..7bddc223 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg @@ -1 +1 @@ -phrase: "Hello"outernullLexicalEnvironment \ No newline at end of file +phrase: "Hello"outernullLexical Environment \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg index 242f970e..79501a5b 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg @@ -1 +1 @@ -say: function phrase: "Hello"name: "John"outerouternull \ No newline at end of file +say: function phrase: "Hello"name: "John"outerouternull \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg index c351f9d7..dea6ac46 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg @@ -1 +1 @@ -say: function phrase: "Hello"name: "John"outerouternullLexicalEnvironment for the call \ No newline at end of file +say: function phrase: "Hello"name: "John"outerouternullLexical Environment of the call \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-search-order.svg b/1-js/06-advanced-functions/03-closure/lexical-search-order.svg index 4b960daa..d9884ec4 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-search-order.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-search-order.svg @@ -1 +1 @@ -123 \ No newline at end of file +123 \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/variable-scope-lookup.svg b/1-js/06-advanced-functions/03-closure/variable-scope-lookup.svg new file mode 100644 index 00000000..f1f1d3b1 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/variable-scope-lookup.svg @@ -0,0 +1 @@ +functionUser(name){this.sayHi=function(){alert(name);};}letuser=newUser("John");user.sayHi(); \ No newline at end of file diff --git a/1-js/06-advanced-functions/04-var/article.md b/1-js/06-advanced-functions/04-var/article.md index 02fd43fe..1579afb6 100644 --- a/1-js/06-advanced-functions/04-var/article.md +++ b/1-js/06-advanced-functions/04-var/article.md @@ -1,37 +1,34 @@ # The old "var" +```smart header="This article is for understanding old scripts" +The information in this article is useful for understanding old scripts. + +That's not how we write new code. +``` + In the very first chapter about [variables](info:variables), we mentioned three ways of variable declaration: 1. `let` 2. `const` 3. `var` -`let` and `const` behave exactly the same way in terms of Lexical Environments. - -But `var` is a very different beast, that originates from very old times. It's generally not used in modern scripts, but still lurks in the old ones. - -If you don't plan on meeting such scripts you may even skip this chapter or postpone it, but then there's a chance that it bites you later. - -From the first sight, `var` behaves similar to `let`. That is, declares a variable: +The `var` declaration is similar to `let`. Most of the time we can replace `let` by `var` or vice-versa and expect things to work: ```js run -function sayHi() { - var phrase = "Hello"; // local variable, "var" instead of "let" - - alert(phrase); // Hello -} - -sayHi(); - -alert(phrase); // Error, phrase is not defined +var message = "Hi"; +alert(message); // Hi ``` -...But here are the differences. +But internally `var` is a very different beast, that originates from very old times. It's generally not used in modern scripts, but still lurks in the old ones. + +If you don't plan on meeting such scripts you may even skip this chapter or postpone it. + +On the other hand, it's important to understand differences when migrating old scripts from `var` to `let`, to avoid odd errors. ## "var" has no block scope -Variables, declared with `var`, are either function-wide or global. They are visible through blocks. +Variables, declared with `var`, are either function-scoped or global-scoped. They are visible through blocks. For instance: @@ -55,7 +52,7 @@ if (true) { } *!* -alert(test); // Error: test is not defined +alert(test); // ReferenceError: test is not defined */!* ``` @@ -63,11 +60,13 @@ The same thing for loops: `var` cannot be block- or loop-local: ```js for (var i = 0; i < 10; i++) { + var one = 1; // ... } *!* -alert(i); // 10, "i" is visible after loop, it's a global variable +alert(i); // 10, "i" is visible after loop, it's a global variable +alert(one); // 1, "one" is visible after loop, it's a global variable */!* ``` @@ -83,12 +82,32 @@ function sayHi() { } sayHi(); -alert(phrase); // Error: phrase is not defined (Check the Developer Console) +alert(phrase); // ReferenceError: phrase is not defined ``` -As we can see, `var` pierces through `if`, `for` or other code blocks. That's because a long time ago in JavaScript blocks had no Lexical Environments. And `var` is a remnant of that. +As we can see, `var` pierces through `if`, `for` or other code blocks. That's because a long time ago in JavaScript, blocks had no Lexical Environments, and `var` is a remnant of that. -## "var" declarations are processed at the function start +## "var" tolerates redeclarations + +If we declare the same variable with `let` twice in the same scope, that's an error: + +```js run +let user; +let user; // SyntaxError: 'user' has already been declared +``` + +With `var`, we can redeclare a variable any number of times. If we use `var` with an already-declared variable, it's just ignored: + +```js run +var user = "Pete"; + +var user = "John"; // this "var" does nothing (already declared) +// ...it doesn't trigger an error + +alert(user); // John +``` + +## "var" variables can be declared below their use `var` declarations are processed when the function starts (or script starts for globals). @@ -147,7 +166,7 @@ So in the example above, `if (false)` branch never executes, but that doesn't ma **Declarations are hoisted, but assignments are not.** -That's better to demonstrate with an example, like this: +That's best demonstrated with an example: ```js run function sayHi() { @@ -186,15 +205,83 @@ sayHi(); Because all `var` declarations are processed at the function start, we can reference them at any place. But variables are undefined until the assignments. -In both examples above `alert` runs without an error, because the variable `phrase` exists. But its value is not yet assigned, so it shows `undefined`. +In both examples above, `alert` runs without an error, because the variable `phrase` exists. But its value is not yet assigned, so it shows `undefined`. + +## IIFE + +In the past, as there was only `var`, and it has no block-level visibility, programmers invented a way to emulate it. What they did was called "immediately-invoked function expressions" (abbreviated as IIFE). + +That's not something we should use nowadays, but you can find them in old scripts. + +An IIFE looks like this: + +```js run +(function() { + + var message = "Hello"; + + alert(message); // Hello + +})(); +``` + +Here, a Function Expression is created and immediately called. So the code executes right away and has its own private variables. + +The Function Expression is wrapped with parenthesis `(function {...})`, because when JavaScript engine encounters `"function"` in the main code, it understands it as the start of a Function Declaration. But a Function Declaration must have a name, so this kind of code will give an error: + +```js run +// Tries to declare and immediately call a function +function() { // <-- SyntaxError: Function statements require a function name + + var message = "Hello"; + + alert(message); // Hello + +}(); +``` + +Even if we say: "okay, let's add a name", that won't work, as JavaScript does not allow Function Declarations to be called immediately: + +```js run +// syntax error because of parentheses below +function go() { + +}(); // <-- can't call Function Declaration immediately +``` + +So, the parentheses around the function is a trick to show JavaScript that the function is created in the context of another expression, and hence it's a Function Expression: it needs no name and can be called immediately. + +There exist other ways besides parentheses to tell JavaScript that we mean a Function Expression: + +```js run +// Ways to create IIFE + +*!*(*/!*function() { + alert("Parentheses around the function"); +}*!*)*/!*(); + +*!*(*/!*function() { + alert("Parentheses around the whole thing"); +}()*!*)*/!*; + +*!*!*/!*function() { + alert("Bitwise NOT operator starts the expression"); +}(); + +*!*+*/!*function() { + alert("Unary plus starts the expression"); +}(); +``` + +In all the above cases we declare a Function Expression and run it immediately. Let's note again: nowadays there's no reason to write such code. ## Summary There are two main differences of `var` compared to `let/const`: -1. `var` variables have no block scope, they are visible minimum at the function level. +1. `var` variables have no block scope, their visibility is scoped to current function, or global, if declared outside function. 2. `var` declarations are processed at function start (script start for globals). -There's one more minor difference related to the global object, we'll cover that in the next chapter. +There's one more very minor difference related to the global object, that we'll cover in the next chapter. These differences make `var` worse than `let` most of the time. Block-level variables is such a great thing. That's why `let` was introduced in the standard long ago, and is now a major way (along with `const`) to declare a variable. diff --git a/1-js/06-advanced-functions/05-global-object/article.md b/1-js/06-advanced-functions/05-global-object/article.md index 3d195a97..cf4839d9 100644 --- a/1-js/06-advanced-functions/05-global-object/article.md +++ b/1-js/06-advanced-functions/05-global-object/article.md @@ -5,7 +5,7 @@ The global object provides variables and functions that are available anywhere. In a browser it is named `window`, for Node.js it is `global`, for other environments it may have another name. -Recently, `globalThis` was added to the language, as a standardized name for a global object, that should be supported across all environments. In some browsers, namely non-Chromium Edge, `globalThis` is not yet supported, but can be easily polyfilled. +Recently, `globalThis` was added to the language, as a standardized name for a global object, that should be supported across all environments. It's supported in all major browsers. We'll use `window` here, assuming that our environment is a browser. If your script may run in other environments, it's better to use `globalThis` instead. @@ -25,7 +25,9 @@ var gVar = 5; alert(window.gVar); // 5 (became a property of the global object) ``` -Please don't rely on that! This behavior exists for compatibility reasons. Modern scripts use [JavaScript modules](info:modules) where such thing doesn't happen. +Function declarations have the same effect (statements with `function` keyword in the main code flow, not function expressions). + +Please don't rely on that! This behavior exists for compatibility reasons. Modern scripts use [JavaScript modules](info:modules) where such a thing doesn't happen. If we used `let` instead, such thing wouldn't happen: @@ -81,7 +83,7 @@ if (!window.Promise) { 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`. - ...But more often is referred by "old-school" environment-specific names, such as `window` (browser) and `global` (Node.js). As `globalThis` is a recent proposal, it's not supported in non-Chromium Edge (but can be polyfilled). + ...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/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 00000000..c7d7d734 --- /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 00000000..f10dca5d --- /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 00000000..ed567d33 --- /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 5c932691..e97039f7 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 2b4a7ac8..c84f4e52 100644 --- a/1-js/06-advanced-functions/06-function-object/article.md +++ b/1-js/06-advanced-functions/06-function-object/article.md @@ -67,7 +67,7 @@ alert(user.sayBye.name); // sayBye 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() {}]; @@ -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. ``` @@ -347,7 +347,7 @@ If the function is declared as a Function Expression (not in the main code flow) 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 it (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 3214ba37..ffe264a4 100644 --- a/1-js/06-advanced-functions/07-new-function/article.md +++ b/1-js/06-advanced-functions/07-new-function/article.md @@ -92,7 +92,7 @@ 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. So if `new Function` had access to outer variables, it would be unable to find renamed `userName`. 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 d79c7d4e..5a40238b 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/article.md +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/article.md @@ -4,8 +4,8 @@ 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. +- `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. @@ -61,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); @@ -102,7 +102,7 @@ As we can see from `alert` output, in a browser the timer identifier is a number 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 @@ -129,7 +129,7 @@ setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000); ```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 the next `alert` will be shown immediately as you do it. The actual interval between alerts will be shorter than 2 seconds. +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. ``` ## Nested setTimeout @@ -184,7 +184,7 @@ Let's compare two code fragments. The first one uses `setInterval`: ```js let i = 1; setInterval(function() { - func(i); + func(i++); }, 100); ``` @@ -193,12 +193,12 @@ 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.svg) @@ -232,16 +232,16 @@ 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. ```` ## Zero delay setTimeout 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. +So the function is scheduled to run "right after" the current script. For instance, this outputs "Hello", then immediately "World": @@ -251,12 +251,12 @@ 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. +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. There are also advanced browser-related use cases of zero-delay timeout, that we'll discuss in the chapter . ````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 [HTML5 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.". +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 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: @@ -281,22 +281,22 @@ The similar thing happens if we use `setInterval` instead of `setTimeout`: `setI 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 [setImmediate](https://nodejs.org/api/timers.html) for Node.js. So this note is browser-specific. +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. ```` ## 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`, allowing 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 code is complete". -- The browser limits the minimal delay for five or more nested call of `setTimeout` or for `setInterval` (after 5th call) to 4ms. That's for historical reasons. +- 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. 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 OS-level performance settings. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg index 9a214c54..bce7d6a8 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg @@ -1 +1 @@ -func(1)func(2)func(3)100200300 \ No newline at end of file +func(1)func(2)func(3)100200300 \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg index 13b22a89..d6d233b2 100644 --- a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg @@ -1 +1 @@ -func(1)func(2)func(3)100100 \ No newline at end of file +func(1)func(2)func(3)100100 \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js index 065a77d1..661dd0cf 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 16dc171e..750e649f 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; - } - - f = debounce(f, 1000); - - f(1); // runs at once - f(2); // ignored - - 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) - - this.clock.tick(5000); - assert.equal(log, "14"); + debounced('test'); + assert(f.notCalled, 'not called immediately'); + this.clock.tick(1000); + assert(f.calledOnceWith('test'), 'called after 1000ms'); }); - it("keeps the context of the call", function() { + it('for 3 calls - runs the last one after given ms', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); + + debounced('a'); + setTimeout(() => debounced('b'), 200); // ignored (too early) + setTimeout(() => debounced('c'), 500); // runs (1000 ms passed) + this.clock.tick(1000); + + assert(f.notCalled, 'not called after 1000ms'); + + this.clock.tick(500); + + assert(f.calledOnceWith('c'), 'called after 1500ms'); + }); + + 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 00000000..e624ce02 --- /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 00000000..e3b4d584 --- /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 4f5867de..83e75f31 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,28 +1,13 @@ ```js demo -function debounce(f, ms) { - - let isCooldown = false; - +function debounce(func, ms) { + let timeout; return function() { - if (isCooldown) return; - - f.apply(this, arguments); - - isCooldown = true; - - setTimeout(() => isCooldown = false, ms); + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, arguments), ms); }; - } + ``` -A call to `debounce` returns a wrapper. There may be two states: +A call to `debounce` returns a wrapper. When called, it schedules the original function call after given `ms` and cancels the previous such timeout. -- `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. - -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 2620f1c7..5b0fcc5f 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 future calls to the function made less than `ms` milliseconds after the previous call 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") ``` -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. +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. +``` + +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 5339c8d1..e671438f 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 c844016d..6950664b 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 @@ -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 567c9ce7..cbd47319 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,16 +4,21 @@ 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 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). - +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.** ...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. @@ -31,8 +36,8 @@ 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 8536cf31..c5d78549 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 @@ -36,11 +36,11 @@ function cachingDecorator(func) { 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. @@ -75,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(); // (*) } @@ -149,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: @@ -209,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. @@ -236,7 +236,7 @@ There are many solutions possible: For many practical applications, the 3rd variant is good enough, so we'll stick to it. -Also we need to replace `func.call(this, x)` with `func.call(this, ...arguments)`, to pass all arguments to the wrapped function call, not just the first one. +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)`. Here's a more powerful `cachingDecorator`: @@ -284,6 +284,8 @@ 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.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: @@ -299,18 +301,18 @@ The only syntax difference between `call` and `apply` is that `call` expects a l So these two calls are almost equivalent: ```js -func.call(context, ...args); // pass an array as list with spread operator -func.apply(context, args); // is same as using apply +func.call(context, ...args); +func.apply(context, args); ``` -There's only a minor difference: +They perform the same call of `func` with given context and arguments. -- The spread operator `...` allows to pass *iterable* `args` as the list to `call`. +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`. -So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works. - -And for objects that are both iterable and array-like, like a real array, we technically could use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better. +...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*. @@ -344,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: @@ -372,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? diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg index 5fc7743f..9b63cb98 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg +++ b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg @@ -1 +1 @@ -wrapperaround the function \ No newline at end of file +wrapperaround the function \ No newline at end of file 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 8cd18ec5..d6cfb44b 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/6-ask-partial/task.md b/1-js/06-advanced-functions/10-bind/6-ask-partial/task.md index f8b83d7a..c90851c2 100644 --- 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 @@ -8,7 +8,7 @@ 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`? +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) { diff --git a/1-js/06-advanced-functions/10-bind/article.md b/1-js/06-advanced-functions/10-bind/article.md index ce0c94a5..9d705cdc 100644 --- a/1-js/06-advanced-functions/10-bind/article.md +++ b/1-js/06-advanced-functions/10-bind/article.md @@ -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,7 +100,7 @@ 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); ``` @@ -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,7 +202,7 @@ 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)](http://lodash.com/docs#bindAll) in lodash. ```` ## Partial functions @@ -238,7 +247,7 @@ The call to `mul.bind(null, 2)` creates a new function `double` that passes call 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`. +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: @@ -258,7 +267,7 @@ 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 first argument of every time as it's fixed with `bind`. +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. @@ -270,7 +279,7 @@ What if we'd like to fix some arguments, but not the context `this`? For example The native `bind` does not allow that. We can't just omit the context and jump to arguments. -Fortunately, a helper function `partial` for binding only arguments can be easily implemented. +Fortunately, a function `partial` for binding only arguments can be easily implemented. Like this: @@ -304,7 +313,7 @@ The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that call - 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? +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. 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 abc5dd80..8730277a 100644 --- a/1-js/06-advanced-functions/12-arrow-functions/article.md +++ b/1-js/06-advanced-functions/12-arrow-functions/article.md @@ -4,7 +4,7 @@ Let's revisit arrow functions. 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: @@ -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-properties/01-property-descriptors/article.md b/1-js/07-object-properties/01-property-descriptors/article.md index 11e14221..bdc69341 100644 --- a/1-js/07-object-properties/01-property-descriptors/article.md +++ b/1-js/07-object-properties/01-property-descriptors/article.md @@ -3,7 +3,7 @@ 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 flexible and powerful thing. +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. @@ -19,7 +19,7 @@ We didn't see them yet, because generally they do not show up. When we create a 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 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 @@ -54,7 +54,7 @@ alert( JSON.stringify(descriptor, null, 2 ) ); */ ``` -To change the flags, we can use [Object.defineProperty](mdn:js/Object/defineProperty). +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: @@ -66,7 +66,7 @@ Object.defineProperty(obj, propertyName, descriptor) : The object and its property to apply the descriptor. `descriptor` -: Property descriptor to apply. +: 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`. @@ -134,7 +134,7 @@ let user = { }; Object.defineProperty(user, "name", { *!* value: "John", - // for new properties need to explicitly list what's true + // for new properties we need to explicitly list what's true enumerable: true, configurable: true */!* @@ -148,7 +148,7 @@ user.name = "Pete"; // Error 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: +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 = { @@ -162,7 +162,7 @@ let user = { 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: +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 = { @@ -194,7 +194,7 @@ alert(Object.keys(user)); // name The non-configurable flag (`configurable:false`) is sometimes preset for built-in objects and properties. -A non-configurable property can not be deleted. +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: @@ -214,49 +214,67 @@ alert( JSON.stringify(descriptor, null, 2 ) ); So, a programmer is unable to change the value of `Math.PI` or overwrite it. ```js run -Math.PI = 3; // Error +Math.PI = 3; // Error, because it has writable: false // delete Math.PI won't work either ``` -Making a property non-configurable is a one-way road. We cannot change it back with `defineProperty`. - -To be precise, non-configurability imposes several restrictions on `defineProperty`: -1. Can't change `configurable` flag. -2. Can't change `enumerable` flag. -3. Can't change `writable: false` to `true` (the other way round works). -4. Can't change `get/set` for an accessor property (but can assign them if absent). - -Here we are making `user.name` a "forever sealed" constant: +We also can't change `Math.PI` to be `writable` again: ```js run -let user = { }; +// 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", { - 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", { value: "Pete" }) -Object.defineProperty(user, "name", {writable: true}); // Error -*/!* +user.name = "Pete"; +delete user.name; +Object.defineProperty(user, "name", { value: "Pete" }); ``` -```smart header="\"Non-configurable\" doesn't mean \"non-writable\"" -Notable exception: a value of non-configurable, but writable property can be changed. +```smart header="The only attribute change possible: writable true -> false" +There's a minor exception about changing flags. -The idea of `configurable: false` is to prevent changes to property flags and its deletion, not changes to its value. +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)](mdn:js/Object/defineProperties) that allows to define many properties at once. +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: @@ -282,7 +300,7 @@ 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). +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: @@ -300,7 +318,7 @@ for (let key in user) { ...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. +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 @@ -308,23 +326,24 @@ 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) +[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)](mdn:js/Object/seal) +[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)](mdn:js/Object/freeze) +[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)](mdn:js/Object/isExtensible) +[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)](mdn:js/Object/isSealed) +[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)](mdn:js/Object/isFrozen) +[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 index b3bc2ec0..c2aa35d5 100644 --- a/1-js/07-object-properties/02-property-accessors/article.md +++ b/1-js/07-object-properties/02-property-accessors/article.md @@ -1,11 +1,11 @@ # Property getters and setters -There are two kinds of properties. +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 till now were data 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 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. +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 @@ -27,7 +27,7 @@ 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 +```js let user = { name: "John", surname: "Smith" @@ -53,7 +53,7 @@ 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. +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: @@ -94,17 +94,13 @@ alert(user.name); // Alice alert(user.surname); // Cooper ``` -As the result, we have a "virtual" property `fullName`. It is readable and writable, but in fact does not exist. - -```smart header="No way to handle `delete`" -There's no similar method to handle deletion of an accessor property. Only getter/setter methods may exist. -``` +As the result, we have a "virtual" property `fullName`. It is readable and writable. ## Accessor descriptors -Descriptors for accessor properties are different -- as compared with data properties. +Descriptors for accessor properties are different from those for data properties. -For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions. +For accessor properties, there is no `value` or `writable`, but instead there are `get` and `set` functions. That is, an accessor descriptor may have: @@ -138,7 +134,7 @@ 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 (has `get/set` methods) or a data property (has a `value`), not both. +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: @@ -189,9 +185,9 @@ Technically, external code is able to access the name directly by using `user._n ## Using for compatibility -One of the great uses of accessors -- they allow to take control over a "regular" data property at any moment by replacing it with getter and setter and tweak its behavior. +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`: +Imagine we started implementing user objects using data properties `name` and `age`: ```js function User(name, age) { 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 index 421b57e0..bc2db47f 100644 --- 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 @@ -6,7 +6,7 @@ importance: 5 The task has two parts. -We have objects: +Given the following objects: ```js let head = { 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 index b37499ba..ed8482c0 100644 --- 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 @@ -2,7 +2,7 @@ importance: 5 --- -# Where it writes? +# Where does it write? We have `rabbit` inheriting from `animal`. 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 index bd412f12..c141b2ec 100644 --- 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 @@ -44,7 +44,7 @@ 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: +Also we can totally avoid the problem by making sure that each hamster has their own stomach: ```js run let hamster = { 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 index 6f9fb279..50171123 100644 --- 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 @@ -2,11 +2,11 @@ importance: 5 --- -# Why two hamsters are full? +# 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 to fix it? +When we feed one of them, the other one is also full. Why? How can we fix it? ```js run let hamster = { diff --git a/1-js/08-prototypes/01-prototype-inheritance/article.md b/1-js/08-prototypes/01-prototype-inheritance/article.md index 5895a0b3..ef6c7ffe 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/article.md +++ b/1-js/08-prototypes/01-prototype-inheritance/article.md @@ -12,11 +12,11 @@ In JavaScript, objects have a special hidden property `[[Prototype]]` (as named ![prototype](object-prototype-empty.svg) -The prototype is a little bit "magical". 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. +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 `__proto__`, like this: +One of them is to use the special name `__proto__`, like this: ```js run let animal = { @@ -27,23 +27,15 @@ let rabbit = { }; *!* -rabbit.__proto__ = animal; +rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal */!* ``` -```smart header="`__proto__` is a historical getter/setter for `[[Prototype]]`" -Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. - -It exists for historical reasons, in modern language it is replaced with functions `Object.getPrototypeOf/Object.setPrototypeOf` that also get/set the prototype. We'll study the reasons for that and these functions later. - -By the specification, `__proto__` must only be supported by browsers, but in fact all environments including server-side support it. For now, as `__proto__` notation is a little bit more intuitively obvious, we'll use it in the examples. -``` - -If we look for a property in `rabbit`, and it's missing, JavaScript automatically takes it from `animal`. +Now if we read a property from `rabbit`, and it's missing, JavaScript will automatically take it from `animal`. For instance: -```js run +```js let animal = { eats: true }; @@ -62,7 +54,7 @@ alert( rabbit.eats ); // true (**) alert( rabbit.jumps ); // true ``` -Here the line `(*)` sets `animal` to be a prototype of `rabbit`. +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): @@ -130,6 +122,8 @@ 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. @@ -137,6 +131,18 @@ There are only two limitations: 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. @@ -197,13 +203,16 @@ 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 the properties `this.name` and `this.surname` are written: into `user` or `admin`? +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. @@ -211,7 +220,7 @@ The answer is simple: `this` is not affected by prototypes at all. 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 inherit from it. Then inherited objects can run its methods, and they will modify the state of these objects, not the big one. +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. @@ -246,13 +255,13 @@ 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. +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` loops over inherited properties too. +The `for..in` loop iterates over inherited properties too. For instance: @@ -267,7 +276,7 @@ let rabbit = { }; *!* -// Object.keys only return own keys +// Object.keys only returns own keys alert(Object.keys(rabbit)); // jumps */!* @@ -277,7 +286,7 @@ 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)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. +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): @@ -308,9 +317,9 @@ Here we have the following inheritance chain: `rabbit` inherits from `animal`, t 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 `hasOwnProperty` does not appear in `for..in` loop, like `eats` and `jumps`, if it lists all inherited properties. +...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. That's why they are not listed. +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. @@ -324,6 +333,6 @@ They only operate on the object itself. Properties from the prototype are *not* - 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 for act directly on the object, they don't use the prototype (assuming it's a data property, not is a setter). +- 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 own and inherited properties. All other key/value-getting methods only operate on the object itself. +- 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 index fe4c2cac..eb79c19f 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg @@ -1 +1 @@ -prototype objectobject[[Prototype]] \ No newline at end of file +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 index 3e81f262..4bf580ae 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg @@ -1 +1 @@ -eats: true walk: functionanimaljumps: truerabbit[[Prototype]]earLength: 10longEar[[Prototype]] \ No newline at end of file +eats: true walk: functionanimaljumps: truerabbit[[Prototype]]earLength: 10longEar[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg index 3cf5c4c7..838c7839 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg @@ -1 +1 @@ -eats: true walk: functionanimalwalk: functionrabbit[[Prototype]] \ No newline at end of file +eats: true walk: functionanimalwalk: functionrabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg index acd42063..d791e539 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg @@ -1 +1 @@ -walk: function sleep: functionanimalrabbit[[Prototype]]name: "White Rabbit" isSleeping: true \ No newline at end of file +walk: function sleep: functionanimalrabbit[[Prototype]]name: "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 index ebdef958..b3247102 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg @@ -1 +1 @@ -eats: true walk: functionanimaljumps: truerabbit[[Prototype]] \ No newline at end of file +eats: true walk: functionanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg index 735e1f2b..4f3c1bc0 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg @@ -1 +1 @@ -eats: trueanimaljumps: truerabbit[[Prototype]] \ No newline at end of file +eats: trueanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg index 433bc613..bf0baf01 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg @@ -1 +1 @@ -name: "John" surname: "Smith" set fullName: functionisAdmin: true name: "Alice" surname: "Cooper"useradmin[[Prototype]] \ No newline at end of file +name: "John" surname: "Smith" set fullName: functionisAdmin: true name: "Alice" surname: "Cooper"useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg index d32585b4..32a9858f 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg +++ b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg @@ -1 +1 @@ -toString: function hasOwnProperty: function ...Object.prototypeanimal[[Prototype]][[Prototype]][[Prototype]]nulleats: truerabbitjumps: true \ No newline at end of file +toString: function hasOwnProperty: function ...Object.prototypeanimal[[Prototype]][[Prototype]][[Prototype]]nulleats: truerabbitjumps: true \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/1-changing-prototype/task.md b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/task.md index 4b8522d3..2838c125 100644 --- 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 @@ -20,7 +20,7 @@ alert( rabbit.eats ); // true ``` -1. We added one more string (emphasized), what `alert` shows now? +1. We added one more string (emphasized). What will `alert` show now? ```js function Rabbit() {} @@ -54,7 +54,7 @@ alert( rabbit.eats ); // true alert( rabbit.eats ); // ? ``` -3. Like this (replaced one line)? +3. And like this (replaced one line)? ```js function Rabbit() {} diff --git a/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md b/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md index 43190e16..372d50dd 100644 --- a/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md +++ b/1-js/08-prototypes/02-function-prototype/4-new-object-same-constructor/solution.md @@ -15,7 +15,7 @@ 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. +..But if someone, so to speak, overwrites `User.prototype` and forgets to recreate `constructor` to reference `User`, then it would fail. For instance: @@ -38,7 +38,12 @@ 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. +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. -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. +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/08-prototypes/02-function-prototype/article.md b/1-js/08-prototypes/02-function-prototype/article.md index 29b3773e..b1ef5182 100644 --- a/1-js/08-prototypes/02-function-prototype/article.md +++ b/1-js/08-prototypes/02-function-prototype/article.md @@ -2,7 +2,7 @@ Remember, new objects can be created with a constructor function, like `new F()`. -If `F.prototype` is an object, then `new` operator uses it to set `[[Prototype]]` for the new object. +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. @@ -41,7 +41,7 @@ That's the resulting picture: 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. After that, there's no connection between `F.prototype` and the new object. Think of it as a "one-time gift". +`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. ``` @@ -158,9 +158,9 @@ Rabbit.prototype = { 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: +Everything is quite simple, just a few notes to make things clear: -- The `F.prototype` property (don't mess with `[[Prototype]]`) sets `[[Prototype]]` of new objects when `new F()` is called. +- 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`. diff --git a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg index 35cdc61f..59d60b39 100644 --- a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg +++ b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg @@ -1 +1 @@ -Rabbitprototypeconstructordefault "prototype" \ No newline at end of file +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 index 3489ecdd..ede4e122 100644 --- a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg +++ b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg @@ -1 +1 @@ -eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file +eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg index 3e11f275..54b3d798 100644 --- a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg +++ b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg @@ -1 +1 @@ -default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file +default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file 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 index e3651683..99c358c9 100644 --- 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 @@ -15,3 +15,27 @@ function f(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/08-prototypes/03-native-prototypes/article.md b/1-js/08-prototypes/03-native-prototypes/article.md index 66be00ca..bdfc86dd 100644 --- a/1-js/08-prototypes/03-native-prototypes/article.md +++ b/1-js/08-prototypes/03-native-prototypes/article.md @@ -2,7 +2,7 @@ The `"prototype"` property is widely used by the core of JavaScript itself. All built-in constructor functions use it. -First we'll see at the details, and then how to use it for adding new capabilities to built-in objects. +First we'll look at the details, and then how to use it for adding new capabilities to built-in objects. ## Object.prototype @@ -33,7 +33,9 @@ We can check it like this: let obj = {}; alert(obj.__proto__ === Object.prototype); // true -// obj.toString === obj.__proto__.toString == Object.prototype.toString + +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`: @@ -99,12 +101,12 @@ alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects 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. +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 too. +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] @@ -129,9 +131,9 @@ 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 JavaScript specification, but not yet supported by current JavaScript engine. +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. -Then we may implement it manually and populate the built-in prototype with it. +We may then implement it manually and populate the built-in prototype with it. For instance: @@ -144,7 +146,7 @@ if (!String.prototype.repeat) { // if there's no such method // 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 for use + // but even an imperfect polyfill is often considered good enough return new Array(n + 1).join(this); }; } @@ -179,18 +181,18 @@ 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 that the object is indeed the array. And many built-in methods are like that. +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 functionality from different objects if needed. +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`, `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. Probably the only allowable cause is when we add-in a new standard, but not yet supported by the engine JavaScript method. + - 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/08-prototypes/03-native-prototypes/function-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg index 35cdc61f..59d60b39 100644 --- a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg +++ b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg @@ -1 +1 @@ -Rabbitprototypeconstructordefault "prototype" \ No newline at end of file +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 index 770c908c..ebb4f320 100644 --- a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg @@ -1 +1 @@ -toString: function ...Array.prototypetoString: function ...Object.prototype[[Prototype]][[Prototype]][1, 2, 3] \ No newline at end of file +toString: function ...Array.prototypetoString: function ...Object.prototype[[Prototype]][[Prototype]][1, 2, 3] \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg index 4989df56..4d6129e0 100644 --- a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg @@ -1 +1 @@ -toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file +toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg index 38c33cae..9630e68e 100644 --- a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg @@ -1 +1 @@ -constructor: Object toString: function ...Object.prototypeObjectobj = new Object()[[Prototype]]prototype \ No newline at end of file +constructor: Object toString: function ...Object.prototypeObjectobj = new Object()[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg index 858f8317..9ccb3422 100644 --- a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg @@ -1 +1 @@ -obj[[Prototype]]null \ No newline at end of file +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg index 8d3d0bee..024dd302 100644 --- a/1-js/08-prototypes/03-native-prototypes/object-prototype.svg +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg @@ -1 +1 @@ -constructor: Object toString: function ...Object.prototypeObjectprototype \ No newline at end of file +constructor: Object toString: function ...Object.prototypeObjectprototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg b/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg deleted file mode 100644 index 3489ecdd..00000000 --- a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg +++ /dev/null @@ -1 +0,0 @@ -eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg index 3e11f275..54b3d798 100644 --- a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg +++ b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg @@ -1 +1 @@ -default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file +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 index a92e1790..f3c9cf0e 100644 --- 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 @@ -28,4 +28,4 @@ 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 the chapter [](info:property-descriptors) for review. +See the chapter [](info:property-descriptors) for review. diff --git a/1-js/08-prototypes/04-prototype-methods/article.md b/1-js/08-prototypes/04-prototype-methods/article.md index 8a71dbf1..71f118e1 100644 --- a/1-js/08-prototypes/04-prototype-methods/article.md +++ b/1-js/08-prototypes/04-prototype-methods/article.md @@ -3,15 +3,18 @@ In the first chapter of this section, we mentioned that there are modern methods to setup a prototype. -The `__proto__` is considered outdated and somewhat deprecated (in browser-only part of the JavaScript standard). +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 are: +The modern methods to get/set a prototype are: -- [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`. -These should be used instead of `__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: @@ -22,13 +25,13 @@ let animal = { // create a new object with animal as a prototype *!* -let rabbit = Object.create(animal); +let rabbit = Object.create(animal); // same as {__proto__: animal} */!* alert(rabbit.eats); // true *!* -alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit +alert(Object.getPrototypeOf(rabbit) === animal); // true */!* *!* @@ -36,7 +39,9 @@ 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: +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 = { @@ -57,32 +62,39 @@ The descriptors are in the same format as described in the chapter ... get __proto__: function set __proto__: functionObject.prototypeObjectobj[[Prototype]]prototype \ No newline at end of file +... get __proto__: function set __proto__: functionObject.prototypeObjectobj[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg index 858f8317..9ccb3422 100644 --- a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg @@ -1 +1 @@ -obj[[Prototype]]null \ No newline at end of file +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/09-classes/01-class/1-rewrite-to-class/task.md b/1-js/09-classes/01-class/1-rewrite-to-class/task.md index 05365e41..4477de67 100644 --- 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 @@ -4,6 +4,6 @@ importance: 5 # Rewrite to class -The `Clock` class is written in functional style. Rewrite it the "class" syntax. +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 index 3cfcd4cb..135d2492 100644 --- a/1-js/09-classes/01-class/article.md +++ b/1-js/09-classes/01-class/article.md @@ -51,7 +51,7 @@ user.sayHi(); When `new User("John")` is called: 1. A new object is created. -2. The `constructor` runs with the given argument and assigns `this.name` to it. +2. The `constructor` runs with the given argument and assigns it to `this.name`. ...Then we can call object methods, such as `user.sayHi()`. @@ -68,7 +68,7 @@ So, what exactly is a `class`? That's not an entirely new language-level entity, 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 a function. +In JavaScript, a class is a kind of function. Here, take a look: @@ -89,7 +89,7 @@ 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`. -Afterwards, for `new User` objects, when we call a method, it's taken from the prototype, just as described in the chapter . So the object has access to class methods. +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: @@ -110,15 +110,15 @@ alert(typeof User); // function alert(User === User.prototype.constructor); // true // The methods are in User.prototype, e.g: -alert(User.prototype.sayHi); // alert(this.name); +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 syntax sugar +## Not just a syntactic sugar -Sometimes people say that `class` is a "syntax sugar" (syntax that is designed to make things easier to read, but doesn't introduce anything new), because we could actually declare the same without `class` keyword at all: +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 @@ -127,7 +127,7 @@ Sometimes people say that `class` is a "syntax sugar" (syntax that is designed t function User(name) { this.name = name; } -// any function prototype has constructor property by default, +// a function prototype has "constructor" property by default, // so we don't need to create it // 2. Add the method to prototype @@ -140,13 +140,13 @@ 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 syntax sugar to define a constructor together with its prototype methods. +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. -Although, there are important differences. +Still, there are important differences. -1. First, a function created by `class` is labelled by a special internal property `[[FunctionKind]]:"classConstructor"`. So it's not entirely the same as creating it manually. +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. - Unlike a regular function, a class constructor must be called with `new`: + 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 { @@ -166,6 +166,7 @@ Although, there are important differences. 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"`. @@ -179,7 +180,7 @@ 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. +Just like functions, classes can be defined inside another expression, passed around, returned, assigned, etc. Here's an example of a class expression: @@ -209,7 +210,6 @@ 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 @@ -218,7 +218,7 @@ function makeClass(phrase) { return class { sayHi() { alert(phrase); - }; + } }; } @@ -229,9 +229,9 @@ new User().sayHi(); // Hello ``` -## Getters/setters, other shorthands +## Getters/setters -Just like literal objects, classes may include getters/setters, generators, computed properties etc. +Just like literal objects, classes may include getters/setters, computed properties etc. Here's an example for `user.name` implemented using `get/set`: @@ -264,25 +264,14 @@ class User { let user = new User("John"); alert(user.name); // John -user = new User(""); // Name too short. +user = new User(""); // Name is too short. ``` -The class declaration creates getters and setters in `User.prototype`, like this: +Technically, such class declaration works by creating getters and setters in `User.prototype`. -```js -Object.defineProperties(User.prototype, { - name: { - get() { - return this._name - }, - set(name) { - // ... - } - } -}); -``` +## Computed names [...] -Here's an example with a computed property in brackets `[...]`: +Here's an example with a computed method name using brackets `[...]`: ```js run class User { @@ -298,20 +287,24 @@ class User { new User().sayHi(); ``` -For a generator method, similarly, prepend it with `*`. +Such features are easy to remember, as they resemble that of literal objects. -## Class properties +## Class fields ```warn header="Old browsers may need a polyfill" -Class-level properties are a recent addition to the language. +Class fields are a recent addition to the language. ``` -In the example above, `User` only had methods. Let's add a property: +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 = "Anonymous"; + name = "John"; */!* sayHi() { @@ -319,10 +312,94 @@ class User { } } -new User().sayHi(); +new User().sayHi(); // Hello, John! ``` -The property `name` is not placed into `User.prototype`. Instead, it is created by `new` before calling constructor, it's the property of the object itself. +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 @@ -346,6 +423,6 @@ class MyClass { } ``` -`MyClass` is technically a function (the one that we provide as `constructor`), while methods, getters and settors are written to `MyClass.prototype`. +`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 index 5ac0146a..418d71d1 100644 --- a/1-js/09-classes/01-class/class-user.svg +++ b/1-js/09-classes/01-class/class-user.svg @@ -1 +1 @@ -sayHi: functionUserUser.prototypeprototypeconstructor: User \ No newline at end of file +sayHi: functionUserUser.prototypeprototypeconstructor: User \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js index ca613ca5..be2053cf 100644 --- a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js @@ -1,7 +1,7 @@ class ExtendedClock extends Clock { constructor(options) { super(options); - let { precision=1000 } = options; + let { precision = 1000 } = options; this.precision = precision; } diff --git a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg index 3412d982..63b5a18a 100644 --- a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg +++ b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg @@ -1 +1 @@ -constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "White Rabbit"constructorconstructor \ No newline at end of file +constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "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 index 6921698e..464042d8 100644 --- a/1-js/09-classes/02-class-inheritance/article.md +++ b/1-js/09-classes/02-class-inheritance/article.md @@ -1,9 +1,13 @@ # Class inheritance -Let's say we have two classes. +Class inheritance is a way for one class to extend another class. -`Animal`: +So we can create new functionality on top of the existing. + +## The "extends" keyword + +Let's say we have class `Animal`: ```js class Animal { @@ -12,7 +16,7 @@ class Animal { this.name = name; } run(speed) { - this.speed += speed; + this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { @@ -24,52 +28,19 @@ class Animal { 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`. -...And `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 { - constructor(name) { - this.name = name; - } - hide() { - alert(`${this.name} hides!`); - } -} - -let rabbit = new Rabbit("My rabbit"); -``` - -![](rabbit-animal-independent-rabbit.svg) - - -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. - -To inherit from another class, we should specify `"extends"` and the parent class before the braces `{..}`. - -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} stands still.`); - } -} - -// Inherit from Animal by specifying "extends Animal" *!* class Rabbit extends Animal { */!* @@ -84,15 +55,18 @@ rabbit.run(5); // White Rabbit runs with speed 5. rabbit.hide(); // White Rabbit hides! ``` -Now the `Rabbit` code became a bit shorter, as it uses `Animal` constructor by default, and it also can `run`, as animals do. +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 adds `[[Prototype]]` reference from `Rabbit.prototype` to `Animal.prototype`: +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) -So, if a method is not found in `Rabbit.prototype`, JavaScript takes it from `Animal.prototype`. +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 uses prototypal inheritance for build-in objects. E.g. `Date.prototype.[[Prototype]]` is `Object.prototype`, so dates have generic object methods. +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`. @@ -102,8 +76,8 @@ For instance, a function call that generates the parent class: ```js run function f(phrase) { return class { - sayHi() { alert(phrase) } - } + sayHi() { alert(phrase); } + }; } *!* @@ -119,19 +93,20 @@ That may be useful for advanced programming patterns when we use functions to ge ## 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`. +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`. -If we specify our own `stop` in `Rabbit`, then it will be used instead: +But if we specify our own method in `Rabbit`, such as `stop()` then it will be used instead: ```js class Rabbit extends Animal { stop() { - // ...this will be used for rabbit.stop() + // ...now this will be used for rabbit.stop() + // instead of stop() from class Animal } } ``` -...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. +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. @@ -149,7 +124,7 @@ class Animal { } run(speed) { - this.speed += speed; + this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } @@ -176,7 +151,7 @@ class Rabbit extends Animal { let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. -rabbit.stop(); // White Rabbit stands still. White rabbit hides! +rabbit.stop(); // White Rabbit stands still. White Rabbit hides! ``` Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the process. @@ -185,6 +160,7 @@ Now `Rabbit` has the `stop` method that calls the parent `super.stop()` in the p 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() { @@ -201,12 +177,11 @@ setTimeout(function() { super.stop() }, 1000); ``` ```` - ## Overriding constructor With constructors it gets a little bit tricky. -Till now, `Rabbit` did not have its own `constructor`. +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: @@ -255,22 +230,24 @@ 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`. +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. +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" and all others. In an inheriting class, the corresponding constructor function is labelled with a special internal property `[[ConstructorKind]]:"derived"`. +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. -The difference is: +That label affects its behavior with `new`. -- When a normal constructor runs, it creates an empty object and assigns it to `this`. +- 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 if we're making a constructor of our own, then we must call `super`, because otherwise the object for `this` won't be created. And we'll get an error. +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 `Rabbit` constructor to work, it needs to call `super()` before using `this`, like here: +For the `Rabbit` constructor to work, it needs to call `super()` before using `this`, like here: ```js run class Animal { @@ -303,6 +280,99 @@ 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]] @@ -312,7 +382,7 @@ 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 by the way. +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! @@ -463,7 +533,7 @@ It works as intended, due to `[[HomeObject]]` mechanics. A method, such as `long 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 existance of `[[HomeObject]]` violates that principle, because methods remember their objects. `[[HomeObject]]` can't be changed, so this bond is forever. +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. @@ -472,7 +542,7 @@ Here's the demo of a wrong `super` result after copying: ```js run let animal = { sayHi() { - console.log(`I'm an animal`); + alert(`I'm an animal`); } }; @@ -486,7 +556,7 @@ let rabbit = { let plant = { sayHi() { - console.log("I'm a plant"); + alert("I'm a plant"); } }; @@ -503,7 +573,7 @@ tree.sayHi(); // I'm an animal (?!?) */!* ``` -A call to `tree.sayHi()` shows "I'm an animal". Definitevely wrong. +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? @@ -524,7 +594,7 @@ In the example below a non-method syntax is used for comparison. `[[HomeObject]] ```js run let animal = { - eat: function() { // intentially writing like this instead of eat() {... + eat: function() { // intentionally writing like this instead of eat() {... // ... } }; @@ -554,4 +624,4 @@ rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) - So it's not safe to copy a method with `super` from one object to another. Also: -- Arrow functions don't have own `this` or `super`, so they transparently fit into the surrounding context. +- 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 index 546aa334..5ea9bf29 100644 --- a/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg @@ -1 +1 @@ -slice: function ...Array.prototypearrhasOwnProperty: function ...Object.prototype[1, 2, 3][[Prototype]][[Prototype]] \ No newline at end of file +slice: function ...Array.prototypearrhasOwnProperty: function ...Object.prototype[1, 2, 3][[Prototype]][[Prototype]] \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg index 3bdda5a0..72e47e34 100644 --- a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg @@ -1 +1 @@ -jump: functionRabbit.prototyperabbiteat: functionAnimal.prototypename: "White Rabbit"[[Prototype]][[Prototype]]Rabbit.prototype.__proto__ = Animal.prototype sets thistoString: function hasOwnProperty: function ...Object.prototype[[Prototype]][[Prototype]]null \ No newline at end of file +jump: functionRabbit.prototyperabbiteat: functionAnimal.prototypename: "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 index 91f82896..bced3d35 100644 --- a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg @@ -1 +1 @@ -methods of RabbitRabbit.prototyperabbitmethods of AnimalAnimal.prototype[[Prototype]][[Prototype]]properties of rabbit \ No newline at end of file +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 index bf86db77..f53fc92d 100644 --- a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg @@ -1 +1 @@ - constructor: Animal run: function stop: functionAnimal.prototypeAnimalnew Animal[[Prototype]]prototypename: "My animal" \ No newline at end of file + constructor: Animal run: function stop: functionAnimal.prototypeAnimalnew Animal[[Prototype]]prototypename: "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 index 8a5e2503..2f30a3a9 100644 --- a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg @@ -1 +1 @@ - constructor: Rabbit hide: functionRabbit.prototypeRabbitnew Rabbit[[Prototype]]prototypename: "My rabbit" \ No newline at end of file + constructor: Rabbit hide: functionRabbit.prototypeRabbitnew Rabbit[[Prototype]]prototypename: "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 index c9c8fea9..f6450ddc 100644 --- a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg +++ b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg @@ -1 +1 @@ -sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file +sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/this-super-loop.svg b/1-js/09-classes/02-class-inheritance/this-super-loop.svg index 342574da..4f5f4503 100644 --- a/1-js/09-classes/02-class-inheritance/this-super-loop.svg +++ b/1-js/09-classes/02-class-inheritance/this-super-loop.svg @@ -1 +1 @@ -rabbitlongEarrabbitlongEar \ No newline at end of file +rabbitlongEarrabbitlongEar \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/rabbit-extends-object.svg similarity index 54% rename from 1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg rename to 1-js/09-classes/03-static-properties-methods/3-class-extend-object/rabbit-extends-object.svg index 0a1f4382..915ab9aa 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg +++ b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/rabbit-extends-object.svg @@ -1 +1 @@ -call: function bind: function ...Function.prototypeconstructorObjectRabbit[[Prototype]][[Prototype]]constructorcall: function bind: function ...Function.prototypeRabbit[[Prototype]]constructorclass Rabbitclass Rabbit extends Object \ No newline at end of file +call: function bind: function ...Function.prototypeconstructorObjectRabbit[[Prototype]][[Prototype]]constructorcall: function bind: function ...Function.prototypeRabbit[[Prototype]]constructorclass Rabbitclass Rabbit extends Object \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/solution.md similarity index 76% rename from 1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md rename to 1-js/09-classes/03-static-properties-methods/3-class-extend-object/solution.md index fa26ec83..cb9829ce 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md +++ b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/solution.md @@ -21,14 +21,14 @@ 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`. +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 itself (for static methods). +2. Between the constructor functions themselves (for static methods). -In our case, for `class Rabbit extends Object` it means: +In the case of `class Rabbit extends Object` it means: ```js run class Rabbit extends Object {} @@ -37,7 +37,7 @@ 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: +So `Rabbit` now provides access to the static methods of `Object` via `Rabbit`, like this: ```js run class Rabbit extends Object {} @@ -67,7 +67,7 @@ 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`. +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: diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/task.md similarity index 88% rename from 1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md rename to 1-js/09-classes/03-static-properties-methods/3-class-extend-object/task.md index ca6628ed..1d0f98a7 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md +++ b/1-js/09-classes/03-static-properties-methods/3-class-extend-object/task.md @@ -1,4 +1,4 @@ -importance: 5 +importance: 3 --- @@ -19,7 +19,6 @@ let rabbit = new Rabbit("Rab"); *!* // hasOwnProperty method is from Object.prototype -// rabbit.__proto__ === Object.prototype alert( rabbit.hasOwnProperty('name') ); // true */!* ``` @@ -39,5 +38,5 @@ class Rabbit extends Object { let rabbit = new Rabbit("Rab"); -alert( rabbit.hasOwnProperty('name') ); // true +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 index fab401df..3e354b89 100644 --- a/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg +++ b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg @@ -1 +1 @@ -constructor: Animal run: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitrabbit[[Prototype]][[Prototype]][[Prototype]]prototypeprototypecomparename: "White Rabbit" \ No newline at end of file +constructor: Animal run: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitrabbit[[Prototype]][[Prototype]][[Prototype]]prototypeprototypecomparename: "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 index 583fabcf..4b493a5e 100644 --- a/1-js/09-classes/03-static-properties-methods/article.md +++ b/1-js/09-classes/03-static-properties-methods/article.md @@ -1,9 +1,9 @@ # Static properties and methods -We can also assign a method to the class function itself, not to its `"prototype"`. Such methods are called *static*. +We can also assign a method to the class as a whole. Such methods are called *static*. -In a class, they are prepended by `static` keyword, like this: +In a class declaration, they are prepended by `static` keyword, like this: ```js run class User { @@ -19,19 +19,23 @@ User.staticMethod(); // true That actually does the same as assigning it as a property directly: -```js -class User() { } +```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, but not to any particular object of it. +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` method, like this: +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 { @@ -61,9 +65,11 @@ articles.sort(Article.compare); alert( articles[0].title ); // CSS ``` -Here `Article.compare` stands "above" articles, as a means to compare them. It's not a method of an article, but rather of the whole class. +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. Imagine, we need few ways to create an article: +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. @@ -71,7 +77,7 @@ Another example would be a so-called "factory" method. Imagine, we need few ways 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: +Such as `Article.createTodays()` here: ```js run class Article { @@ -99,10 +105,21 @@ Static methods are also used in database-related classes to search/save/remove e ```js // assuming Article is a special class for managing articles -// static method to remove the article: +// 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] @@ -123,14 +140,15 @@ That is the same as a direct assignment to `Article`: Article.publisher = "Ilya Kantor"; ``` -## Inheritance of static methods +## Inheritance of static properties and methods [#statics-and-inheritance] -Static methods are inherited. +Static properties and methods are inherited. -For instance, `Animal.compare` in the code below is inherited and accessible as `Rabbit.compare`: +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; @@ -167,9 +185,11 @@ rabbits.sort(Rabbit.compare); */!* rabbits[0].run(); // Black Rabbit runs with speed 5. + +alert(Rabbit.planet); // Earth ``` -Now when we can call `Rabbit.compare`, the inherited `Animal.compare` will be called. +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`. @@ -180,7 +200,7 @@ 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 the result, inheritance works both for regular and static methods. +As a result, inheritance works both for regular and static methods. Here, let's check that by code: @@ -192,12 +212,12 @@ class Rabbit extends Animal {} alert(Rabbit.__proto__ === Animal); // true // for regular methods -alert(Rabbit.prototype.__proto__ === Animal.prototype); +alert(Rabbit.prototype.__proto__ === Animal.prototype); // true ``` ## Summary -Static methods are used for the functionality that belongs to the class "as a whole", doesn't relate to a concrete class instance. +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()`. diff --git a/1-js/09-classes/04-private-protected-properties-methods/article.md b/1-js/09-classes/04-private-protected-properties-methods/article.md index ef0d497a..91efb89e 100644 --- a/1-js/09-classes/04-private-protected-properties-methods/article.md +++ b/1-js/09-classes/04-private-protected-properties-methods/article.md @@ -50,7 +50,7 @@ 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. Till now we were only using public 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. @@ -96,7 +96,9 @@ class CoffeeMachine { _waterAmount = 0; set waterAmount(value) { - if (value < 0) throw new Error("Negative water"); + if (value < 0) { + value = 0; + } this._waterAmount = value; } @@ -114,10 +116,10 @@ class CoffeeMachine { let coffeeMachine = new CoffeeMachine(100); // add water -coffeeMachine.waterAmount = -10; // Error: Negative water +coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10 ``` -Now the access is under control, so setting the water below zero fails. +Now the access is under control, so setting the water amount below zero becomes impossible. ## Read-only "power" @@ -159,7 +161,7 @@ class CoffeeMachine { _waterAmount = 0; *!*setWaterAmount(value)*/!* { - if (value < 0) throw new Error("Negative water"); + if (value < 0) value = 0; this._waterAmount = value; } @@ -190,7 +192,7 @@ There's a finished JavaScript proposal, almost in the standard, that provides la 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 `#checkWater`: +For instance, here's a private `#waterLimit` property and the water-checking private method `#fixWaterAmount`: ```js run class CoffeeMachine { @@ -199,19 +201,23 @@ class CoffeeMachine { */!* *!* - #checkWater(value) { - if (value < 0) throw new Error("Negative water"); - if (value > this.#waterLimit) throw new Error("Too much water"); + #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.#checkWater(); // Error +coffeeMachine.#fixWaterAmount(123); // Error coffeeMachine.#waterLimit = 1000; // Error */!* ``` @@ -232,7 +238,7 @@ class CoffeeMachine { } set waterAmount(value) { - if (value < 0) throw new Error("Negative water"); + if (value < 0) value = 0; this.#waterAmount = value; } } @@ -248,7 +254,7 @@ Unlike protected ones, private fields are enforced by the language itself. That' 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() { +class MegaCoffeeMachine extends CoffeeMachine { method() { *!* alert( this.#waterAmount ); // Error: can only access from CoffeeMachine @@ -257,7 +263,7 @@ class MegaCoffeeMachine extends CoffeeMachine() { } ``` -In many scenarios such limitation is too severe. If we extend a `CoffeeMachine`, we may have legitimate reason to access its internals. That's why protected fields are used more often, even though they are not supported by the language syntax. +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. @@ -279,11 +285,11 @@ With private fields that's impossible: `this['#name']` doesn't work. That's a sy ## 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)"). +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 feet +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. @@ -302,15 +308,15 @@ Supportable 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 to use things that are simple. At least from outside. What's inside is a different thing. +: 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 internal interface we use either protected or private properties: +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 only can access those from inside the class. +- 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/05-extend-natives/article.md b/1-js/09-classes/05-extend-natives/article.md index 2dc4902b..28b4c6eb 100644 --- a/1-js/09-classes/05-extend-natives/article.md +++ b/1-js/09-classes/05-extend-natives/article.md @@ -21,7 +21,7 @@ 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 object `constructor` property for that. +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 @@ -32,7 +32,7 @@ When `arr.filter()` is called, it internally creates the new array of results us Even more, we can customize that behavior. -We can add a special static getter `Symbol.species` to the class. If exists, it should return the constructor that JavaScript will use internally to create new entities in `map`, `filter` and so on. +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: @@ -74,11 +74,11 @@ Built-in objects have their own static methods, for instance `Object.keys`, `Arr 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 chapter [](info:static-properties-methods#statics-and-inheritance). +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 `Array.keys()` and `Date.keys()` static methods. +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`: diff --git a/1-js/09-classes/05-extend-natives/object-date-inheritance.svg b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg index a0165bcc..be47d7fd 100644 --- a/1-js/09-classes/05-extend-natives/object-date-inheritance.svg +++ b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg @@ -1 +1 @@ -constructor: Object toString: function hasOwnProperty: function ...Object.prototypeconstructor: Date toString: function getDate: function ...Date.prototypeObjectDatenew Date()[[Prototype]][[Prototype]]prototypeprototypedefineProperty keys ...now parse ...1 Jan 2019 \ No newline at end of file +constructor: Object toString: function hasOwnProperty: function ...Object.prototypeconstructor: Date toString: function getDate: function ...Date.prototypeObjectDatenew Date()[[Prototype]][[Prototype]]prototypeprototypedefineProperty keys ...now parse ...1 Jan 2019 \ No newline at end of file diff --git a/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md b/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md index e9481912..5b8dc7de 100644 --- a/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md +++ b/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md @@ -4,7 +4,7 @@ importance: 5 # Strange instanceof -Why `instanceof` below returns `true`? We can easily see that `a` is not created by `B()`. +In the code below, why does `instanceof` return `true`? We can easily see that `a` is not created by `B()`. ```js run function A() {} diff --git a/1-js/09-classes/06-instanceof/article.md b/1-js/09-classes/06-instanceof/article.md index 0b02c99b..f9db989c 100644 --- a/1-js/09-classes/06-instanceof/article.md +++ b/1-js/09-classes/06-instanceof/article.md @@ -2,7 +2,7 @@ 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. +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] @@ -44,9 +44,9 @@ 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`. +Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypically inherits from `Object`. -Normally, `instanceof` operator examines the prototype chain for the check. We can also set a custom logic in the static method `Symbol.hasInstance`. +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: @@ -68,7 +68,7 @@ The algorithm of `obj instanceof Class` works roughly as follows: 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` equals to one of prototypes in the `obj` prototype chain. +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 @@ -93,7 +93,7 @@ The algorithm of `obj instanceof Class` works roughly as follows: alert(rabbit instanceof Animal); // true */!* - // rabbit.__proto__ === Rabbit.prototype + // rabbit.__proto__ === Animal.prototype (no match) *!* // rabbit.__proto__.__proto__ === Animal.prototype (match!) */!* @@ -105,9 +105,9 @@ Here's the illustration of what `rabbit instanceof Animal` compares with `Animal 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. +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 `prototype` property is changed after the object is created. +That can lead to interesting consequences when a `prototype` property is changed after the object is created. Like here: @@ -186,11 +186,11 @@ let user = { alert( {}.toString.call(user) ); // [object User] ``` -For most environment-specific objects, there is such a property. Here are few browser specific examples: +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( window[Symbol.toStringTag]); // Window alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest alert( {}.toString.call(window) ); // [object Window] diff --git a/1-js/09-classes/06-instanceof/instanceof.svg b/1-js/09-classes/06-instanceof/instanceof.svg index 920be68b..d63b03a8 100644 --- a/1-js/09-classes/06-instanceof/instanceof.svg +++ b/1-js/09-classes/06-instanceof/instanceof.svg @@ -1 +1 @@ -Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file +Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file diff --git a/1-js/09-classes/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md index d5f1ab83..526b832e 100644 --- a/1-js/09-classes/07-mixins/article.md +++ b/1-js/09-classes/07-mixins/article.md @@ -69,7 +69,7 @@ let sayMixin = { }; let sayHiMixin = { - __proto__: sayMixin, // (or we could use Object.create to set the prototype here) + __proto__: sayMixin, // (or we could use Object.setPrototypeOf to set the prototype here) sayHi() { *!* @@ -101,21 +101,21 @@ 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 on the picture above. +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]]`, not `User.[[Prototype]]`. +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 is a great way to "broadcast information" to anyone who wants it. So let's make a mixin that allows to easily add event-related functions to any class/object. +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 `.trigger` call. -- ...And the method `.off(name, handler)` that removes `handler` listener. +- 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 become able to generate an event `"login"` when the visitor logs in. And another object, say, `calendar` may want to listen to such events to load the calendar for the logged-in person. +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. @@ -140,7 +140,7 @@ let eventMixin = { * menu.off('select', handler) */ off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; + let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { @@ -154,7 +154,7 @@ let eventMixin = { * this.trigger('select', data1, data2); */ trigger(eventName, ...args) { - if (!this._eventHandlers || !this._eventHandlers[eventName]) { + if (!this._eventHandlers?.[eventName]) { return; // no handlers for that event name } @@ -165,7 +165,7 @@ let eventMixin = { ``` -- `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name happens. Technically, there's `_eventHandlers` property, that stores an array of handlers for each event name. So it just adds it to the list. +- `.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`. @@ -193,7 +193,7 @@ menu.on("select", value => alert(`Value selected: ${value}`)); menu.choose("123"); ``` -Now if we'd like any code to react on menu selection, we can listen to it with `menu.on(...)`. +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. @@ -201,8 +201,8 @@ And `eventMixin` mixin makes it easy to add such behavior to as many classes as *Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes. -Some other languages like allow multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying methods into prototype. +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 multiple behaviors, like event-handling as we have seen above. +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. +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 index 77ea38b2..20e3a635 100644 --- a/1-js/09-classes/07-mixins/head.html +++ b/1-js/09-classes/07-mixins/head.html @@ -18,7 +18,7 @@ let eventMixin = { * menu.off('select', handler) */ off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; + let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for(let i = 0; i < handlers.length; i++) { if (handlers[i] == handler) { diff --git a/1-js/09-classes/07-mixins/mixin-inheritance.svg b/1-js/09-classes/07-mixins/mixin-inheritance.svg index 2e7ba8b3..1fdc2239 100644 --- a/1-js/09-classes/07-mixins/mixin-inheritance.svg +++ b/1-js/09-classes/07-mixins/mixin-inheritance.svg @@ -1 +1 @@ -sayHi: function sayBye: functionsayHiMixinsay: functionsayMixin[[Prototype]]constructor: User sayHi: function sayBye: functionUser.prototype[[Prototype]]name: ...user[[HomeObject] \ No newline at end of file +sayHi: function sayBye: functionsayHiMixinsay: functionsayMixin[[Prototype]]constructor: User sayHi: function sayBye: functionUser.prototype[[Prototype]]name: ...user[[HomeObject] \ No newline at end of file diff --git a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/solution.md b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/solution.md index 303431d6..ec0dabc9 100644 --- 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 @@ -1,8 +1,8 @@ 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`. +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. +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() { @@ -11,7 +11,7 @@ function f() { *!* return "result"; */!* - } catch (e) { + } catch (err) { /// ... } finally { alert('cleanup!'); @@ -28,11 +28,11 @@ function f() { try { alert('start'); throw new Error("an error"); - } catch (e) { + } catch (err) { // ... if("can't handle the error") { *!* - throw e; + throw err; */!* } diff --git a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md index c573cc23..b6dc8132 100644 --- a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md +++ b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md @@ -6,12 +6,12 @@ importance: 5 Compare the two code fragments. -1. The first one uses `finally` to execute the code after `try..catch`: +1. The first one uses `finally` to execute the code after `try...catch`: ```js try { work work - } catch (e) { + } catch (err) { handle errors } finally { *!* @@ -19,12 +19,12 @@ Compare the two code fragments. */!* } ``` -2. The second fragment puts the cleaning right after `try..catch`: +2. The second fragment puts the cleaning right after `try...catch`: ```js try { work work - } catch (e) { + } catch (err) { handle errors } diff --git a/1-js/10-error-handling/1-try-catch/article.md b/1-js/10-error-handling/1-try-catch/article.md index fa8ba5e9..a928da28 100644 --- a/1-js/10-error-handling/1-try-catch/article.md +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -1,14 +1,14 @@ -# Error handling, "try..catch" +# 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. +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 to "catch" errors and, instead of dying, do something more reasonable. +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" syntax -The `try..catch` construct has two main blocks: `try`, and then `catch`: +The `try...catch` construct has two main blocks: `try`, and then `catch`: ```js try { @@ -25,14 +25,14 @@ try { 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 `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. +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`. +So, an error inside the `try {...}` block does not kill the script -- we have a chance to handle it in `catch`. -Let's see examples. +Let's look at some examples. - An errorless example: shows `alert` `(1)` and `(2)`: @@ -45,7 +45,7 @@ Let's see examples. alert('End of try runs'); // *!*(2) <--*/!* - } catch(err) { + } catch (err) { alert('Catch is ignored, because there are no errors'); // (3) @@ -64,7 +64,7 @@ Let's see examples. alert('End of try (never reached)'); // (2) - } catch(err) { + } catch (err) { alert(`Error has occurred!`); // *!*(3) <--*/!* @@ -72,45 +72,45 @@ Let's see examples. ``` -````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. +````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) { +} 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 phrase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code. +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 the valid code. Such errors are called "runtime errors" or, sometimes, "exceptions". +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: +````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) { +} 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. +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: +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! + noSuchVariable; // try...catch handles the error! } catch { alert( "error is caught here!" ); } @@ -125,7 +125,7 @@ When an error occurs, JavaScript generates an object containing the details abou ```js try { // ... -} catch(err) { // <-- the "error object", could use another word instead of err +} catch (err) { // <-- the "error object", could use another word instead of err // ... } ``` @@ -150,7 +150,7 @@ try { *!* lalala; // error, variable is not defined! */!* -} catch(err) { +} catch (err) { alert(err.name); // ReferenceError alert(err.message); // lalala is not defined alert(err.stack); // ReferenceError: lalala is not defined at (...call stack) @@ -175,9 +175,9 @@ try { } ``` -## Using "try..catch" +## Using "try...catch" -Let's explore a real-life use case of `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. @@ -201,11 +201,11 @@ 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! +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: +Let's use `try...catch` to handle the error: ```js run let json = "{ bad json }"; @@ -217,12 +217,12 @@ try { */!* alert( user.name ); // doesn't work -} catch (e) { +} catch (err) { *!* // ...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 ); + alert( err.name ); + alert( err.message ); */!* } ``` @@ -245,7 +245,7 @@ try { alert( user.name ); // no name! */!* -} catch (e) { +} catch (err) { alert( "doesn't execute" ); } ``` @@ -294,11 +294,11 @@ Let's see what kind of error `JSON.parse` generates: ```js run try { JSON.parse("{ bad json o_O }"); -} catch(e) { +} catch (err) { *!* - alert(e.name); // SyntaxError + alert(err.name); // SyntaxError */!* - alert(e.message); // Unexpected token o in JSON at position 2 + alert(err.message); // Unexpected token b in JSON at position 2 } ``` @@ -323,8 +323,8 @@ try { alert( user.name ); -} catch(e) { - alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name +} catch (err) { + alert( "JSON Error: " + err.message ); // JSON Error: Incomplete data: no name } ``` @@ -334,9 +334,9 @@ Now `catch` became a single place for all error handling: both for `JSON.parse` ## 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 that "incorrect data" thing. +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. -Like this: +For example: ```js run let json = '{ "age": 30 }'; // incomplete data @@ -345,7 +345,7 @@ try { user = JSON.parse(json); // <-- forgot to put "let" before user // ... -} catch(err) { +} catch (err) { alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined // (no JSON Error actually) } @@ -353,29 +353,33 @@ try { 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 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. +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. -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: +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 `catch(err) {...}` block we analyze the error object `err`. -2. If we don't know how to handle it, then we do `throw err`. +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`: @@ -395,24 +399,24 @@ try { alert( user.name ); -} catch(e) { +} catch (err) { *!* - if (e.name == "SyntaxError") { - alert( "JSON Error: " + e.message ); + if (err instanceof SyntaxError) { + alert( "JSON Error: " + err.message ); } else { - throw e; // rethrow (*) + 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. +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`: +The example below demonstrates how such errors can be caught by one more level of `try...catch`: ```js run function readData() { @@ -423,11 +427,11 @@ function readData() { *!* blabla(); // error! */!* - } catch (e) { + } catch (err) { // ... - if (e.name != 'SyntaxError') { + if (!(err instanceof SyntaxError)) { *!* - throw e; // rethrow (don't know how to deal with it) + throw err; // rethrow (don't know how to deal with it) */!* } } @@ -435,20 +439,20 @@ function readData() { try { readData(); -} catch (e) { +} catch (err) { *!* - alert( "External catch got: " + e ); // caught it! + 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. +Here `readData` only knows how to handle `SyntaxError`, while the outer `try...catch` knows how to handle everything. -## try..catch..finally +## try...catch...finally Wait, that's not all. -The `try..catch` construct may have one more code clause: `finally`. +The `try...catch` construct may have one more code clause: `finally`. If it exists, it runs in all cases: @@ -460,7 +464,7 @@ The extended syntax looks like this: ```js *!*try*/!* { ... try to execute the code ... -} *!*catch*/!*(e) { +} *!*catch*/!* (err) { ... handle errors ... } *!*finally*/!* { ... execute always ... @@ -473,7 +477,7 @@ Try running this code: try { alert( 'try' ); if (confirm('Make an error?')) BAD_CODE(); -} catch (e) { +} catch (err) { alert( 'catch' ); } finally { alert( 'finally' ); @@ -509,7 +513,7 @@ let start = Date.now(); try { result = fib(num); -} catch (e) { +} catch (err) { result = 0; *!* } finally { @@ -522,19 +526,19 @@ 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, an the execution will take `0ms`. Both measurements are done correctly. +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`. +```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`. +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. @@ -546,7 +550,7 @@ function func() { return 1; */!* - } catch (e) { + } catch (err) { /* ... */ } finally { *!* @@ -559,9 +563,9 @@ alert( func() ); // first works alert from finally, and then this one ``` ```` -````smart header="`try..finally`" +````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. +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() { @@ -582,11 +586,11 @@ In the code above, an error inside `try` always falls out, because there's no `c 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. +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. +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, that will run in case of an uncaught error. +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: @@ -639,14 +643,14 @@ They work like this: ## 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 `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) { +} catch (err) { // if an error happened, then jump here // err is the error object } finally { @@ -654,7 +658,7 @@ try { } ``` -There may be no `catch` section or no `finally`, so shorter constructs `try..catch` and `try..finally` are also valid. +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: @@ -662,10 +666,10 @@ Error objects have following properties: - `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) {`. +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 to setup a "global" error handler to catch errors that "fall out". In-browser that's `window.onerror`. +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 index dd15063d..2c0d7134 100644 --- a/1-js/10-error-handling/1-try-catch/try-catch-flow.svg +++ b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg @@ -1 +1 @@ -BeginNo ErrorsAn error occured in the codeIgnore catch blockIgnore the rest of tryExecute catch blocktry { }// code... \ No newline at end of file +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 index bb6b74cf..754e68f9 100644 --- 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 @@ -2,7 +2,7 @@ class FormatError extends SyntaxError { constructor(message) { super(message); - this.name = "FormatError"; + this.name = this.constructor.name; } } diff --git a/1-js/10-error-handling/2-custom-errors/article.md b/1-js/10-error-handling/2-custom-errors/article.md index 2414ce7e..91828931 100644 --- a/1-js/10-error-handling/2-custom-errors/article.md +++ b/1-js/10-error-handling/2-custom-errors/article.md @@ -2,11 +2,11 @@ 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`. +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. +As the application grows, our own errors naturally form a hierarchy. For instance, `HttpTimeoutError` may inherit from `HttpError`, and so on. ## Extending Error @@ -21,9 +21,9 @@ Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it thr 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. +Our `ValidationError` class should inherit from the `Error` class. -That class is built-in, here's it approximate code, for us to understand what we're extending: +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 @@ -117,15 +117,15 @@ We could also look at `err.name`, like this: // 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 (due to a typo in the code or other unknown ones) should fall through. +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`). Let's make a more concrete class `PropertyRequiredError`, exactly for absent properties. It will carry additional information about the property that's missing. +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 { @@ -180,7 +180,7 @@ try { 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 ours custom errors from it. +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`. @@ -215,11 +215,39 @@ Now custom errors are much shorter, especially `ValidationError`, as we got rid 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` in the `catch` block, that check the class and handle known errors 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`? +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. -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. +The scheme is like this: -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`. +```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`: @@ -291,12 +319,12 @@ try { 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. +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 and more convenient to use for the calling code. It is widely used in object-oriented programming. +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, just need to take care of `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 the 3rd-party library and there's no easy way to get the class. Then `name` property can be used for such checks. +- 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/11-async/01-callbacks/article.md b/1-js/11-async/01-callbacks/article.md index c2f67c6c..57115a90 100644 --- a/1-js/11-async/01-callbacks/article.md +++ b/1-js/11-async/01-callbacks/article.md @@ -2,30 +2,44 @@ # Introduction: callbacks -Many actions in JavaScript are *asynchronous*. +```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. -For instance, take a look at the function `loadScript(src)`: +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 ``` -If we really need to make a window-level global variable, we can explicitly assign it to `window` and access as `window.user`. But that's an exception requiring a good reason. +```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 places, its code is executed only the first time, then exports are given to all importers. +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. -That has important consequences. Let's see that on examples. +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: @@ -129,9 +146,11 @@ import `./alert.js`; // Module is evaluated! import `./alert.js`; // (shows nothing) ``` -In practice, top-level module code is mostly used for initialization, creation of internal data structures, and if we want something to be reusable -- export it. +The second import shows nothing, because the module has already been evaluated. -Now, a more advanced example. +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: @@ -156,54 +175,67 @@ import {admin} from './admin.js'; alert(admin.name); // Pete *!* -// Both 1.js and 2.js imported the same object +// Both 1.js and 2.js reference the same admin object // Changes made in 1.js are visible in 2.js */!* ``` -So, let's reiterate -- the module is executed only once. Exports are generated, and then they are shared between importers, so if something changes the `admin` object, other modules will see that. +As you can see, when `1.js` changes the `name` property in the imported `admin`, then `2.js` can see the new `admin.name`. -Such behavior allows to *configure* modules on first import. We can setup its properties once, and then in further imports it's ready. +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. -For instance, `admin.js` module may provide certain functionality, but expect the credentials to come into the `admin` object from outside: +**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 admin = { }; +export let config = { }; export function sayHi() { - alert(`Ready to serve, ${admin.name}!`); + alert(`Ready to serve, ${config.user}!`); } ``` -In `init.js`, the first script of our app, we set `admin.name`. Then everyone will see it, including calls made from inside `admin.js` itself: +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 {admin} from './admin.js'; -admin.name = "Pete"; +import {config} from './admin.js'; +config.user = "Pete"; ``` -Another module can also see `admin.name`: +...Now the module `admin.js` is configured. + +Further importers can call it, and it correctly shows the current user: ```js -// 📁 other.js -import {admin, sayHi} from './admin.js'; - -alert(admin.name); // *!*Pete*/!* +// 📁 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: +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 ``` @@ -229,18 +261,18 @@ Compare it to non-module scripts, where `this` is a global object: There are also several browser-specific differences of scripts with `type="module"` compared to regular ones. -You may want skip this section for now if you're reading for the first time, or if you don't use JavaScript in a browser. +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 of external module scripts ` @@ -264,21 +296,21 @@ Compare to regular script below: ``` -Please note: the second script actually works before the first! So we'll see `undefined` first, and then `object`. +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 way wait for the document to be processed. The regular scripts runs immediately, so we saw its output first. +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 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. +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, `async` attribute only works on external scripts. Async scripts run immediately when ready, independently of other scripts or the HTML document. +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 any scripts. +For module scripts, it works on inline scripts as well. -For example, the script below has `async`, so it doesn't wait for anyone. +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 HTML document is not finished yet, or if other scripts are still pending. +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. @@ -296,7 +328,7 @@ That's good for functionality that doesn't depend on anything, like counters, ad External scripts that have `type="module"` are different in two aspects: -1. External scripts with same `src` run only once: +1. External scripts with the same `src` run only once: ```html @@ -322,11 +354,11 @@ 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 own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet. +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 the unknown type are just ignored. For them, it's possible to provide a fallback using `nomodule` attribute: +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 @@ -56,36 +62,36 @@ On the picture above, you can click on element nodes and their children will ope Every tree node is an object. -Tags are *element nodes* (or just elements), they form the tree structure: `` is at the root, then `` and `` are its children, etc. +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, 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). +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. ```

@@ -100,11 +106,11 @@ On further DOM pictures we'll sometimes omit them when they are irrelevant. Such ## 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:
@@ -117,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 document with unclosed tags: +A document with unclosed tags: ```html no-beautify

Hello @@ -126,7 +132,7 @@ Such document with unclosed tags:

  • 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:
    @@ -137,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: @@ -154,7 +160,7 @@ 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 @@ -167,7 +173,7 @@ For example, comments: - The truth about elks. + The truth about elk.
    1. An elk is a smart
    2. *!* @@ -182,7 +188,7 @@ For example, comments:
      @@ -193,7 +199,7 @@ We may think -- why is a comment added to the DOM? It doesn't affect the visual **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. @@ -202,29 +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. +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 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 particular 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. @@ -247,15 +253,15 @@ Now the last selected element is available as `$0`, the previously selected is ` 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. 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 DOM-node 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. @@ -273,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 121c11d7..00000000 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 00000000..eb99f193 --- /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 a8953395..00000000 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 c04f015c..00000000 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 00000000..02ef5f0a --- /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 ce0fa0ff..00000000 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/elks.html b/2-ui/1-document/02-dom-nodes/elk.html similarity index 86% rename from 2-ui/1-document/02-dom-nodes/elks.html rename to 2-ui/1-document/02-dom-nodes/elk.html index 7d29f3d4..dc5d65f5 100644 --- a/2-ui/1-document/02-dom-nodes/elks.html +++ b/2-ui/1-document/02-dom-nodes/elk.html @@ -1,7 +1,7 @@ - The truth about elks. + The truth about elk.
      1. An elk is a smart
      2. 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 00000000..448eea9d --- /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.png b/2-ui/1-document/02-dom-nodes/elks.png deleted file mode 100644 index 03177c40..00000000 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 e8a15bd5..00000000 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 075cf930..00000000 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 00000000..60696ec0 --- /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 8743dd29..00000000 Binary files a/2-ui/1-document/02-dom-nodes/inspect@2x.png and /dev/null differ diff --git a/2-ui/1-document/03-dom-navigation/article.md b/2-ui/1-document/03-dom-navigation/article.md index 3e327b47..b5f03098 100644 --- a/2-ui/1-document/03-dom-navigation/article.md +++ b/2-ui/1-document/03-dom-navigation/article.md @@ -149,7 +149,7 @@ There are two important consequences: The first thing is nice. The second is tolerable, because we can use `Array.from` to create a "real" array from the collection, if we want array methods: ```js run - alert( Array.from(document.body.childNodes).filter ); // now it's there + alert( Array.from(document.body.childNodes).filter ); // function ``` ```warn header="DOM collections are read-only" @@ -201,7 +201,7 @@ The parent is available as `parentNode`. For example: -```js +```js run // parent of is alert( document.body.parentNode === document.documentElement ); // true @@ -214,7 +214,7 @@ alert( document.body.previousSibling ); // HTMLHeadElement ## Element-only navigation -Navigation properties listed above refer to *all* nodes. For instance, in `childNodes` we can see both text nodes, element nodes, and even comment nodes if there exist. +Navigation properties listed above refer to *all* nodes. For instance, in `childNodes` we can see both text nodes, element nodes, and even comment nodes if they exist. But for many tasks we don't want text or comment nodes. We want to manipulate element nodes that represent tags and form the structure of the page. @@ -226,7 +226,7 @@ The links are similar to those given above, just with `Element` word inside: - `children` -- only those children that are element nodes. - `firstElementChild`, `lastElementChild` -- first and last element children. -- `previousElementSibling`, `nextElementSibling` -- neighbour elements. +- `previousElementSibling`, `nextElementSibling` -- neighbor elements. - `parentElement` -- parent element. ````smart header="Why `parentElement`? Can the parent be *not* an element?" @@ -280,12 +280,12 @@ Till now we described the basic navigation properties. Certain types of DOM elements may provide additional properties, specific to their type, for convenience. -Tables are a great example and a particularly important case for that. +Tables are a great example of that, and represent a particularly important case: **The ``** element supports (in addition to the given above) these properties: - `table.rows` -- the collection of `` elements of the table. - `table.caption/tHead/tFoot` -- references to elements ``, ``. -- `table.tBodies` -- the collection of `` elements (can be many according to the standard). +- `table.tBodies` -- the collection of `` elements (can be many according to the standard, but there will always be at least one -- even if it is not in the source HTML, the browser will put it in the DOM). **``, ``, ``** elements provide the `rows` property: - `tbody.rows` -- the collection of `` inside. @@ -311,8 +311,9 @@ An example of usage:
        `, `
        ``` @@ -320,9 +321,9 @@ The specification: [tabular data](https://html.spec.whatwg.org/multipage/tables. There are also additional navigation properties for HTML forms. We'll look at them later when we start working with forms. -# Summary +## Summary -Given a DOM node, we can go to its immediate neighbours using navigation properties. +Given a DOM node, we can go to its immediate neighbors using navigation properties. There are two main sets of them: diff --git a/2-ui/1-document/03-dom-navigation/dom-links-elements.svg b/2-ui/1-document/03-dom-navigation/dom-links-elements.svg index 20da46c1..fd0b2826 100644 --- a/2-ui/1-document/03-dom-navigation/dom-links-elements.svg +++ b/2-ui/1-document/03-dom-navigation/dom-links-elements.svg @@ -1 +1 @@ -document.documentElement <HTML>document.body (if inside body)parent Element<DIV>next Element Siblingprevious Element Siblingchildrenfirst Element Child last Element Child \ No newline at end of file +document.documentElement <HTML>document.body (if inside body)parent Element<DIV>next Element Siblingprevious Element Siblingchildrenfirst Element Child last Element Child \ No newline at end of file diff --git a/2-ui/1-document/03-dom-navigation/dom-links.svg b/2-ui/1-document/03-dom-navigation/dom-links.svg index fccbff1d..6c34bca4 100644 --- a/2-ui/1-document/03-dom-navigation/dom-links.svg +++ b/2-ui/1-document/03-dom-navigation/dom-links.svg @@ -1 +1 @@ -documentdocument.documentElement <HTML>document.body (if inside body)parentNode<DIV>nextSiblingpreviousSiblingchildNodesfirstChild lastChild \ No newline at end of file +documentdocument.documentElement <HTML>document.body (if inside body)parentNode<DIV>nextSiblingpreviousSiblingchildNodesfirstChild lastChild \ No newline at end of file diff --git a/2-ui/1-document/04-searching-elements-dom/article.md b/2-ui/1-document/04-searching-elements-dom/article.md index cd988f73..de47eac9 100644 --- a/2-ui/1-document/04-searching-elements-dom/article.md +++ b/2-ui/1-document/04-searching-elements-dom/article.md @@ -6,40 +6,7 @@ There are additional searching methods for that. ## document.getElementById or just id -If an element has the `id` attribute, then there's a global variable by the name from that `id`. - -We can use it to immediately access the element no matter where it is: - -```html run -
        -
        Element
        -
        - - -``` - -The behavior is described [in the specification](http://www.whatwg.org/specs/web-apps/current-work/#dom-window-nameditem), but it is supported mainly for compatibility. The browser tries to help us by mixing namespaces of JS and DOM. Good for very simple scripts, but there may be name conflicts. Also, when we look in JS and don't have HTML in view, it's not obvious where the variable comes from. - -If we declare a variable with the same name, it takes precedence: - -```html run untrusted height=0 -
        - - -``` - -The better alternative is to use a special method `document.getElementById(id)`. +If an element has the `id` attribute, we can get the element using the method `document.getElementById(id)`, no matter where it is. For instance: @@ -49,24 +16,62 @@ For instance: ``` -Here in the tutorial we'll often use `id` to directly reference an element, but that's only to keep things short. In real life `document.getElementById` is the preferred method. +Also, there's a global variable named by `id` that references the element: -```smart header="There can be only one" -The `id` must be unique. There can be only one element in the document with the given `id`. +```html run +
        +
        Element
        +
        -If there are multiple elements with the same `id`, then the behavior of corresponding methods is unpredictable. The browser may return any of them at random. So please stick to the rule and keep `id` unique. + ``` -```warn header="Only `document.getElementById`, not `anyNode.getElementById`" -The method `getElementById` that can be called only on `document` object. It looks for the given `id` in the whole document. +...That's unless we declare a JavaScript variable with the same name, then it takes precedence: + +```html run untrusted height=0 +
        + + +``` + +```warn header="Please don't use id-named global variables to access elements" +This behavior is described [in the specification](http://www.whatwg.org/specs/web-apps/current-work/#dom-window-nameditem), so it's a kind of standard. But it is supported mainly for compatibility. + +The browser tries to help us by mixing namespaces of JS and DOM. That's fine for simple scripts, inlined into HTML, but generally isn't a good thing. There may be naming conflicts. Also, when one reads JS code and doesn't have HTML in view, it's not obvious where the variable comes from. + +Here in the tutorial we use `id` to directly reference an element for brevity, when it's obvious where the element comes from. + +In real life `document.getElementById` is the preferred method. +``` + +```smart header="The `id` must be unique" +The `id` must be unique. There can be only one element in the document with the given `id`. + +If there are multiple elements with the same `id`, then the behavior of methods that use it is unpredictable, e.g. `document.getElementById` may return any of such elements at random. So please stick to the rule and keep `id` unique. +``` + +```warn header="Only `document.getElementById`, not `anyElem.getElementById`" +The method `getElementById` can be called only on `document` object. It looks for the given `id` in the whole document. ``` ## querySelectorAll [#querySelectorAll] @@ -98,7 +103,7 @@ Here we look for all `
      3. ` elements that are last children: This method is indeed powerful, because any CSS selector can be used. ```smart header="Can use pseudo-classes as well" -Pseudo-classes in the CSS selector like `:hover` and `:active` are also supported. For instance, `document.querySelectorAll(':hover')` will return the collection with elements that the pointer is over now (in nesting order: from the outermost `` to the most nested one). +Pseudo-classes in the CSS selector like `:hover` and `:active` are also supported. For instance, `document.querySelectorAll(':hover')` will return the collection with elements that the pointer is over now (in nesting order: from the outermost `` to the most nested one). ``` ## querySelector [#querySelector] @@ -111,9 +116,9 @@ In other words, the result is the same as `elem.querySelectorAll(css)[0]`, but t Previous methods were searching the DOM. -The [elem.matches(css)](http://dom.spec.whatwg.org/#dom-element-matches) does not look for anything, it merely checks if `elem` matches the given CSS-selector. It returns `true` or `false`. +The [elem.matches(css)](https://dom.spec.whatwg.org/#dom-element-matches) does not look for anything, it merely checks if `elem` matches the given CSS-selector. It returns `true` or `false`. -The method comes in handy when we are iterating over elements (like in array or something) and trying to filter those that interest us. +The method comes in handy when we are iterating over elements (like in an array or something) and trying to filter out those that interest us. For instance: @@ -137,7 +142,7 @@ For instance: *Ancestors* of an element are: parent, the parent of parent, its parent and so on. The ancestors together form the chain of parents from the element to the top. -The method `elem.closest(css)` looks the nearest ancestor that matches the CSS-selector. The `elem` itself is also included in the search. +The method `elem.closest(css)` looks for the nearest ancestor that matches the CSS-selector. The `elem` itself is also included in the search. In other words, the method `closest` goes up from the element and checks each of parents. If it matches the selector, then the search stops, and the ancestor is returned. @@ -149,7 +154,7 @@ For instance:
        • Chapter 1
        • -
        • Chapter 1
        • +
        • Chapter 2
        @@ -173,7 +178,7 @@ So here we cover them mainly for completeness, while you can still find them in - `elem.getElementsByTagName(tag)` looks for elements with the given tag and returns the collection of them. The `tag` parameter can also be a star `"*"` for "any tags". - `elem.getElementsByClassName(className)` returns elements that have the given CSS class. -- `document.getElementsByName(name)` returns elements with the given `name` attribute, document-wide. very rarely used. +- `document.getElementsByName(name)` returns elements with the given `name` attribute, document-wide. Very rarely used. For instance: ```js @@ -358,7 +363,7 @@ There are 6 main methods to search for nodes in DOM: -By far the most used are `querySelector` and `querySelectorAll`, but `getElementBy*` can be sporadically helpful or found in the old scripts. +By far the most used are `querySelector` and `querySelectorAll`, but `getElement(s)By*` can be sporadically helpful or found in the old scripts. Besides that: diff --git a/2-ui/1-document/05-basic-dom-node-properties/article.md b/2-ui/1-document/05-basic-dom-node-properties/article.md index 6c0053d0..99dde5bc 100644 --- a/2-ui/1-document/05-basic-dom-node-properties/article.md +++ b/2-ui/1-document/05-basic-dom-node-properties/article.md @@ -10,7 +10,7 @@ Different DOM nodes may have different properties. For instance, an element node Each DOM node belongs to the corresponding built-in class. -The root of the hierarchy is [EventTarget](https://dom.spec.whatwg.org/#eventtarget), that is inherited by [Node](http://dom.spec.whatwg.org/#interface-node), and other DOM nodes inherit from it. +The root of the hierarchy is [EventTarget](https://dom.spec.whatwg.org/#eventtarget), that is inherited by [Node](https://dom.spec.whatwg.org/#interface-node), and other DOM nodes inherit from it. Here's the picture, explanations to follow: @@ -18,16 +18,39 @@ Here's the picture, explanations to follow: The classes are: -- [EventTarget](https://dom.spec.whatwg.org/#eventtarget) -- is the root "abstract" class. Objects of that class are never created. It serves as a base, so that all DOM nodes support so-called "events", we'll study them later. -- [Node](http://dom.spec.whatwg.org/#interface-node) -- is also an "abstract" class, serving as a base for DOM nodes. It provides the core tree functionality: `parentNode`, `nextSibling`, `childNodes` and so on (they are getters). Objects of `Node` class are never created. But there are concrete node classes that inherit from it, namely: `Text` for text nodes, `Element` for element nodes and more exotic ones like `Comment` for comment nodes. -- [Element](http://dom.spec.whatwg.org/#interface-element) -- is a base class for DOM elements. It provides element-level navigation like `nextElementSibling`, `children` and searching methods like `getElementsByTagName`, `querySelector`. A browser supports not only HTML, but also XML and SVG. The `Element` class serves as a base for more specific classes: `SVGElement`, `XMLElement` and `HTMLElement`. -- [HTMLElement](https://html.spec.whatwg.org/multipage/dom.html#htmlelement) -- is finally the basic class for all HTML elements. It is inherited by concrete HTML elements: +- [EventTarget](https://dom.spec.whatwg.org/#eventtarget) -- is the root "abstract" class for everything. + + Objects of that class are never created. It serves as a base, so that all DOM nodes support so-called "events", we'll study them later. + +- [Node](https://dom.spec.whatwg.org/#interface-node) -- is also an "abstract" class, serving as a base for DOM nodes. + + It provides the core tree functionality: `parentNode`, `nextSibling`, `childNodes` and so on (they are getters). Objects of `Node` class are never created. But there are other classes that inherit from it (and so inherit the `Node` functionality). + +- [Document](https://dom.spec.whatwg.org/#interface-document), for historical reasons often inherited by `HTMLDocument` (though the latest spec doesn't dictate it) -- is a document as a whole. + + The `document` global object belongs exactly to this class. It serves as an entry point to the DOM. + +- [CharacterData](https://dom.spec.whatwg.org/#interface-characterdata) -- an "abstract" class, inherited by: + - [Text](https://dom.spec.whatwg.org/#interface-text) -- the class corresponding to a text inside elements, e.g. `Hello` in `

        Hello

        `. + - [Comment](https://dom.spec.whatwg.org/#interface-comment) -- the class for comments. They are not shown, but each comment becomes a member of DOM. + +- [Element](https://dom.spec.whatwg.org/#interface-element) -- is the base class for DOM elements. + + It provides element-level navigation like `nextElementSibling`, `children` and searching methods like `getElementsByTagName`, `querySelector`. + + A browser supports not only HTML, but also XML and SVG. So the `Element` class serves as a base for more specific classes: `SVGElement`, `XMLElement` (we don't need them here) and `HTMLElement`. + +- Finally, [HTMLElement](https://html.spec.whatwg.org/multipage/dom.html#htmlelement) is the basic class for all HTML elements. We'll work with it most of the time. + + It is inherited by concrete HTML elements: - [HTMLInputElement](https://html.spec.whatwg.org/multipage/forms.html#htmlinputelement) -- the class for `` elements, - [HTMLBodyElement](https://html.spec.whatwg.org/multipage/semantics.html#htmlbodyelement) -- the class for `` elements, - [HTMLAnchorElement](https://html.spec.whatwg.org/multipage/semantics.html#htmlanchorelement) -- the class for `` elements, - - ...and so on, each tag has its own class that may provide specific properties and methods. + - ...and so on. -So, the full set of properties and methods of a given node comes as the result of the inheritance. +There are many other tags with their own classes that may have specific properties and methods, while some elements, such as ``, `
        `, `
        ` do not have any specific properties, so they are instances of `HTMLElement` class. + +So, the full set of properties and methods of a given node comes as the result of the chain of inheritance. For example, let's consider the DOM object for an `` element. It belongs to [HTMLInputElement](https://html.spec.whatwg.org/multipage/forms.html#htmlinputelement) class. @@ -36,7 +59,7 @@ It gets properties and methods as a superposition of (listed in inheritance orde - `HTMLInputElement` -- this class provides input-specific properties, - `HTMLElement` -- it provides common HTML element methods (and getters/setters), - `Element` -- provides generic element methods, -- `Node` -- provides common DOM node properties,. +- `Node` -- provides common DOM node properties, - `EventTarget` -- gives the support for events (to be covered), - ...and finally it inherits from `Object`, so "plain object" methods like `hasOwnProperty` are also available. @@ -128,13 +151,13 @@ For instance: ```html run - ``` -**Beware: unlike `innerHTML`, writing to `outerHTML` does not change the element. Instead, it replaces it as a whole in the outer context.** +**Beware: unlike `innerHTML`, writing to `outerHTML` does not change the element. Instead, it replaces it in the DOM.** Yeah, sounds strange, and strange it is, that's why we make a separate note about it here. Take a look. @@ -302,7 +325,7 @@ Consider the example: div.outerHTML = '

        A new element

        '; // (*) *!* - // Wow! The div is still the same! + // Wow! 'div' is still the same! */!* alert(div.outerHTML); //
        Hello, world!
        (**) @@ -310,18 +333,18 @@ Consider the example: Looks really odd, right? -In the line `(*)` we replaced `div` with `

        A new element

        `. In the outer document we can see the new content instead of the `
        `. But, as we can see in line `(**)`, the old `div` variable is still the same! +In the line `(*)` we replaced `div` with `

        A new element

        `. In the outer document (the DOM) we can see the new content instead of the `
        `. But, as we can see in line `(**)`, the value of the old `div` variable hasn't changed! -The `outerHTML` assignment does not modify the DOM element, but removes it from the outer context and inserts a new piece of HTML instead of it. +The `outerHTML` assignment does not modify the DOM element (the object referenced by, in this case, the variable 'div'), but removes it from the DOM and inserts the new HTML in its place. So what happened in `div.outerHTML=...` is: - `div` was removed from the document. -- Another HTML `

        A new element

        ` was inserted instead. -- `div` still has the old value. The new HTML wasn't saved to any variable. +- Another piece of HTML `

        A new element

        ` was inserted in its place. +- `div` still has its old value. The new HTML wasn't saved to any variable. It's so easy to make an error here: modify `div.outerHTML` and then continue to work with `div` as if it had the new content in it. But it doesn't. Such thing is correct for `innerHTML`, but not for `outerHTML`. -We can write to `elem.outerHTML`, but should keep in mind that it doesn't change the element we're writing to. It creates the new HTML on its place instead. We can get references to new elements by querying DOM. +We can write to `elem.outerHTML`, but should keep in mind that it doesn't change the element we're writing to ('elem'). It puts the new HTML in its place instead. We can get references to the new elements by querying the DOM. ## nodeValue/data: text node content @@ -397,7 +420,7 @@ Compare the two:
        ``` -But there are exclusions, for instance `input.value` synchronizes only from attribute -> to property, but not back: +But there are exclusions, for instance `input.value` synchronizes only from attribute -> property, but not back: ```html run @@ -298,16 +298,16 @@ For instance, here for the order state the attribute `order-state` is used:
        ``` -Why the attribute may be preferable to classes like `.order-state-new`, `.order-state-pending`, `order-state-canceled`? +Why would using an attribute be preferable to having classes like `.order-state-new`, `.order-state-pending`, `.order-state-canceled`? -That's because an attribute is more convenient to manage. The state can be changed as easy as: +Because an attribute is more convenient to manage. The state can be changed as easy as: ```js // a bit simpler than removing old/adding a new class div.setAttribute('order-state', 'canceled'); ``` -But there may be a possible problem with custom attributes. What if we use a non-standard attribute for our purposes and later the standard introduces it and makes it do something? The HTML language is alive, it grows, more attributes appear to suit the needs of developers. There may be unexpected effects in such case. +But there may be a possible problem with custom attributes. What if we use a non-standard attribute for our purposes and later the standard introduces it and makes it do something? The HTML language is alive, it grows, and more attributes appear to suit the needs of developers. There may be unexpected effects in such case. To avoid conflicts, there exist [data-*](https://html.spec.whatwg.org/#embedding-custom-non-visible-data-with-the-data-*-attributes) attributes. diff --git a/2-ui/1-document/07-modifying-document/1-createtextnode-vs-innerhtml/task.md b/2-ui/1-document/07-modifying-document/1-createtextnode-vs-innerhtml/task.md index e127bc0e..40c75dff 100644 --- a/2-ui/1-document/07-modifying-document/1-createtextnode-vs-innerhtml/task.md +++ b/2-ui/1-document/07-modifying-document/1-createtextnode-vs-innerhtml/task.md @@ -6,7 +6,7 @@ importance: 5 We have an empty DOM element `elem` and a string `text`. -Which of these 3 commands do exactly the same? +Which of these 3 commands will do exactly the same? 1. `elem.append(document.createTextNode(text))` 2. `elem.innerHTML = text` diff --git a/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.md b/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.md index 15238fcf..1414e90c 100644 --- a/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.md +++ b/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.md @@ -39,15 +39,19 @@ The clock-managing functions: ```js let timerId; -function clockStart() { // run the clock - timerId = setInterval(update, 1000); +function clockStart() { // run the clock + if (!timerId) { // only set a new interval if the clock is not running + timerId = setInterval(update, 1000); + } update(); // (*) } function clockStop() { clearInterval(timerId); - timerId = null; + timerId = null; // (**) } ``` Please note that the call to `update()` is not only scheduled in `clockStart()`, but immediately run in the line `(*)`. Otherwise the visitor would have to wait till the first execution of `setInterval`. And the clock would be empty till then. + +Also it is important to set a new interval in `clockStart()` only when the clock is not running. Otherways clicking the start button several times would set multiple concurrent intervals. Even worse - we would only keep the `timerID` of the last interval, losing references to all others. Then we wouldn't be able to stop the clock ever again! Note that we need to clear the `timerID` when the clock is stopped in the line `(**)`, so that it can be started again by running `clockStart()`. diff --git a/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.view/index.html b/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.view/index.html index 1bf642b1..84ee26f1 100644 --- a/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.view/index.html +++ b/2-ui/1-document/07-modifying-document/10-clock-setinterval/solution.view/index.html @@ -43,15 +43,19 @@ } function clockStart() { - timerId = setInterval(update, 1000); + // set a new interval only if the clock is stopped + // otherwise we would rewrite the timerID reference to the running interval and wouldn't be able to stop the clock ever again + if (!timerId) { + timerId = setInterval(update, 1000); + } update(); // <-- start right now, don't wait 1 second till the first setInterval works } function clockStop() { clearInterval(timerId); + timerId = null; // <-- clear timerID to indicate that the clock has been stopped, so that it is possible to start it again in clockStart() } - clockStart(); diff --git a/2-ui/1-document/07-modifying-document/12-sort-table/solution.md b/2-ui/1-document/07-modifying-document/12-sort-table/solution.md index 25dd58b0..49243e8e 100644 --- a/2-ui/1-document/07-modifying-document/12-sort-table/solution.md +++ b/2-ui/1-document/07-modifying-document/12-sort-table/solution.md @@ -1,19 +1,18 @@ The solution is short, yet may look a bit tricky, so here I provide it with extensive comments: - ```js -let sortedRows = Array.from(table.rows) - .slice(1) - .sort((rowA, rowB) => rowA.cells[0].innerHTML > rowB.cells[0].innerHTML ? 1 : -1); +let sortedRows = Array.from(table.tBodies[0].rows) // 1 + .sort((rowA, rowB) => rowA.cells[0].innerHTML.localeCompare(rowB.cells[0].innerHTML)); -table.tBodies[0].append(...sortedRows); +table.tBodies[0].append(...sortedRows); // (3) ``` -1. Get all ``, like `table.querySelectorAll('tr')`, then make an array from them, cause we need array methods. -2. The first TR (`table.rows[0]`) is actually a table header, so we take the rest by `.slice(1)`. -3. Then sort them comparing by the content of the first `` (the name field). -4. Now insert nodes in the right order by `.append(...sortedRows)`. +The step-by-step algorthm: - Tables always have an implicit element, so we need to take it and insert into it: a simple `table.append(...)` would fail. +1. Get all ``, from ``. +2. Then sort them comparing by the content of the first `` (the name field). +3. Now insert nodes in the right order by `.append(...sortedRows)`. - Please note: we don't have to remove them, just "re-insert", they leave the old place automatically. +We don't have to remove row elements, just "re-insert", they leave the old place automatically. + +P.S. In our case, there's an explicit `` in the table, but even if HTML table doesn't have ``, the DOM structure always has it. diff --git a/2-ui/1-document/07-modifying-document/12-sort-table/solution.view/index.html b/2-ui/1-document/07-modifying-document/12-sort-table/solution.view/index.html index 81e98574..40692031 100644 --- a/2-ui/1-document/07-modifying-document/12-sort-table/solution.view/index.html +++ b/2-ui/1-document/07-modifying-document/12-sort-table/solution.view/index.html @@ -1,37 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - - -
        NameSurnameAge
        JohnSmith10
        PeteBrown15
        AnnLee5
        + + + + + + + + + + + + + + + + + + + + +
        NameSurnameAge
        JohnSmith10
        PeteBrown15
        AnnLee5
        .........
        - - - - + table.tBodies[0].append(...sortedRows); + diff --git a/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html b/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html index e41eb229..9071c88e 100644 --- a/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html +++ b/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html @@ -1,33 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - - - -
        NameSurnameAge
        JohnSmith10
        PeteBrown15
        AnnLee5
        + + + + + + + + + + + + + + + + + + + + +
        NameSurnameAge
        JohnSmith10
        PeteBrown15
        AnnLee5
        .........
        - - - - + diff --git a/2-ui/1-document/07-modifying-document/12-sort-table/task.md b/2-ui/1-document/07-modifying-document/12-sort-table/task.md index 41d6fca2..7cdba35b 100644 --- a/2-ui/1-document/07-modifying-document/12-sort-table/task.md +++ b/2-ui/1-document/07-modifying-document/12-sort-table/task.md @@ -6,33 +6,29 @@ importance: 5 There's a table: +```html run - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + +
        NameSurnameAge
        JohnSmith10
        PeteBrown15
        AnnLee5
        .........
        NameSurnameAge
        JohnSmith10
        PeteBrown15
        AnnLee5
        .........
        +``` There may be more rows in it. diff --git a/2-ui/1-document/07-modifying-document/5-why-aaa/solution.md b/2-ui/1-document/07-modifying-document/5-why-aaa/solution.md index 6b85168b..3d1f6698 100644 --- a/2-ui/1-document/07-modifying-document/5-why-aaa/solution.md +++ b/2-ui/1-document/07-modifying-document/5-why-aaa/solution.md @@ -1,9 +1,9 @@ The HTML in the task is incorrect. That's the reason of the odd thing. -The browser has to fix it automatically. But there may be no text inside the ``: according to the spec only table-specific tags are allowed. So the browser adds `"aaa"` *before* the `
        `. +The browser has to fix it automatically. But there may be no text inside the `
        `: according to the spec only table-specific tags are allowed. So the browser shows `"aaa"` *before* the `
        `. Now it's obvious that when we remove the table, it remains. -The question can be easily answered by exploring the DOM using the browser tools. It shows `"aaa"` before the `
        `. +The question can be easily answered by exploring the DOM using the browser tools. You'll see `"aaa"` before the `
        `. The HTML standard specifies in detail how to process bad HTML, and such behavior of the browser is correct. diff --git a/2-ui/1-document/07-modifying-document/5-why-aaa/task.md b/2-ui/1-document/07-modifying-document/5-why-aaa/task.md index 03064ed2..861f7050 100644 --- a/2-ui/1-document/07-modifying-document/5-why-aaa/task.md +++ b/2-ui/1-document/07-modifying-document/5-why-aaa/task.md @@ -4,7 +4,11 @@ importance: 1 # Why does "aaa" remain? -Run the example. Why does `table.remove()` not delete the text `"aaa"`? +In the example below, the call `table.remove()` removes the table from the document. + +But if you run it, you can see that the text `"aaa"` is still visible. + +Why does that happen? ```html height=100 run
        @@ -18,6 +22,6 @@ Run the example. Why does `table.remove()` not delete the text `"aaa"`? alert(table); // the table, as it should be table.remove(); - // why there's still aaa in the document? + // why there's still "aaa" in the document? ``` diff --git a/2-ui/1-document/07-modifying-document/6-create-list/task.md b/2-ui/1-document/07-modifying-document/6-create-list/task.md index 43b0a34a..a57e7e2d 100644 --- a/2-ui/1-document/07-modifying-document/6-create-list/task.md +++ b/2-ui/1-document/07-modifying-document/6-create-list/task.md @@ -10,7 +10,7 @@ For every list item: 1. Ask a user about its content using `prompt`. 2. Create the `
      4. ` with it and add it to `
          `. -3. Continue until the user cancels the input (by pressing `key:Esc` or CANCEL in prompt). +3. Continue until the user cancels the input (by pressing `key:Esc` or via an empty entry). All elements should be created dynamically. diff --git a/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md b/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md index 67bb5e13..de8be56e 100644 --- a/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md +++ b/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md @@ -3,7 +3,7 @@ We'll create the table as a string: `"
      5. ...
        "`, and then assign it t The algorithm: 1. Create the table header with `` and weekday names. -1. Create the date object `d = new Date(year, month-1)`. That's the first day of `month` (taking into account that months in JavaScript start from `0`, not `1`). -2. First few cells till the first day of the month `d.getDay()` may be empty. Let's fill them in with ``. -3. Increase the day in `d`: `d.setDate(d.getDate()+1)`. If `d.getMonth()` is not yet the next month, then add the new cell `` to the calendar. If that's a Sunday, then add a newline "</tr><tr>". -4. If the month has finished, but the table row is not yet full, add empty `` into it, to make it square. +2. Create the date object `d = new Date(year, month-1)`. That's the first day of `month` (taking into account that months in JavaScript start from `0`, not `1`). +3. First few cells till the first day of the month `d.getDay()` may be empty. Let's fill them in with ``. +4. Increase the day in `d`: `d.setDate(d.getDate()+1)`. If `d.getMonth()` is not yet the next month, then add the new cell `` to the calendar. If that's a Sunday, then add a newline "</tr><tr>". +5. If the month has finished, but the table row is not yet full, add empty `` into it, to make it square. diff --git a/2-ui/1-document/07-modifying-document/article.md b/2-ui/1-document/07-modifying-document/article.md index 9cdb79dd..75ce1fbb 100644 --- a/2-ui/1-document/07-modifying-document/article.md +++ b/2-ui/1-document/07-modifying-document/article.md @@ -1,12 +1,12 @@ # Modifying the document -DOM modifications is the key to create "live" pages. +DOM modification is the key to creating "live" pages. Here we'll see how to create new elements "on the fly" and modify the existing page content. ## Example: show a message -Let's see the methods on example. We'll add a message on the page that looks nicer than `alert`. +Let's demonstrate using an example. We'll add a message on the page that looks nicer than `alert`. Here's how it will look: @@ -28,7 +28,7 @@ Here's how it will look: */!* ``` -That was an HTML example. Now let's create the same `div` with JavaScript (assuming that the styles are in the HTML or an external CSS file). +That was the HTML example. Now let's create the same `div` with JavaScript (assuming that the styles are in the HTML/CSS already). ## Creating an element @@ -48,21 +48,28 @@ To create DOM nodes, there are two methods: let textNode = document.createTextNode('Here I am'); ``` +Most of the time we need to create element nodes, such as the `div` for the message. + ### Creating the message -In our case the message is a `div` with `alert` class and the HTML in it: +Creating the message div takes 3 steps: ```js +// 1. Create
        element let div = document.createElement('div'); + +// 2. Set its class to "alert" div.className = "alert"; + +// 3. Fill it with the content div.innerHTML = "Hi there! You've read an important message."; ``` -We created the element, but as of now it's only in a variable. We can't see the element on the page, as it's not yet a part of the document. +We've created the element. But as of now it's only in a variable named `div`, not in the page yet. So we can't see it. ## Insertion methods -To make the `div` show up, we need to insert it somewhere into `document`. For instance, in `document.body`. +To make the `div` show up, we need to insert it somewhere into `document`. For instance, into `` element, referenced by `document.body`. There's a special method `append` for that: `document.body.append(div)`. @@ -90,14 +97,20 @@ Here's the full code: ``` -This set of methods provides more ways to insert: +Here we called `append` on `document.body`, but we can call `append` method on any other element, to put another element into it. For instance, we can append something to `
        ` by calling `div.append(anotherElement)`. -- `node.append(...nodes or strings)` -- append nodes or strings at the end of `node`, -- `node.prepend(...nodes or strings)` -- insert nodes or strings into the beginning of `node`, -- `node.before(...nodes or strings)` –- insert nodes or strings before the `node`, -- `node.after(...nodes or strings)` –- insert nodes or strings after the `node`, +Here are more insertion methods, they specify different places where to insert: + +- `node.append(...nodes or strings)` -- append nodes or strings *at the end* of `node`, +- `node.prepend(...nodes or strings)` -- insert nodes or strings *at the beginning* of `node`, +- `node.before(...nodes or strings)` –- insert nodes or strings *before* `node`, +- `node.after(...nodes or strings)` –- insert nodes or strings *after* `node`, - `node.replaceWith(...nodes or strings)` –- replaces `node` with the given nodes or strings. +Arguments of these methods are an arbitrary list of DOM nodes to insert, or text strings (that become text nodes automatically). + +Let's see them in action. + Here's an example of using these methods to add items to a list and the text before/after it: ```html autorun @@ -121,7 +134,7 @@ Here's an example of using these methods to add items to a list and the text bef ``` -Here's a visual picture what methods do: +Here's a visual picture of what the methods do: ![](before-prepend-append-after.svg) @@ -139,7 +152,7 @@ before after ``` -These methods can insert multiple lists of nodes and text pieces in a single call. +As said, these methods can insert multiple nodes and text pieces in a single call. For instance, here a string and an element are inserted: @@ -150,7 +163,7 @@ For instance, here a string and an element are inserted: ``` -All text is inserted *as text*. +Please note: the text is inserted "as text", not "as HTML", with proper escaping of characters such as `<`, `>`. So the final HTML is: @@ -166,7 +179,7 @@ In other words, strings are inserted in a safe way, like `elem.textContent` does So, these methods can only be used to insert DOM nodes or text pieces. -But what if we want to insert HTML "as html", with all tags and stuff working, like `elem.innerHTML`? +But what if we'd like to insert an HTML string "as html", with all tags and stuff working, in the same manner as `elem.innerHTML` does it? ## insertAdjacentHTML/Text/Element @@ -199,7 +212,7 @@ For instance:

        Bye

        ``` -That's how we can append an arbitrary HTML to the page. +That's how we can append arbitrary HTML to the page. Here's the picture of insertion variants: @@ -534,13 +547,13 @@ So if we need to add a lot of text into HTML dynamically, and we're at page load All these methods return `node`. -- Given a piece of HTML: `elem.insertAdjacentHTML(where, html)`, inserts depending on `where`: +- Given some HTML in `html`, `elem.insertAdjacentHTML(where, html)` inserts it depending on the value of `where`: - `"beforebegin"` -- insert `html` right before `elem`, - `"afterbegin"` -- insert `html` into `elem`, at the beginning, - `"beforeend"` -- insert `html` into `elem`, at the end, - `"afterend"` -- insert `html` right after `elem`. - Also there are similar methods `elem.insertAdjacentText` and `elem.insertAdjacentElement`, they insert text strings and elements, but they are rarely used. + Also there are similar methods, `elem.insertAdjacentText` and `elem.insertAdjacentElement`, that insert text strings and elements, but they are rarely used. - To append HTML to the page before it has finished loading: - `document.write(html)` diff --git a/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg b/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg index 1811adea..0843713c 100644 --- a/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg +++ b/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg @@ -1 +1 @@ -ol.afterol.appendol.prependol.before(…nodes or strings) \ No newline at end of file +ol.afterol.appendol.prependol.before(…nodes or strings) \ No newline at end of file diff --git a/2-ui/1-document/07-modifying-document/insert-adjacent.svg b/2-ui/1-document/07-modifying-document/insert-adjacent.svg index 4b247fd7..e26fd023 100644 --- a/2-ui/1-document/07-modifying-document/insert-adjacent.svg +++ b/2-ui/1-document/07-modifying-document/insert-adjacent.svg @@ -1 +1 @@ -ol.insertAdjacentHTML(*, html)afterendbeforeendafterbeginbeforebegin \ No newline at end of file +ol.insertAdjacentHTML(*, html)afterendbeforeendafterbeginbeforebegin \ No newline at end of file diff --git a/2-ui/1-document/08-styles-and-classes/article.md b/2-ui/1-document/08-styles-and-classes/article.md index e6357c8e..46aaa3b0 100644 --- a/2-ui/1-document/08-styles-and-classes/article.md +++ b/2-ui/1-document/08-styles-and-classes/article.md @@ -128,6 +128,14 @@ setTimeout(() => document.body.style.display = "", 1000); // back to normal If we set `style.display` to an empty string, then the browser applies CSS classes and its built-in styles normally, as if there were no such `style.display` property at all. +Also there is a special method for that, `elem.style.removeProperty('style property')`. So, We can remove a property like this: + +```js run +document.body.style.background = 'red'; //set background to red + +setTimeout(() => document.body.style.removeProperty('background'), 1000); // remove background after 1 second +``` + ````smart header="Full rewrite with `style.cssText`" Normally, we use `style.*` to assign individual style properties. We can't set the full style like `div.style="color: red; width: 100px"`, because `div.style` is an object, and it's read-only. @@ -249,7 +257,7 @@ For instance: ```smart header="Computed and resolved values" There are two concepts in [CSS](https://drafts.csswg.org/cssom/#resolved-values): -1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`. +1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`. 2. A *resolved* style value is the one finally applied to the element. Values like `1em` or `125%` are relative. The browser takes the computed value and makes all units fixed and absolute, for instance: `height:20px` or `font-size:16px`. For geometry properties resolved values may have a floating point, like `width:50.5px`. A long time ago `getComputedStyle` was created to get computed values, but it turned out that resolved values are much more convenient, and the standard changed. @@ -261,20 +269,6 @@ So nowadays `getComputedStyle` actually returns the resolved value of the proper We should always ask for the exact property that we want, like `paddingLeft` or `marginTop` or `borderTopWidth`. Otherwise the correct result is not guaranteed. For instance, if there are properties `paddingLeft/paddingTop`, then what should we get for `getComputedStyle(elem).padding`? Nothing, or maybe a "generated" value from known paddings? There's no standard rule here. - -There are other inconsistencies. As an example, some browsers (Chrome) show `10px` in the document below, and some of them (Firefox) -- do not: - -```html run - - -``` ```` ```smart header="Styles applied to `:visited` links are hidden!" @@ -282,7 +276,7 @@ Visited links may be colored using `:visited` CSS pseudoclass. But `getComputedStyle` does not give access to that color, because otherwise an arbitrary page could find out whether the user visited a link by creating it on the page and checking the styles. -JavaScript may not see the styles applied by `:visited`. And also, there's a limitation in CSS that forbids to apply geometry-changing styles in `:visited`. That's to guarantee that there's no sideway for an evil page to test if a link was visited and hence to break the privacy. +JavaScript may not see the styles applied by `:visited`. And also, there's a limitation in CSS that forbids applying geometry-changing styles in `:visited`. That's to guarantee that there's no side way for an evil page to test if a link was visited and hence to break the privacy. ``` ## Summary diff --git a/2-ui/1-document/09-size-and-scroll/1-get-scroll-height-bottom/task.md b/2-ui/1-document/09-size-and-scroll/1-get-scroll-height-bottom/task.md index be47cfd3..796039c2 100644 --- a/2-ui/1-document/09-size-and-scroll/1-get-scroll-height-bottom/task.md +++ b/2-ui/1-document/09-size-and-scroll/1-get-scroll-height-bottom/task.md @@ -4,7 +4,7 @@ importance: 5 # What's the scroll from the bottom? -The `elem.scrollTop` property is the size of the scrolled out part from the top. How to get the size from the bottom scroll (let's call it `scrollBottom`)? +The `elem.scrollTop` property is the size of the scrolled out part from the top. How to get the size of the bottom scroll (let's call it `scrollBottom`)? Write the code that works for an arbitrary `elem`. diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html index ca9c4d57..8f855ecf 100755 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html @@ -9,7 +9,7 @@ background-color: #00FF00; position: relative; } - + #ball { position: absolute; } @@ -20,7 +20,7 @@
        - . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
        @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg index 515ac7f8..f5bd9f4f 100644 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg @@ -1 +1 @@ -(0,0)clientWidth \ No newline at end of file +(0,0)clientWidth \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md index c6fe6c3b..afa1d8f5 100644 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md @@ -24,17 +24,22 @@ ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px'; ``` -**Attention: the pitfall!** +Now the ball is finally centered. + +````warn header="Attention: the pitfall!" The code won't work reliably while `` has no width/height: ```html ``` +```` When the browser does not know the width/height of an image (from tag attributes or CSS), then it assumes them to equal `0` until the image finishes loading. -After the first load browser usually caches the image, and on next loads it will have the size immediately. But on the first load the value of `ball.offsetWidth` is `0`. That leads to wrong coordinates. +So the value of `ball.offsetWidth` will be `0` until the image loads. That leads to wrong coordinates in the code above. + +After the first load, the browser usually caches the image, and on reloads it will have the size immediately. But on the first load the value of `ball.offsetWidth` is `0`. We should fix that by adding `width/height` to ``: diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html index 9ebe6001..9f21e542 100755 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html @@ -26,7 +26,7 @@ ``` - From the CSS standpoint, `width:auto` is perfectly normal, but in JavaScript we need an exact size in `px` that we can use in calculations. So here CSS width is useless at all. + From the CSS standpoint, `width:auto` is perfectly normal, but in JavaScript we need an exact size in `px` that we can use in calculations. So here CSS width is useless. -And there's one more reason: a scrollbar. Sometimes the code that works fine without a scrollbar starts to bug with it, because a scrollbar takes the space from the content in some browsers. So the real width available for the content is *less* than CSS width. And `clientWidth/clientHeight` take that into account. +And there's one more reason: a scrollbar. Sometimes the code that works fine without a scrollbar becomes buggy with it, because a scrollbar takes the space from the content in some browsers. So the real width available for the content is *less* than CSS width. And `clientWidth/clientHeight` take that into account. ...But with `getComputedStyle(elem).width` the situation is different. Some browsers (e.g. Chrome) return the real inner width, minus the scrollbar, and some of them (e.g. Firefox) -- CSS width (ignore the scrollbar). Such cross-browser differences is the reason not to use `getComputedStyle`, but rather rely on geometry properties. @@ -268,7 +268,7 @@ Elements have the following geometry properties: - `offsetParent` -- is the nearest positioned ancestor or `td`, `th`, `table`, `body`. - `offsetLeft/offsetTop` -- coordinates relative to the upper-left edge of `offsetParent`. - `offsetWidth/offsetHeight` -- "outer" width/height of an element including borders. -- `clientLeft/clientTop` -- the distance from the upper-left outer corner the inner corner. For left-to-right OS they are always the widths of left/top borders. For right-to-left OS the vertical scrollbar is on the left so `clientLeft` includes its width too. +- `clientLeft/clientTop` -- the distances from the upper-left outer corner to the upper-left inner (content + padding) corner. For left-to-right OS they are always the widths of left/top borders. For right-to-left OS the vertical scrollbar is on the left so `clientLeft` includes its width too. - `clientWidth/clientHeight` -- the width/height of the content including paddings, but without the scrollbar. - `scrollWidth/scrollHeight` -- the width/height of the content, just like `clientWidth/clientHeight`, but also include scrolled-out, invisible part of the element. - `scrollLeft/scrollTop` -- width/height of the scrolled out upper part of the element, starting from its upper-left corner. diff --git a/2-ui/1-document/09-size-and-scroll/metric-all.svg b/2-ui/1-document/09-size-and-scroll/metric-all.svg index e7a632a2..20a59e18 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-all.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-all.svg @@ -1 +1 @@ -Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.scrollHeightoffsetHeightscrollTopclientHeightoffsetTopclientLeftclientWidthclientTopoffsetLeftoffsetWidth \ No newline at end of file +Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.scrollHeightoffsetHeightscrollTopclientHeightoffsetTopclientLeftclientWidthclientTopoffsetLeftoffsetWidth \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-client-left-top-rtl.svg b/2-ui/1-document/09-size-and-scroll/metric-client-left-top-rtl.svg index ec3847e9..e8dd3d60 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-client-left-top-rtl.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-client-left-top-rtl.svg @@ -1 +1 @@ -clientTop: 25px = borderclientLeft: 41px \ No newline at end of file +clientTop: 25px = borderclientLeft: 41px \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-client-left-top.svg b/2-ui/1-document/09-size-and-scroll/metric-client-left-top.svg index c5479234..8097afa7 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-client-left-top.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-client-left-top.svg @@ -1 +1 @@ -Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997.clientTop: 25px = borderclientLeft: 25px \ No newline at end of file +Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997.clientTop: 25px = borderclientLeft: 25px \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-client-width-height.svg b/2-ui/1-document/09-size-and-scroll/metric-client-width-height.svg index feadb321..2603b05f 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-client-width-height.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-client-width-height.svg @@ -1 +1 @@ -border 25pxpadding 20pxcontent width: 284pxborder 25pxpadding 20pxscrollbar 16pxclientWidth = 20+284+20 = 324pxclientHeight: 240pxheight: 200pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with \ No newline at end of file +border 25pxpadding 20pxcontent width: 284pxborder 25pxpadding 20pxscrollbar 16pxclientWidth = 20+284+20 = 324pxclientHeight: 240pxheight: 200pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-client-width-nopadding.svg b/2-ui/1-document/09-size-and-scroll/metric-client-width-nopadding.svg index 702a8922..330d2a7c 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-client-width-nopadding.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-client-width-nopadding.svg @@ -1 +1 @@ -clientWidth: 284px = content widthCSS width: 300pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with padding: 0; width: 300px; \ No newline at end of file +clientWidth: 284px = content widthCSS width: 300pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with padding: 0; width: 300px; \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-css.svg b/2-ui/1-document/09-size-and-scroll/metric-css.svg index bf56a011..1f2e5f78 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-css.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-css.svg @@ -1 +1 @@ -padding: 20pxheight: 200pxpadding: 20pxborder 25pxpadding 20pxcontent width: 284pxborder 25pxpadding 20pxscrollbar 16pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with \ No newline at end of file +padding: 20pxheight: 200pxpadding: 20pxborder 25pxpadding 20pxcontent width: 284pxborder 25pxpadding 20pxscrollbar 16pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-offset-parent.svg b/2-ui/1-document/09-size-and-scroll/metric-offset-parent.svg index 3ba87dad..2d108473 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-offset-parent.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-offset-parent.svg @@ -1 +1 @@ -offsetTop: 180pxoffsetLeft: 180pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoftposition: absolute; left: 180px; top: 180px;offsetParent <MAIN> <DIV> \ No newline at end of file +offsetTop: 180pxoffsetLeft: 180pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoftposition: absolute; left: 180px; top: 180px;offsetParent <MAIN> <DIV> \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-offset-width-height.svg b/2-ui/1-document/09-size-and-scroll/metric-offset-width-height.svg index c6ce482b..4d30d90c 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-offset-width-height.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-offset-width-height.svg @@ -1 +1 @@ -border 25pxpadding 20pxcontent width: 284pxheight: 200pxborder 25pxpadding 20pxscrollbar 16pxoffsetWidth = 25+20+284+20+16+25 = 390pxoffsetHeight: 290pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with \ No newline at end of file +border 25pxpadding 20pxcontent width: 284pxheight: 200pxborder 25pxpadding 20pxscrollbar 16pxoffsetWidth = 25+20+284+20+16+25 = 390pxoffsetHeight: 290pxIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-scroll-top.svg b/2-ui/1-document/09-size-and-scroll/metric-scroll-top.svg index 84a1d12c..7f72de42 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-scroll-top.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-scroll-top.svg @@ -1 +1 @@ -Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.scrollTopscrollHeight: 723px \ No newline at end of file +Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.scrollTopscrollHeight: 723px \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/metric-scroll-width-height.svg b/2-ui/1-document/09-size-and-scroll/metric-scroll-width-height.svg index c5232f39..75a24e3b 100644 --- a/2-ui/1-document/09-size-and-scroll/metric-scroll-width-height.svg +++ b/2-ui/1-document/09-size-and-scroll/metric-scroll-width-height.svg @@ -1 +1 @@ -Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.scrollHeight: 723pxscrollWidth = 324px \ No newline at end of file +Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.scrollHeight: 723pxscrollWidth = 324px \ No newline at end of file diff --git a/2-ui/1-document/10-size-and-scroll-window/article.md b/2-ui/1-document/10-size-and-scroll-window/article.md index edfb8bcd..08a2f657 100644 --- a/2-ui/1-document/10-size-and-scroll-window/article.md +++ b/2-ui/1-document/10-size-and-scroll-window/article.md @@ -1,12 +1,12 @@ # Window sizes and scrolling -How to find out the width and height of the browser window? How to get the full width and height of the document, including the scrolled out part? How to scroll the page using JavaScript? +How do we find the width and height of the browser window? How do we get the full width and height of the document, including the scrolled out part? How do we scroll the page using JavaScript? -For most such requests, we can use the root document element `document.documentElement`, that corresponds to `` tag. But there are additional methods and peculiarities important enough to consider. +For this type of information, we can use the root document element `document.documentElement`, that corresponds to the `` tag. But there are additional methods and peculiarities to consider. ## Width/height of the window -To get window width and height we can use `clientWidth/clientHeight` of `document.documentElement`: +To get window width and height, we can use the `clientWidth/clientHeight` of `document.documentElement`: ![](document-client-width-height.svg) @@ -16,12 +16,12 @@ For instance, this button shows the height of your window: ``` -````warn header="Not `window.innerWidth/Height`" -Browsers also support properties `window.innerWidth/innerHeight`. They look like what we want. So why not to use them instead? +````warn header="Not `window.innerWidth/innerHeight`" +Browsers also support properties like `window.innerWidth/innerHeight`. They look like what we want, so why not to use them instead? -If there exists a scrollbar, and it occupies some space, `clientWidth/clientHeight` provide the width/height without it (subtract it). In other words, they return width/height of the visible part of the document, available for the content. +If there exists a scrollbar, and it occupies some space, `clientWidth/clientHeight` provide the width/height without it (subtract it). In other words, they return the width/height of the visible part of the document, available for the content. -...And `window.innerWidth/innerHeight` include the scrollbar. +`window.innerWidth/innerHeight` includes the scrollbar. If there's a scrollbar, and it occupies some space, then these two lines show different values: ```js run @@ -29,7 +29,7 @@ alert( window.innerWidth ); // full window width alert( document.documentElement.clientWidth ); // window width minus the scrollbar ``` -In most cases we need the *available* window width: to draw or position something. That is: inside scrollbars if there are any. So we should use `documentElement.clientHeight/Width`. +In most cases, we need the *available* window width in order to draw or position something within scrollbars (if there are any), so we should use `documentElement.clientHeight/clientWidth`. ```` ```warn header="`DOCTYPE` is important" @@ -40,9 +40,9 @@ In modern HTML we should always write `DOCTYPE`. ## Width/height of the document -Theoretically, as the root document element is `documentElement.clientWidth/Height`, and it encloses all the content, we could measure document full size as `documentElement.scrollWidth/scrollHeight`. +Theoretically, as the root document element is `document.documentElement`, and it encloses all the content, we could measure the document's full size as `document.documentElement.scrollWidth/scrollHeight`. -But on that element, for the whole page, these properties do not work as intended. In Chrome/Safari/Opera if there's no scroll, then `documentElement.scrollHeight` may be even less than `documentElement.clientHeight`! Sounds like a nonsense, weird, right? +But on that element, for the whole page, these properties do not work as intended. In Chrome/Safari/Opera, if there's no scroll, then `documentElement.scrollHeight` may be even less than `documentElement.clientHeight`! Weird, right? To reliably obtain the full document height, we should take the maximum of these properties: @@ -60,11 +60,11 @@ Why so? Better don't ask. These inconsistencies come from ancient times, not a " ## Get the current scroll [#page-scroll] -DOM elements have their current scroll state in `elem.scrollLeft/scrollTop`. +DOM elements have their current scroll state in their `scrollLeft/scrollTop` properties. -For document scroll `document.documentElement.scrollLeft/Top` works in most browsers, except oldler WebKit-based ones, like Safari (bug [5991](https://bugs.webkit.org/show_bug.cgi?id=5991)), where we should use `document.body` instead of `document.documentElement`. +For document scroll, `document.documentElement.scrollLeft/scrollTop` works in most browsers, except older WebKit-based ones, like Safari (bug [5991](https://bugs.webkit.org/show_bug.cgi?id=5991)), where we should use `document.body` instead of `document.documentElement`. -Luckily, we don't have to remember these peculiarities at all, because the scroll is available in the special properties `window.pageXOffset/pageYOffset`: +Luckily, we don't have to remember these peculiarities at all, because the scroll is available in the special properties, `window.pageXOffset/pageYOffset`: ```js run alert('Current scroll from the top: ' + window.pageYOffset); @@ -73,19 +73,25 @@ alert('Current scroll from the left: ' + window.pageXOffset); These properties are read-only. +```smart header="Also available as `window` properties `scrollX` and `scrollY`" +For historical reasons, both properties exist, but they are the same: +- `window.pageXOffset` is an alias of `window.scrollX`. +- `window.pageYOffset` is an alias of `window.scrollY`. +``` + ## Scrolling: scrollTo, scrollBy, scrollIntoView [#window-scroll] ```warn -To scroll the page from JavaScript, its DOM must be fully built. +To scroll the page with JavaScript, its DOM must be fully built. -For instance, if we try to scroll the page from the script in ``, it won't work. +For instance, if we try to scroll the page with a script in ``, it won't work. ``` Regular elements can be scrolled by changing `scrollTop/scrollLeft`. -We can do the same for the page using `document.documentElement.scrollTop/Left` (except Safari, where `document.body.scrollTop/Left` should be used instead). +We can do the same for the page using `document.documentElement.scrollTop/scrollLeft` (except Safari, where `document.body.scrollTop/Left` should be used instead). -Alternatively, there's a simpler, universal solution: special methods [window.scrollBy(x,y)](mdn:api/Window/scrollBy) and [window.scrollTo(pageX,pageY)](mdn:api/Window/scrollTo). +Alternatively, there's a simpler, universal solution: special methods [window.scrollBy(x,y)](mdn:api/Window/scrollBy) and [window.scrollTo(pageX,pageY)](mdn:api/Window/scrollTo). - The method `scrollBy(x,y)` scrolls the page *relative to its current position*. For instance, `scrollBy(0,10)` scrolls the page `10px` down. @@ -106,50 +112,50 @@ These methods work for all browsers the same way. ## scrollIntoView -For completeness, let's cover one more method: [elem.scrollIntoView(top)](mdn:api/Element/scrollIntoView). +For completeness, let's cover one more method: [elem.scrollIntoView(top)](mdn:api/Element/scrollIntoView). The call to `elem.scrollIntoView(top)` scrolls the page to make `elem` visible. It has one argument: -- if `top=true` (that's the default), then the page will be scrolled to make `elem` appear on the top of the window. The upper edge of the element is aligned with the window top. -- if `top=false`, then the page scrolls to make `elem` appear at the bottom. The bottom edge of the element is aligned with the window bottom. +- If `top=true` (that's the default), then the page will be scrolled to make `elem` appear on the top of the window. The upper edge of the element will be aligned with the window top. +- If `top=false`, then the page scrolls to make `elem` appear at the bottom. The bottom edge of the element will be aligned with the window bottom. ```online -The button below scrolls the page to make itself show at the window top: +The button below scrolls the page to position itself at the window top: -And this button scrolls the page to show it at the bottom: +And this button scrolls the page to position itself at the bottom: ``` ## Forbid the scrolling -Sometimes we need to make the document "unscrollable". For instance, when we need to cover it with a large message requiring immediate attention, and we want the visitor to interact with that message, not with the document. +Sometimes we need to make the document "unscrollable". For instance, when we need to cover the page with a large message requiring immediate attention, and we want the visitor to interact with that message, not with the document. -To make the document unscrollable, it's enough to set `document.body.style.overflow = "hidden"`. The page will freeze on its current scroll. +To make the document unscrollable, it's enough to set `document.body.style.overflow = "hidden"`. The page will "freeze" at its current scroll position. ```online Try it: - + - + -The first button freezes the scroll, the second one resumes it. +The first button freezes the scroll, while the second one releases it. ``` -We can use the same technique to "freeze" the scroll for other elements, not just for `document.body`. +We can use the same technique to freeze the scroll for other elements, not just for `document.body`. -The drawback of the method is that the scrollbar disappears. If it occupied some space, then that space is now free, and the content "jumps" to fill it. +The drawback of the method is that the scrollbar disappears. If it occupied some space, then that space is now free and the content "jumps" to fill it. -That looks a bit odd, but can be worked around if we compare `clientWidth` before and after the freeze, and if it increased (the scrollbar disappeared) then add `padding` to `document.body` in place of the scrollbar, to keep the content width the same. +That looks a bit odd, but can be worked around if we compare `clientWidth` before and after the freeze. If it increased (the scrollbar disappeared), then add `padding` to `document.body` in place of the scrollbar to keep the content width the same. ## Summary Geometry: -- Width/height of the visible part of the document (content area width/height): `document.documentElement.clientWidth/Height` +- Width/height of the visible part of the document (content area width/height): `document.documentElement.clientWidth/clientHeight` - Width/height of the whole document, with the scrolled out part: ```js diff --git a/2-ui/1-document/10-size-and-scroll-window/document-client-width-height.svg b/2-ui/1-document/10-size-and-scroll-window/document-client-width-height.svg index 34187cdb..18cd37a7 100644 --- a/2-ui/1-document/10-size-and-scroll-window/document-client-width-height.svg +++ b/2-ui/1-document/10-size-and-scroll-window/document-client-width-height.svg @@ -1 +1 @@ -documentElement.clientHeightdocumentElement.clientWidth \ No newline at end of file +documentElement.clientHeightdocumentElement.clientWidth \ No newline at end of file diff --git a/2-ui/1-document/11-coordinates/1-find-point-coordinates/solution.md b/2-ui/1-document/11-coordinates/1-find-point-coordinates/solution.md index f3547096..4101d491 100644 --- a/2-ui/1-document/11-coordinates/1-find-point-coordinates/solution.md +++ b/2-ui/1-document/11-coordinates/1-find-point-coordinates/solution.md @@ -1,6 +1,6 @@ # Outer corners -Outer corners are basically what we get from [elem.getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/DOM/element.getBoundingClientRect). +Outer corners are basically what we get from [elem.getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/DOM/element.getBoundingClientRect). Coordinates of the upper-left corner `answer1` and the bottom-right corner `answer2`: diff --git a/2-ui/1-document/11-coordinates/3-position-at-absolute/solution.view/index.html b/2-ui/1-document/11-coordinates/3-position-at-absolute/solution.view/index.html index d627bee8..56c95d5e 100644 --- a/2-ui/1-document/11-coordinates/3-position-at-absolute/solution.view/index.html +++ b/2-ui/1-document/11-coordinates/3-position-at-absolute/solution.view/index.html @@ -28,8 +28,8 @@ let box = elem.getBoundingClientRect(); return { - top: box.top + pageYOffset, - left: box.left + pageXOffset + top: box.top + window.pageYOffset, + left: box.left + window.pageXOffset }; } diff --git a/2-ui/1-document/11-coordinates/4-position-inside-absolute/solution.view/index.html b/2-ui/1-document/11-coordinates/4-position-inside-absolute/solution.view/index.html index 7e841397..b89db379 100644 --- a/2-ui/1-document/11-coordinates/4-position-inside-absolute/solution.view/index.html +++ b/2-ui/1-document/11-coordinates/4-position-inside-absolute/solution.view/index.html @@ -26,8 +26,8 @@ let box = elem.getBoundingClientRect(); return { - top: box.top + pageYOffset, - left: box.left + pageXOffset + top: box.top + window.pageYOffset, + left: box.left + window.pageXOffset }; } diff --git a/2-ui/1-document/11-coordinates/article.md b/2-ui/1-document/11-coordinates/article.md index c71fa0fc..4775ff0e 100644 --- a/2-ui/1-document/11-coordinates/article.md +++ b/2-ui/1-document/11-coordinates/article.md @@ -69,29 +69,29 @@ As you can see, `x/y` and `width/height` fully describe the rectangle. Derived p Please note: -- Coordinates may be decimal fractions, such as `10.5`. That's normal, internally browser uses fractions in calculations. We don't have to round them when setting to `style.position.left/top`. +- Coordinates may be decimal fractions, such as `10.5`. That's normal, internally browser uses fractions in calculations. We don't have to round them when setting to `style.left/top`. - Coordinates may be negative. For instance, if the page is scrolled so that `elem` is now above the window, then `elem.getBoundingClientRect().top` is negative. ```smart header="Why derived properties are needed? Why does `top/left` exist if there's `x/y`?" Mathematically, a rectangle is uniquely defined with its starting point `(x,y)` and the direction vector `(width,height)`. So the additional derived properties are for convenience. -Technically it's possible for `width/height` to be negative, that allows for "directed" rectangle, e.g. to represent mouse selection with properly marked start and end. +Technically it's possible for `width/height` to be negative, that allows for "directed" rectangle, e.g. to represent mouse selection with properly marked start and end. + +Negative `width/height` values mean that the rectangle starts at its bottom-right corner and then "grows" left-upwards. Here's a rectangle with negative `width` and `height` (e.g. `width=-200`, `height=-100`): ![](coordinates-negative.svg) -The rectangle starts at its bottom-right corner and then spans left/up, as negative `width/height` lead it backwards by coordinates. +As you can see, `left/top` do not equal `x/y` in such case. -As you can see, `left/top` are not `x/y` here. So these are actually not duplicates. Their formula can be adjusted to cover negative `width/height`, that's simple enough, but rarely needed, as the result of `elem.getBoundingClientRect()` always has positive width/height. - -Here we mention negative `width/height` only for you to understand why these seemingly duplicate properties are not actually duplicates. +In practice though, `elem.getBoundingClientRect()` always returns positive width/height, here we mention negative `width/height` only for you to understand why these seemingly duplicate properties are not actually duplicates. ``` -```warn header="Internet Explorer and Edge: no support for `x/y`" -Internet Explorer and Edge don't support `x/y` properties for historical reasons. +```warn header="Internet Explorer: no support for `x/y`" +Internet Explorer doesn't support `x/y` properties for historical reasons. -So we can either make a polywill (add getters in `DomRect.prototype`) or just use `top/left`, as they are always the same as `x/y` for positive `width/height`, in particular in the result of `elem.getBoundingClientRect()`. +So we can either make a polyfill (add getters in `DomRect.prototype`) or just use `top/left`, as they are always the same as `x/y` for positive `width/height`, in particular in the result of `elem.getBoundingClientRect()`. ``` ```warn header="Coordinates right/bottom are different from CSS position properties" @@ -215,8 +215,10 @@ function getCoords(elem) { let box = elem.getBoundingClientRect(); return { - top: box.top + pageYOffset, - left: box.left + pageXOffset + top: box.top + window.pageYOffset, + right: box.right + window.pageXOffset, + bottom: box.bottom + window.pageYOffset, + left: box.left + window.pageXOffset }; } ``` @@ -250,4 +252,4 @@ Any point on the page has coordinates: Window coordinates are great to use with `position:fixed`, and document coordinates do well with `position:absolute`. -Both coordinate systems have their "pro" and "contra", there are times we need one or the other one, just like CSS `position` `absolute` and `fixed`. +Both coordinate systems have their pros and cons; there are times we need one or the other one, just like CSS `position` `absolute` and `fixed`. diff --git a/2-ui/1-document/11-coordinates/coordinates-negative.svg b/2-ui/1-document/11-coordinates/coordinates-negative.svg index 3570d230..4f2e7868 100644 --- a/2-ui/1-document/11-coordinates/coordinates-negative.svg +++ b/2-ui/1-document/11-coordinates/coordinates-negative.svg @@ -1 +1 @@ -bottom(x,y)(x,y)leftrightIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at top(width,height) \ No newline at end of file +bottom(x,y)(x,y)leftrightIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at top(width,height) \ No newline at end of file diff --git a/2-ui/1-document/11-coordinates/coordinates.svg b/2-ui/1-document/11-coordinates/coordinates.svg index 47514c01..261ff669 100644 --- a/2-ui/1-document/11-coordinates/coordinates.svg +++ b/2-ui/1-document/11-coordinates/coordinates.svg @@ -1 +1 @@ -heightbottomxleftywidthrightIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at top \ No newline at end of file +heightbottomxleftywidthrightIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at top \ No newline at end of file diff --git a/2-ui/1-document/11-coordinates/document-and-window-coordinates-scrolled.svg b/2-ui/1-document/11-coordinates/document-and-window-coordinates-scrolled.svg index 4873ea4b..f03317f0 100644 --- a/2-ui/1-document/11-coordinates/document-and-window-coordinates-scrolled.svg +++ b/2-ui/1-document/11-coordinates/document-and-window-coordinates-scrolled.svg @@ -1 +1 @@ -Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.😍pageYclientYpageXclientXIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.pageYclientYpageXclientX😍 \ No newline at end of file +Introduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.😍pageYclientYpageXclientXIntroduction This Ecma Standard is based on several originating technologies, the most well known being JavaScript (Netscape) and JScript (Microsoft). The language was invented by Brendan Eich at Netscape and first appeared in that company’s Navigator 2.0 browser. It has appeared in all subsequent browsers from Netscape and in all browsers from Microsoft starting with Internet Explorer 3.0. The development of this Standard started in November 1996. The first edition of this Ecma Standard was adopted by the Ecma General Assembly of June 1997. That Ecma Standard was submitted to ISO/ IEC JTC 1 for adoption under the fast-track procedure, and approved as international standard ISO/IEC 16262, in April 1998. The Ecma General Assembly of June 1998 approved the second edition of ECMA-262 to keep it fully aligned with ISO/IEC 16262. Changes between the first and the second edition are editorial in nature.pageYclientYpageXclientX😍 \ No newline at end of file diff --git a/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/move-ball-coords.svg b/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/move-ball-coords.svg index fc6f50fa..2acc6b03 100644 --- a/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/move-ball-coords.svg +++ b/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/move-ball-coords.svg @@ -1 +1 @@ -ball.style.position.left?fieldCoords.leftevent.clientX \ No newline at end of file +ball.style.left?fieldCoords.leftevent.clientX \ No newline at end of file diff --git a/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/solution.md b/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/solution.md index 24f454a2..b04cb823 100644 --- a/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/solution.md +++ b/2-ui/2-events/01-introduction-browser-events/04-move-ball-field/solution.md @@ -22,7 +22,7 @@ Then the ball will be positioned relatively to the field: } ``` -Next we need to assign the correct `ball.style.position.left/top`. They contain field-relative coordinates now. +Next we need to assign the correct `ball.style.left/top`. They contain field-relative coordinates now. Here's the picture: @@ -36,7 +36,7 @@ To get field-relative `left` coordinate of the click, we can substract the field let left = event.clientX - fieldCoords.left - field.clientLeft; ``` -Normally, `ball.style.position.left` means the "left edge of the element" (the ball). So if we assign that `left`, then the ball edge, not center, would be under the mouse cursor. +Normally, `ball.style.left` means the "left edge of the element" (the ball). So if we assign that `left`, then the ball edge, not center, would be under the mouse cursor. We need to move the ball half-width left and half-height up to make it center. diff --git a/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel1.svg b/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel1.svg index 95c3939b..5bb161f6 100644 --- a/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel1.svg +++ b/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel1.svg @@ -1 +1 @@ -div (container)130x130ul (width: 9999px) \ No newline at end of file +div (container)130x130ul (width: 9999px) \ No newline at end of file diff --git a/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel2.svg b/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel2.svg index 70976269..81aea5b8 100644 --- a/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel2.svg +++ b/2-ui/2-events/01-introduction-browser-events/07-carousel/carousel2.svg @@ -1 +1 @@ -div (container)130x130ul (margin-left: -350px) \ No newline at end of file +div (container)130x130ul (margin-left: -350px) \ No newline at end of file diff --git a/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.md b/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.md index 41de7e1f..1c6b52ce 100644 --- a/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.md +++ b/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.md @@ -1,6 +1,6 @@ The images ribbon can be represented as `ul/li` list of images ``. -Normally, such a ribbon is wide, but we put a fixed-size `
        ` around to "cut" it, so that only a part of the ribbon is visibble: +Normally, such a ribbon is wide, but we put a fixed-size `
        ` around to "cut" it, so that only a part of the ribbon is visible: ![](carousel1.svg) diff --git a/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.view/index.html b/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.view/index.html index 2c607331..baf86766 100644 --- a/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.view/index.html +++ b/2-ui/2-events/01-introduction-browser-events/07-carousel/solution.view/index.html @@ -10,7 +10,7 @@