diff --git a/.gitattributes b/.gitattributes index 6313b56c..d3877a53 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto eol=lf +*.svg binary diff --git a/1-js/01-getting-started/1-intro/article.md b/1-js/01-getting-started/1-intro/article.md index a611d960..4822fdb4 100644 --- a/1-js/01-getting-started/1-intro/article.md +++ b/1-js/01-getting-started/1-intro/article.md @@ -6,7 +6,7 @@ Let's see what's so special about JavaScript, what we can achieve with it, and w *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 executed automatically as the page loads. +The programs in this language are called *scripts*. They can be written right in a web page's HTML and run automatically as the page loads. Scripts are provided and executed as plain text. They don't need special preparation or compilation to run. @@ -45,7 +45,7 @@ The engine applies optimizations at each step of the process. It even watches th 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. -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. +JavaScript's capabilities greatly depend on the environment it's running in. For instance, [Node.js](https://wikipedia.org/wiki/Node.js) supports functions that allow JavaScript to read/write arbitrary files, perform network requests, etc. In-browser JavaScript can do everything related to webpage manipulation, interaction with the user, and the webserver. @@ -70,12 +70,12 @@ Examples of such restrictions include: 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). - This is called the "Same Origin Policy". To work around that, *both pages* must contain a special JavaScript code that handles data exchange. + This is called the "Same Origin Policy". To work around that, *both pages* must agree for data exchange and contain a 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. - JavaScript can easily communicate over the net to the server where the current page came from. But its ability to receive data from other sites/domains is crippled. Though possible, it requires explicit agreement (expressed in HTTP headers) from the remote side. Once again, that's a safety limitation. -![](limitations.png) +![](limitations.svg) Such limits do not exist if JavaScript is used outside of the browser, for example on a server. Modern browsers also allow plugin/extensions which may ask for extended permissions. @@ -88,12 +88,11 @@ There are at least *three* great things about JavaScript: + Simple things are done simply. + Support by all major browsers and enabled by default. ``` -Javascript is the only browser technology that combines these three things. +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. -While planning to learn a new technology, it's beneficial to check its perspectives. So let's move on to the modern trends affecting it, including new languages and browser abilities. - +That said, JavaScript also allows to create servers, mobile applications, etc. ## Languages "over" JavaScript @@ -109,9 +108,10 @@ 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. -- [Dart](https://www.dartlang.org/) is a standalone language that has its own engine that runs in non-browser environments (like mobile apps). It was initially offered by Google as a replacement for JavaScript, but as of now, browsers require it to be transpiled to JavaScript just like the ones above. +- [Flow](http://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. -There are more. Of course, even if we use one of these languages, we should also know JavaScript to really understand what we're doing. +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. ## Summary diff --git a/1-js/01-getting-started/1-intro/limitations.png b/1-js/01-getting-started/1-intro/limitations.png deleted file mode 100644 index a07b9903..00000000 Binary files a/1-js/01-getting-started/1-intro/limitations.png and /dev/null differ diff --git a/1-js/01-getting-started/1-intro/limitations.svg b/1-js/01-getting-started/1-intro/limitations.svg new file mode 100644 index 00000000..a7863c63 --- /dev/null +++ b/1-js/01-getting-started/1-intro/limitations.svg @@ -0,0 +1 @@ +https://javascript.info<script> ... </script>https://gmail.comhttps://javascript.info \ No newline at end of file diff --git a/1-js/01-getting-started/1-intro/limitations@2x.png b/1-js/01-getting-started/1-intro/limitations@2x.png deleted file mode 100644 index 287b06d9..00000000 Binary files a/1-js/01-getting-started/1-intro/limitations@2x.png and /dev/null differ diff --git a/1-js/01-getting-started/2-manuals-specifications/article.md b/1-js/01-getting-started/2-manuals-specifications/article.md new file mode 100644 index 00000000..85a7737c --- /dev/null +++ b/1-js/01-getting-started/2-manuals-specifications/article.md @@ -0,0 +1,42 @@ + +# 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. + +## Specification + +[The ECMA-262 specification](https://www.ecma-international.org/publications/standards/Ecma-262.htm) contains the most in-depth, detailed and formalized information about JavaScript. It defines the language. + +But being that formalized, it's difficult to understand at first. So if you need the most trustworthy source of information about the language details, the specification is the right place. But it's not for everyday use. + +A new specification version is released every year. In-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. + +## 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. + + One 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". + +## Compatibility tables + +JavaScript is a developing language, new features get added regularly. + +To see their support among browser-based and other engines, see: + +- - per-feature tables of support, e.g. to see which engines support modern cryptography functions: . +- - a table with language features and engines that support those or don't support. + +All these resources are useful in real-life development, as they contain valuable information about language details, their support etc. + +Please remember them (or this page) for the cases when you need in-depth information about a particular feature. diff --git a/1-js/01-getting-started/2-code-editors/article.md b/1-js/01-getting-started/3-code-editors/article.md similarity index 69% rename from 1-js/01-getting-started/2-code-editors/article.md rename to 1-js/01-getting-started/3-code-editors/article.md index d36561bc..6532e54a 100644 --- a/1-js/01-getting-started/2-code-editors/article.md +++ b/1-js/01-getting-started/3-code-editors/article.md @@ -12,14 +12,12 @@ 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: -- [WebStorm](http://www.jetbrains.com/webstorm/) for frontend development. The same company offers other editors for other languages (paid). -- [Netbeans](http://netbeans.org/) (free). +- [Visual Studio Code](https://code.visualstudio.com/) (cross-platform, free). +- [WebStorm](http://www.jetbrains.com/webstorm/) (cross-platform, paid). -All of these IDEs are cross-platform. +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/). -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. A free version of it is called [Visual Studio Community](https://www.visualstudio.com/vs/community/). - -Many IDEs are paid but have a trial period. Their cost is usually negligible compared to a qualified developer's salary, so just choose the best one for you. +Many IDEs are paid, but have a trial period. Their cost is usually negligible compared to a qualified developer's salary, so just choose the best one for you. ## Lightweight editors @@ -33,21 +31,11 @@ In practice, lightweight editors may have a lot of plugins including directory-l The following options deserve your attention: -- [Visual Studio Code](https://code.visualstudio.com/) (cross-platform, free) also has many IDE-like features. - [Atom](https://atom.io/) (cross-platform, free). - [Sublime Text](http://www.sublimetext.com) (cross-platform, shareware). - [Notepad++](https://notepad-plus-plus.org/) (Windows, free). - [Vim](http://www.vim.org/) and [Emacs](https://www.gnu.org/software/emacs/) are also cool if you know how to use them. -## My favorites - -The personal preference of the author is to have both an IDE for projects and a lightweight editor for quick and easy file editing. - -I'm using: - -- As an IDE for JS -- [WebStorm](http://www.jetbrains.com/webstorm/) (I switch to one of the other JetBrains offerings when using other languages) -- As a lightweight editor -- [Sublime Text](http://www.sublimetext.com) or [Atom](https://atom.io/). - ## Let's not argue The editors in the lists above are those that either I or my friends whom I consider good developers have been using for a long time and are happy with. diff --git a/1-js/01-getting-started/3-devtools/safari.png b/1-js/01-getting-started/3-devtools/safari.png deleted file mode 100644 index 37598a26..00000000 Binary files a/1-js/01-getting-started/3-devtools/safari.png and /dev/null differ diff --git a/1-js/01-getting-started/3-devtools/safari@2x.png b/1-js/01-getting-started/3-devtools/safari@2x.png deleted file mode 100644 index c59cebef..00000000 Binary files a/1-js/01-getting-started/3-devtools/safari@2x.png and /dev/null differ diff --git a/1-js/01-getting-started/3-devtools/article.md b/1-js/01-getting-started/4-devtools/article.md similarity index 95% rename from 1-js/01-getting-started/3-devtools/article.md rename to 1-js/01-getting-started/4-devtools/article.md index 6aa9b7c5..c84d9270 100644 --- a/1-js/01-getting-started/3-devtools/article.md +++ b/1-js/01-getting-started/4-devtools/article.md @@ -50,11 +50,11 @@ 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. -## Multi-line input - +```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 line, press `key:Shift+Enter`. +To insert multiple lines, press `key:Shift+Enter`. This way one can enter long fragments of JavaScript code. +``` ## Summary diff --git a/1-js/01-getting-started/3-devtools/bug.html b/1-js/01-getting-started/4-devtools/bug.html similarity index 100% rename from 1-js/01-getting-started/3-devtools/bug.html rename to 1-js/01-getting-started/4-devtools/bug.html diff --git a/1-js/01-getting-started/3-devtools/chrome.png b/1-js/01-getting-started/4-devtools/chrome.png similarity index 100% rename from 1-js/01-getting-started/3-devtools/chrome.png rename to 1-js/01-getting-started/4-devtools/chrome.png diff --git a/1-js/01-getting-started/3-devtools/chrome@2x.png b/1-js/01-getting-started/4-devtools/chrome@2x.png similarity index 100% rename from 1-js/01-getting-started/3-devtools/chrome@2x.png rename to 1-js/01-getting-started/4-devtools/chrome@2x.png diff --git a/1-js/01-getting-started/4-devtools/safari.png b/1-js/01-getting-started/4-devtools/safari.png new file mode 100644 index 00000000..64c7a3f6 Binary files /dev/null and b/1-js/01-getting-started/4-devtools/safari.png differ diff --git a/1-js/01-getting-started/4-devtools/safari@2x.png b/1-js/01-getting-started/4-devtools/safari@2x.png new file mode 100644 index 00000000..27def4d0 Binary files /dev/null and b/1-js/01-getting-started/4-devtools/safari@2x.png differ diff --git a/1-js/02-first-steps/01-hello-world/article.md b/1-js/02-first-steps/01-hello-world/article.md index a24d3dca..489c01ad 100644 --- a/1-js/02-first-steps/01-hello-world/article.md +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -1,10 +1,10 @@ # Hello, world! -The tutorial that you're reading is about core JavaScript, which is platform-independent. Later on, you'll learn about Node.JS and other platforms that use it. +This part of the tutorial is about core JavaScript, the language itself. -But we need a working environment to run our scripts and, since this book is online, the browser is a good choice. We'll keep the amount of browser-specific commands (like `alert`) to a minimum so that you don't spend time on them if you plan to concentrate on another environment (like Node.JS). We'll focus on JavaScript in the browser in the [next part](/ui) of the tutorial. +But we need a working environment to run our scripts and, since this book is online, the browser is a good choice. We'll keep the amount of browser-specific commands (like `alert`) to a minimum so that you don't spend time on them if you plan to concentrate on another environment (like Node.js). We'll focus on JavaScript in the browser in the [next part](/ui) of the tutorial. -So first, let's see how we attach a script to a webpage. For server-side environments (like Node.JS), you can execute the script with a command like `"node my.js"`. +So first, let's see how we attach a script to a webpage. For server-side environments (like Node.js), you can execute the script with a command like `"node my.js"`. ## The "script" tag @@ -46,7 +46,7 @@ The ` ``` -Here, `/path/to/script.js` is an absolute path to the script file (from the site root). - -You 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"` would mean a file `"script.js"` in the current folder. We can give a full URL as well. For instance: diff --git a/1-js/02-first-steps/01-hello-world/hello-world-render.png b/1-js/02-first-steps/01-hello-world/hello-world-render.png deleted file mode 100644 index ffe81069..00000000 Binary files a/1-js/02-first-steps/01-hello-world/hello-world-render.png and /dev/null differ diff --git a/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png b/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png deleted file mode 100644 index c4411027..00000000 Binary files a/1-js/02-first-steps/01-hello-world/hello-world-render@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md index 6fe7041e..b18aab19 100644 --- a/1-js/02-first-steps/02-structure/article.md +++ b/1-js/02-first-steps/02-structure/article.md @@ -156,4 +156,4 @@ Please, don't hesitate to comment your code. Comments increase the overall code footprint, but that's not a problem at all. There are many tools which minify code before publishing to a production server. They remove comments, so they don't appear in the working scripts. Therefore, comments do not have negative effects on production at all. -Later in the tutorial there will be a chapter that also explains how to write better comments. +Later in the tutorial there will be a chapter that also explains how to write better comments. diff --git a/1-js/02-first-steps/03-strict-mode/article.md b/1-js/02-first-steps/03-strict-mode/article.md index ff6c1527..573d76bc 100644 --- a/1-js/02-first-steps/03-strict-mode/article.md +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -4,7 +4,7 @@ For a long time, JavaScript evolved without compatibility issues. New features w That had the benefit of never breaking existing code. But the downside was that any mistake or an imperfect decision made by JavaScript's creators got stuck in the language forever. -This was the case until 2009 when ECMAScript 5 (ES5) appeared. It added new features to the language and modified some of the existing ones. To keep the old code working, most modifications are off by default. You need to explicitly enable them with a special directive: `"use strict"`. +This was the case until 2009 when ECMAScript 5 (ES5) appeared. It added new features to the language and modified some of the existing ones. To keep the old code working, most such modifications are off by default. You need to explicitly enable them with a special directive: `"use strict"`. ## "use strict" @@ -19,9 +19,7 @@ For example: ... ``` -We will learn functions (a way to group commands) soon. - -Looking ahead, let's just note that `"use strict"` can be put at the start of most kinds of functions instead of the whole script. Doing that enables strict mode in that function only. But usually, people use it for the whole script. +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. ````warn header="Ensure that \"use strict\" is at the top" @@ -53,11 +51,19 @@ For the future, when you use a browser console to test features, please note tha Sometimes, when `use strict` makes a difference, you'll get incorrect results. -Even if we press `key:Shift+Enter` to input multiple lines, and put `use strict` on top, it doesn't work. That's because of how the console executes the code internally. - -The reliable way to ensure `use strict` would be to input the code into console like this: +You can try to press `key:Shift+Enter` to input multiple lines, and put `use strict` on top, like this: +```js +'use strict'; +// ...your code + ``` + +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: + +```js (function() { 'use strict'; diff --git a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md index 9ffc3efc..d56e54d2 100644 --- a/1-js/02-first-steps/04-variables/2-declare-variables/solution.md +++ b/1-js/02-first-steps/04-variables/2-declare-variables/solution.md @@ -1,4 +1,4 @@ -First, the variable for the name of our planet. +## The variable for our planet That's simple: @@ -8,7 +8,7 @@ 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. -Second, the name of the current visitor: +## The name of the current visitor ```js let currentUserName = "John"; diff --git a/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md b/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md index f3a96c69..acd643fd 100644 --- a/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md +++ b/1-js/02-first-steps/04-variables/3-uppercast-constant/solution.md @@ -2,4 +2,4 @@ We generally use upper case for constants that are "hard-coded". Or, in other wo In this code, `birthday` is exactly like that. So we could use the upper case for it. -In contrast, `age` is evaluated in run-time. Today we have one age, a year after we'll have another one. It is constant in a sense that it does not change through the code execution. But it is a bit "less of a constant" than `birthday`, it is calculated, so we should keep the lower case for it. \ No newline at end of file +In contrast, `age` is evaluated in run-time. Today we have one age, a year after we'll have another one. It is constant in a sense that it does not change through the code execution. But it is a bit "less of a constant" than `birthday`: it is calculated, so we should keep the lower case for it. diff --git a/1-js/02-first-steps/04-variables/article.md b/1-js/02-first-steps/04-variables/article.md index dcf8a9ca..80a36d9c 100644 --- a/1-js/02-first-steps/04-variables/article.md +++ b/1-js/02-first-steps/04-variables/article.md @@ -12,7 +12,7 @@ A [variable](https://en.wikipedia.org/wiki/Variable_(computer_science)) is a "na To create a variable in JavaScript, use the `let` keyword. -The statement below creates (in other words: *declares* or *defines*) a variable with the name "message": +The statement below creates (in other words: *declares*) a variable with the name "message": ```js let message; @@ -99,7 +99,7 @@ We can easily grasp the concept of a "variable" if we imagine it as a "box" for For instance, the variable `message` can be imagined as a box labeled `"message"` with the value `"Hello!"` in it: -![](variable.png) +![](variable.svg) We can put any value in the box. @@ -116,7 +116,7 @@ alert(message); When the value is changed, the old data is removed from the variable: -![](variable-change.png) +![](variable-change.svg) We can also declare two variables and copy data from one into the other. @@ -136,7 +136,7 @@ alert(message); // Hello world! ``` ```smart header="Functional languages" -It's interesting to note that [functional](https://en.wikipedia.org/wiki/Functional_programming) programming languages, like [Scala](http://www.scala-lang.org/) or [Erlang](http://www.erlang.org/), forbid changing variable values. +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. In such languages, once the value is stored "in the box", it's there forever. If we need to store something else, the language forces us to create a new box (declare a new variable). We can't reuse the old one. @@ -157,7 +157,7 @@ let userName; let test123; ``` -When the name contains multiple words, [camelCase](https://en.wikipedia.org/wiki/CamelCase) is commonly used. That is: words go one after another, each word starting with a capital letter: `myVeryLongName`. +When the name contains multiple words, [camelCase](https://en.wikipedia.org/wiki/CamelCase) is commonly used. That is: words go one after another, each word except first starting with a capital letter: `myVeryLongName`. What's interesting -- the dollar sign `'$'` and the underscore `'_'` can also be used in names. They are regular symbols, just like letters, without any special meaning. @@ -182,7 +182,7 @@ let my-name; // hyphens '-' aren't allowed in the name Variables named `apple` and `AppLE` are two different variables. ``` -````smart header="Non-English letters are allowed, but not recommended" +````smart header="Non-Latin letters are allowed, but not recommended" It is possible to use any language, including cyrillic letters or even hieroglyphs, like this: ```js @@ -254,7 +254,7 @@ There is a widespread practice to use constants as aliases for difficult-to-reme Such constants are named using capital letters and underscores. -Like this: +For instance, let's make constants for colors in so-called "web" (hexadecimal) format: ```js run const COLOR_RED = "#F00"; @@ -290,7 +290,7 @@ In other words, capital-named constants are only used as aliases for "hard-coded Talking about variables, there's one more extremely important thing. -Please name your variables sensibly. Take time to think about this. +A variable name should have a clean, obvious meaning, describe 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. @@ -323,7 +323,7 @@ Modern JavaScript minifiers and browsers optimize code well enough, so it won't We can declare variables to store data by using the `var`, `let`, or `const` keywords. -- `let` -- is a modern variable declaration. The code must be in strict mode to use `let` in Chrome (V8). +- `let` -- is a modern variable declaration. - `var` -- is an old-school variable declaration. Normally we don't use it at all, but we'll cover subtle differences from `let` in the chapter , just in case you need them. - `const` -- is like `let`, but the value of the variable can't be changed. diff --git a/1-js/02-first-steps/04-variables/variable-change.png b/1-js/02-first-steps/04-variables/variable-change.png deleted file mode 100644 index 2aff675f..00000000 Binary files a/1-js/02-first-steps/04-variables/variable-change.png and /dev/null differ diff --git a/1-js/02-first-steps/04-variables/variable-change.svg b/1-js/02-first-steps/04-variables/variable-change.svg new file mode 100644 index 00000000..427a6388 --- /dev/null +++ b/1-js/02-first-steps/04-variables/variable-change.svg @@ -0,0 +1 @@ +"World!""Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/variable-change@2x.png b/1-js/02-first-steps/04-variables/variable-change@2x.png deleted file mode 100644 index e74094c9..00000000 Binary files a/1-js/02-first-steps/04-variables/variable-change@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/04-variables/variable.png b/1-js/02-first-steps/04-variables/variable.png deleted file mode 100644 index 3a4baaf4..00000000 Binary files a/1-js/02-first-steps/04-variables/variable.png and /dev/null differ diff --git a/1-js/02-first-steps/04-variables/variable.svg b/1-js/02-first-steps/04-variables/variable.svg new file mode 100644 index 00000000..5d15c9e4 --- /dev/null +++ b/1-js/02-first-steps/04-variables/variable.svg @@ -0,0 +1 @@ +"Hello!"message \ No newline at end of file diff --git a/1-js/02-first-steps/04-variables/variable@2x.png b/1-js/02-first-steps/04-variables/variable@2x.png deleted file mode 100644 index 35e9f454..00000000 Binary files a/1-js/02-first-steps/04-variables/variable@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md index f8170500..0da61737 100644 --- a/1-js/02-first-steps/05-types/article.md +++ b/1-js/02-first-steps/05-types/article.md @@ -178,7 +178,7 @@ 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. -The `symbol` type is used to create unique identifiers for objects. We have to mention it here for completeness, but it's better to study this type after objects. +The `symbol` type is used to create unique identifiers for objects. We mention it here for completeness, but we'll study it after objects. ## The typeof operator [#type-typeof] @@ -221,12 +221,12 @@ 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 of the language. We'll study functions in the next chapters where we'll see that there's no special "function" type in JavaScript. Functions belong to the object type. But `typeof` treats them differently. Formally, it's incorrect, but very convenient in practice. +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. ## Summary -There are 7 basic types in JavaScript. +There are 7 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. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md index 7dd0d61c..4964a623 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md +++ b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md @@ -14,6 +14,7 @@ true + false = 1 " -9 " - 5 = -14 // (4) null + 1 = 1 // (5) undefined + 1 = NaN // (6) +" \t \n" - 2 = -2 // (7) ``` 1. The addition with a string `"" + 1` converts `1` to a string: `"" + 1 = "1"`, and then we have `"1" + 0`, the same rule is applied. @@ -22,3 +23,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`. diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md index f17e870d..930c7151 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md +++ b/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md @@ -21,6 +21,7 @@ true + false " -9 " - 5 null + 1 undefined + 1 +" \t \n" - 2 ``` Think well, write down and then compare with the answer. diff --git a/1-js/02-first-steps/06-type-conversions/article.md b/1-js/02-first-steps/06-type-conversions/article.md index f214a207..20d093ea 100644 --- a/1-js/02-first-steps/06-type-conversions/article.md +++ b/1-js/02-first-steps/06-type-conversions/article.md @@ -1,6 +1,6 @@ # Type Conversions -Most of the time, operators and functions automatically convert the values given to them to the right type. This is called "type conversion". +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. @@ -124,7 +124,6 @@ alert( Boolean(" ") ); // spaces, also true (any non-empty string is true) ``` ```` - ## Summary The three most widely used type conversions are to string, to number, and to boolean. diff --git a/1-js/02-first-steps/07-operators/article.md b/1-js/02-first-steps/07-operators/article.md index 74b27e87..a1373ead 100644 --- a/1-js/02-first-steps/07-operators/article.md +++ b/1-js/02-first-steps/07-operators/article.md @@ -26,7 +26,7 @@ Before we move on, let's grasp some common terminology. alert( y - x ); // 2, binary minus subtracts values ``` - Formally, we're talking about two different operators here: the unary negation (single operand: reverses the sign) and the binary subtraction (two operands: subtracts). + 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 + @@ -93,9 +93,7 @@ alert( +"" ); // 0 It actually does the same thing as `Number(...)`, but is shorter. -The need to convert strings to numbers arises very often. For example, if we are getting values from HTML form fields, they are usually strings. - -What if we want to sum them? +The need to convert strings to numbers arises very often. For example, if we are getting values from HTML form fields, they are usually strings. What if we want to sum them? The binary plus would add them as strings: @@ -127,11 +125,11 @@ Why are unary pluses applied to values before the binary ones? As we're going to ## Operator precedence -If an expression has more than one operator, the execution order is defined by their *precedence*, or, in other words, the implicit priority order of operators. +If an expression has more than one operator, the execution order is defined by their *precedence*, or, in other words, the default priority order of operators. From school, we all know that the multiplication in the expression `1 + 2 * 2` should be calculated before the addition. That's exactly the precedence thing. The multiplication is said to have *a higher precedence* than the addition. -Parentheses override any precedence, so if we're not satisfied with the implicit order, we can use them to change it. For example: `(1 + 2) * 2`. +Parentheses override any precedence, so if we're not satisfied with the default order, we can use them to change it. For example, write `(1 + 2) * 2`. There are many operators in JavaScript. Every operator has a corresponding precedence number. The one with the larger number executes first. If the precedence is the same, the execution order is from left to right. @@ -199,9 +197,9 @@ alert( a ); // 3 alert( c ); // 0 ``` -In the example above, the result of `(a = b + 1)` is the value which is assigned to `a` (that is `3`). It is then used to subtract from `3`. +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 3rd-party 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, but shouldn't write anything like that ourselves. Such tricks definitely don't make code clearer or readable. ```` ## Remainder % @@ -253,14 +251,14 @@ So, there are special operators for it: ```js run no-beautify let counter = 2; - counter++; // works the same as counter = counter + 1, but is shorter + counter++; // works the same as counter = counter + 1, but is shorter alert( counter ); // 3 ``` - **Decrement** `--` decreases a variable by 1: ```js run no-beautify let counter = 2; - counter--; // works the same as counter = counter - 1, but is shorter + counter--; // works the same as counter = counter - 1, but is shorter alert( counter ); // 1 ``` @@ -427,10 +425,10 @@ Here, the first expression `1 + 2` is evaluated and its result is thrown away. T ```smart header="Comma has a very low precedence" Please note that the comma operator has very low precedence, lower than `=`, so parentheses are important in the example above. -Without them: `a = 1 + 2, 3 + 4` evaluates `+` first, summing the numbers into `a = 3, 7`, then the assignment operator `=` assigns `a = 3`, and finally the number after the comma, `7`, is not processed so it's ignored. +Without them: `a = 1 + 2, 3 + 4` evaluates `+` first, summing the numbers into `a = 3, 7`, then the assignment operator `=` assigns `a = 3`, and the rest is ignored. It's like `(a = 1 + 2), 3 + 4`. ``` -Why do we need an operator that throws away everything except the last part? +Why do we need an operator that throws away everything except the last expression? Sometimes, people use it in more complex constructs to put several actions in one line. @@ -443,4 +441,4 @@ for (*!*a = 1, b = 3, c = a * b*/!*; a < 10; a++) { } ``` -Such tricks are used in many JavaScript frameworks. That's why we're mentioning them. But, usually, they don't improve code readability so we should think well before using them. +Such tricks are used in many JavaScript frameworks. That's why we're mentioning them. But usually they don't improve code readability so we should think well before using them. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md index 5c8bd2bc..6437b512 100644 --- a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md +++ b/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md @@ -3,11 +3,11 @@ ```js no-beautify 5 > 4 → true "apple" > "pineapple" → false -"2" > "12" → true -undefined == null → true -undefined === null → false +"2" > "12" → true +undefined == null → true +undefined === null → false null == "\n0\n" → false -null === +"\n0\n" → false +null === +"\n0\n" → false ``` Some of the reasons: @@ -17,5 +17,5 @@ Some of the reasons: 3. Again, dictionary comparison, first char of `"2"` is greater than the first char of `"1"`. 4. Values `null` and `undefined` equal each other only. 5. Strict equality is strict. Different types from both sides lead to false. -6. See (4). +6. Similar to `(4)`, `null` only equals `undefined`. 7. Strict equality of different types. diff --git a/1-js/02-first-steps/08-comparison/article.md b/1-js/02-first-steps/08-comparison/article.md index 1b95a743..d889b132 100644 --- a/1-js/02-first-steps/08-comparison/article.md +++ b/1-js/02-first-steps/08-comparison/article.md @@ -74,7 +74,7 @@ alert( '2' > 1 ); // true, string '2' becomes a number 2 alert( '01' == 1 ); // true, string '01' becomes a number 1 ``` -For boolean values, `true` becomes `1` and `false` becomes `0`. +For boolean values, `true` becomes `1` and `false` becomes `0`. For example: @@ -138,11 +138,8 @@ The strict equality operator is a bit longer to write, but makes it obvious what ## Comparison with null and undefined -Let's see more edge cases. - There's a non-intuitive behavior when `null` or `undefined` are compared to other values. - For a strict equality check `===` : These values are different, because each of them is a different type. @@ -172,7 +169,7 @@ alert( null == 0 ); // (2) false alert( null >= 0 ); // (3) *!*true*/!* ``` -Mathematically, that's strange. The last result states that "`null` is greater than or equal to zero", so one of the comparisons above it must be correct, but they are both false. +Mathematically, that's strange. The last result states that "`null` is greater than or equal to zero", so in one of the comparisons above it must be `true`, but they are both false. The reason is that an equality check `==` and comparisons `> < >= <=` work differently. Comparisons convert `null` to a number, treating it as `0`. That's why (3) `null >= 0` is true and (1) `null > 0` is false. @@ -193,7 +190,7 @@ Why does it dislike zero so much? Always false! We get these results because: - Comparisons `(1)` and `(2)` return `false` because `undefined` gets converted to `NaN` and `NaN` is a special numeric value which returns `false` for all comparisons. -- The equality check `(3)` returns `false` because `undefined` only equals `null` and no other value. +- The equality check `(3)` returns `false` because `undefined` only equals `null`, `undefined`, and no other value. ### Evade problems diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/article.md b/1-js/02-first-steps/09-alert-prompt-confirm/article.md index 4bbfd58f..8ba414e9 100644 --- a/1-js/02-first-steps/09-alert-prompt-confirm/article.md +++ b/1-js/02-first-steps/09-alert-prompt-confirm/article.md @@ -1,6 +1,6 @@ # Interaction: alert, prompt, confirm -This part of the tutorial aims to cover JavaScript "as is", without environment-specific tweaks. +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`. @@ -27,10 +27,10 @@ The mini-window with the message is called a *modal window*. The word "modal" me The function `prompt` accepts two arguments: ```js no-beautify -result = prompt(title[, default]); +result = prompt(title, [default]); ``` -It shows a modal window with a text message, an input field for the visitor, and the buttons OK/CANCEL. +It shows a modal window with a text message, an input field for the visitor, and the buttons OK/Cancel. `title` : The text to show the visitor. @@ -38,7 +38,7 @@ 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. +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. The call to `prompt` returns the text from the input field or `null` if the input was canceled. @@ -74,7 +74,7 @@ The syntax: result = confirm(question); ``` -The function `confirm` shows a modal window with a `question` and two buttons: OK and CANCEL. +The function `confirm` shows a modal window with a `question` and two buttons: OK and Cancel. The result is `true` if OK is pressed and `false` otherwise. @@ -94,10 +94,10 @@ We covered 3 browser-specific functions to interact with visitors: : shows a message. `prompt` -: shows a message asking the user to input text. It returns the text or, if CANCEL or `key:Esc` is clicked, `null`. +: shows a message asking the user to input text. It returns the text or, if Cancel button or `key:Esc` is clicked, `null`. `confirm` -: shows a message and waits for the user to press "OK" or "CANCEL". It returns `true` for OK and `false` for CANCEL/`key:Esc`. +: shows a message and waits for the user to press "OK" or "Cancel". It returns `true` for OK and `false` for Cancel/`key:Esc`. All these methods are modal: they pause script execution and don't allow the visitor to interact with the rest of the page until the window has been dismissed. diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png deleted file mode 100644 index 37c5d5fb..00000000 Binary files a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.png and /dev/null differ diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg new file mode 100644 index 00000000..ea4d122f --- /dev/null +++ b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2.svg @@ -0,0 +1 @@ +BeginYou don't know? “ECMAScript”!Right!What's the “official” name of JavaScript?OtherECMAScript \ No newline at end of file diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png b/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png deleted file mode 100644 index 924a22a8..00000000 Binary files a/1-js/02-first-steps/10-ifelse/2-check-standard/ifelse_task2@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/10-ifelse/2-check-standard/task.md b/1-js/02-first-steps/10-ifelse/2-check-standard/task.md index 46fe05dc..a4d94324 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 @@ -8,7 +8,6 @@ Using the `if..else` construct, write the code which asks: 'What is the "officia If the visitor enters "ECMAScript", then output "Right!", otherwise -- output: "Didn't know? ECMAScript!" -![](ifelse_task2.png) +![](ifelse_task2.svg) [demo src="ifelse_task2"] - diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md index 638ce81f..ff32354f 100644 --- a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md +++ b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/solution.md @@ -1,6 +1,6 @@ ```js -result = (a + b < 4) ? 'Below' : 'Over'; +let result = (a + b < 4) ? 'Below' : 'Over'; ``` diff --git a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md index 684e239f..6bdf8453 100644 --- a/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md +++ b/1-js/02-first-steps/10-ifelse/5-rewrite-if-question/task.md @@ -4,13 +4,14 @@ importance: 5 # Rewrite 'if' into '?' -Rewrite this `if` using the ternary operator `'?'`: +Rewrite this `if` using the conditional operator `'?'`: ```js +let result; + if (a + b < 4) { result = 'Below'; } else { result = 'Over'; } ``` - diff --git a/1-js/02-first-steps/10-ifelse/article.md b/1-js/02-first-steps/10-ifelse/article.md index 1e12fa7b..30287ccb 100644 --- a/1-js/02-first-steps/10-ifelse/article.md +++ b/1-js/02-first-steps/10-ifelse/article.md @@ -2,11 +2,11 @@ Sometimes, we need to perform different actions based on different conditions. -To do that, we use the `if` statement and the conditional (ternary) operator which we will be referring to as the “question mark” operator `?` for simplicity. +To do that, we can use the `if` statement and the conditional operator `?`, that's also called a "question mark" operator. ## The "if" statement -The `if` statement evaluates a condition and, if the condition's result is `true`, executes a block of code. +The `if(...)` statement evaluates a condition in parentheses and, if the result is `true`, executes a block of code. For example: @@ -103,7 +103,7 @@ In the code above, JavaScript first checks `year < 2015`. If that is falsy, it g There can be more `else if` blocks. The final `else` is optional. -## Ternary operator '?' +## Conditional operator '?' Sometimes, we need to assign a variable depending on a condition. @@ -124,9 +124,9 @@ if (age > 18) { alert(accessAllowed); ``` -The so-called "ternary" or "question mark" operator lets us do that in a shorter and simpler way. +The so-called "conditional" or "question mark" operator lets us do that in a shorter and simpler way. -The operator is represented by a question mark `?`. The formal term "ternary" means that the operator has three operands. It is actually the one and only operator in JavaScript which has that many. +The operator is represented by a question mark `?`. Sometimes it's called "ternary", because the operator has three operands. It is actually the one and only operator in JavaScript which has that many. The syntax is: ```js @@ -141,7 +141,7 @@ For example: let accessAllowed = (age > 18) ? true : false; ``` -Technically, we can omit the parentheses around `age > 18`. The question mark operator has a low precedence, so it executes after the comparison `>`. +Technically, we can omit the parentheses around `age > 18`. The question mark operator has a low precedence, so it executes after the comparison `>`. This example will do the same thing as the previous one: @@ -216,7 +216,7 @@ Depending on the condition `company == 'Netscape'`, either the first or the seco We don't assign a result to a variable here. Instead, we execute different code depending on the condition. -**We don't recommend using the question mark operator in this way.** +**It's not recommended to use the question mark operator in this way.** The notation is shorter than the equivalent `if` statement, which appeals to some programmers. But it is less readable. diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.png b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.png deleted file mode 100644 index 0d5eb48f..00000000 Binary files a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.png and /dev/null differ diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg new file mode 100644 index 00000000..010a53a4 --- /dev/null +++ b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg @@ -0,0 +1 @@ +BeginCanceledCanceledWelcome!I don't know youWrong passwordWho's there?Password?CancelCancelAdminTheMasterOtherOther \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task@2x.png b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task@2x.png deleted file mode 100644 index 14a06e93..00000000 Binary files a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task@2x.png and /dev/null differ 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 b535650e..a30db7aa 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 @@ -10,7 +10,7 @@ if (userName == 'Admin') { if (pass == 'TheMaster') { alert( 'Welcome!' ); } else if (pass == '' || pass == null) { - alert( 'Canceled.' ); + alert( 'Canceled' ); } else { alert( 'Wrong password' ); } diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/task.md b/1-js/02-first-steps/11-logical-operators/9-check-login/task.md index 780e674a..290a5264 100644 --- a/1-js/02-first-steps/11-logical-operators/9-check-login/task.md +++ b/1-js/02-first-steps/11-logical-operators/9-check-login/task.md @@ -6,17 +6,17 @@ importance: 3 Write the code which asks for a login with `prompt`. -If the visitor enters `"Admin"`, then `prompt` for a password, if the input is an empty line or `key:Esc` -- show "Canceled.", if it's another string -- then show "I don't know you". +If the visitor enters `"Admin"`, then `prompt` for a password, if the input is an empty line or `key:Esc` -- show "Canceled", if it's another string -- then show "I don't know you". The password is checked as follows: - If it equals "TheMaster", then show "Welcome!", - Another string -- show "Wrong password", -- For an empty string or cancelled input, show "Canceled." +- For an empty string or cancelled input, show "Canceled" The schema: -![](ifelse_task.png) +![](ifelse_task.svg) Please use nested `if` blocks. Mind the overall readability of the code. 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 4932020a..25f8ff7f 100644 --- a/1-js/02-first-steps/11-logical-operators/article.md +++ b/1-js/02-first-steps/11-logical-operators/article.md @@ -64,7 +64,7 @@ if (hour < 10 || hour > 18 || isWeekend) { } ``` -## 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,7 +84,7 @@ 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 such value is found. +In other words, a chain of OR `"||"` returns the first truthy value or the last one if no truthy value is found. For instance: @@ -101,7 +101,7 @@ 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 several variables which can either contain data or be `null/undefined`. How can we find the first one with data? + 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? We can use OR `||`: @@ -143,7 +143,7 @@ This leads to some interesting usage compared to a "pure, classical, boolean-onl alert(x); // 1 ``` - An assignment is a simple case. Other side effects can also be involved. + 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. @@ -186,7 +186,7 @@ if (1 && 0) { // evaluated as true && false ``` -## AND finds the first falsy value +## AND "&&" finds the first falsy value Given multiple AND'ed values: diff --git a/1-js/02-first-steps/12-while-for/article.md b/1-js/02-first-steps/12-while-for/article.md index 992c21af..580ac3e1 100644 --- a/1-js/02-first-steps/12-while-for/article.md +++ b/1-js/02-first-steps/12-while-for/article.md @@ -17,7 +17,7 @@ while (condition) { } ``` -While the `condition` is `true`, the `code` from the loop body is executed. +While the `condition` is truthy, the `code` from the loop body is executed. For instance, the loop below outputs `i` while `i < 3`: @@ -47,8 +47,8 @@ while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops } ``` -````smart header="Brackets are not required for a single-line body" -If the loop body has a single statement, we can omit the brackets `{…}`: +````smart header="Curly braces are not required for a single-line body" +If the loop body has a single statement, we can omit the curly braces `{…}`: ```js run let i = 3; @@ -84,7 +84,7 @@ This form of syntax should only be used when you want the body of the loop to ex ## The "for" loop -The `for` loop is the most commonly used loop. +The `for` loop is more complex, but it's also the most commonly used loop. It looks like this: @@ -108,11 +108,11 @@ Let's examine the `for` statement part-by-part: |-------|----------|----------------------------------------------------------------------------| | begin | `i = 0` | Executes once upon entering the loop. | | condition | `i < 3`| Checked before every loop iteration. If false, the loop stops. | -| step| `i++` | Executes after the body on each iteration but before the condition check. | | body | `alert(i)`| Runs again and again while the condition is truthy. | - +| step| `i++` | Executes after the body on each iteration. | The general loop algorithm works like this: + ``` Run begin → (if condition → run body and run step) @@ -121,6 +121,8 @@ Run begin → ... ``` +That is, `begin` executes once, and then it iterates: after each `condition` test, `body` and `step` are executed. + If you are new to loops, it could help to go back to the example and reproduce how it runs step-by-step on a piece of paper. Here's exactly what happens in our case: @@ -289,8 +291,7 @@ if (i > 5) { (i > 5) ? alert(i) : *!*continue*/!*; // continue isn't allowed here ``` -...it stops working. Code like this will give a syntax error: - +...it stops working: there's a syntax error. This is just another reason not to use the question mark operator `?` instead of `if`. ```` @@ -299,7 +300,7 @@ This is just another reason not to use the question mark operator `?` instead of Sometimes we need to break out from multiple nested loops at once. -For example, in the code below we loop over `i` and `j`, prompting for the coordinates `(i, j)` from `(0,0)` to `(3,3)`: +For example, in the code below we loop over `i` and `j`, prompting for the coordinates `(i, j)` from `(0,0)` to `(2,2)`: ```js run no-beautify for (let i = 0; i < 3; i++) { @@ -308,8 +309,7 @@ for (let i = 0; i < 3; i++) { let input = prompt(`Value at coords (${i},${j})`, ''); - // what if I want to exit from here to Done (below)? - + // what if we want to exit from here to Done (below)? } } @@ -358,12 +358,12 @@ for (let i = 0; i < 3; i++) { ... } The `continue` directive can also be used with a label. In this case, code execution jumps to the next iteration of the labeled loop. -````warn header="Labels are not a \"goto\"" +````warn header="Labels do not allow to \"jump\" anywhere" Labels do not allow us to jump into an arbitrary place in the code. For example, it is impossible to do this: ```js -break label; // jumps to label? No. +break label; // doesn't jumps to the label below label: for (...) ``` diff --git a/1-js/02-first-steps/13-switch/article.md b/1-js/02-first-steps/13-switch/article.md index 258f2406..dec40a53 100644 --- a/1-js/02-first-steps/13-switch/article.md +++ b/1-js/02-first-steps/13-switch/article.md @@ -125,7 +125,7 @@ switch (a) { break; *!* - case 3: // (*) grouped two cases + case 3: // (*) grouped two cases case 5: alert('Wrong!'); alert("Why don't you take a math class?"); diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md index 523bb127..46da079c 100644 --- a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md +++ b/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md @@ -13,7 +13,7 @@ function checkAge(age) { if (age > 18) { return true; } else { - return confirm('Do you have your parents permission to access this page?'); + return confirm('Did parents allow you?'); } } ``` diff --git a/1-js/02-first-steps/14-function-basics/4-pow/solution.md b/1-js/02-first-steps/14-function-basics/4-pow/solution.md index 5ef20c38..19fe9011 100644 --- a/1-js/02-first-steps/14-function-basics/4-pow/solution.md +++ b/1-js/02-first-steps/14-function-basics/4-pow/solution.md @@ -14,10 +14,8 @@ let x = prompt("x?", ''); let n = prompt("n?", ''); if (n < 1) { - alert(`Power ${n} is not supported, - use an integer greater than 0`); + alert(`Power ${n} is not supported, use a positive integer`); } else { alert( pow(x, n) ); } ``` - diff --git a/1-js/02-first-steps/14-function-basics/article.md b/1-js/02-first-steps/14-function-basics/article.md index 874890ab..b1881e31 100644 --- a/1-js/02-first-steps/14-function-basics/article.md +++ b/1-js/02-first-steps/14-function-basics/article.md @@ -20,9 +20,13 @@ function showMessage() { } ``` -The `function` keyword goes first, then goes the *name of the function*, then a list of *parameters* between the parentheses (empty in the example above) and finally the code of the function, also named "the function body", between curly braces. +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. -![](function_basics.png) +```js +function name(parameters) { + ...body... +} +``` Our new function can be called by its name: `showMessage()`. @@ -101,7 +105,7 @@ showMessage(); alert( userName ); // *!*Bob*/!*, the value was modified by the function ``` -The outer variable is only used if there's no local one. So an occasional modification may happen if we forget `let`. +The outer variable is only used if there's no local one. If a same-named variable is declared inside the function then it *shadows* the outer one. For instance, in the code below the function uses the local `userName`. The outer one is ignored: @@ -128,7 +132,7 @@ Variables declared outside of any function, such as the outer `userName` in the Global variables are visible from any function (unless shadowed by locals). -Usually, a function declares all variables specific to its task. Global variables only store project-level data, and it's important that these variables are accessible from anywhere. Modern code has few or no globals. Most variables reside in their functions. +It's a good practice to minimize the use of global variables. Modern code has few or no globals. Most variables reside in their functions. Sometimes though, they can be useful to store project-level data. ``` ## Parameters @@ -205,12 +209,11 @@ 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 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. This is in contrast to some other languages like Python, where any default parameters are evaluated only once during the initial interpretation. - +In the example above, `anotherFunction()` is called every time `showMessage()` is called without the `text` parameter. ``` - ````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. @@ -335,7 +338,19 @@ That doesn't work, because JavaScript assumes a semicolon after `return`. That'l return*!*;*/!* (some + long + expression + or + whatever * f(a) + f(b)) ``` -So, it effectively becomes an empty return. We should put the value on the same line instead. + +So, it effectively becomes an empty return. + +If we want the returned expression to wrap across multiple lines, we should start it at the same line as `return`. Or at least put the opening parentheses there as follows: + +```js +return ( + some + long + expression + + or + + whatever * f(a) + f(b) + ) +``` +And it will work just as we expect it to. ```` ## Naming a function [#function-naming] @@ -376,13 +391,13 @@ A few examples of breaking this rule: - `createForm` -- would be bad if it modifies the document, adding a form to it (should only create it and return). - `checkPermission` -- would be bad if it displays the `access granted/denied` message (should only perform the check and return the result). -These examples assume common meanings of prefixes. What they mean for you is determined by you and your team. Maybe it's pretty normal for your code to behave differently. But you should have a firm understanding of what a prefix means, what a prefixed function can and cannot do. All same-prefixed functions should obey the rules. And the team should share the knowledge. +These examples assume common meanings of prefixes. You and your team are free to agree on other meanings, but usually they're not much different. In any case, you should have a firm understanding of what a prefix means, what a prefixed function can and cannot do. All same-prefixed functions should obey the rules. And the team should share the knowledge. ``` ```smart header="Ultrashort function names" Functions that are used *very often* sometimes have ultrashort names. -For example, the [jQuery](http://jquery.com) framework defines a function with `$`. The [LoDash](http://lodash.com/) library has its core function named `_`. +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. ``` diff --git a/1-js/02-first-steps/14-function-basics/function_basics.png b/1-js/02-first-steps/14-function-basics/function_basics.png deleted file mode 100644 index a558b31a..00000000 Binary files a/1-js/02-first-steps/14-function-basics/function_basics.png and /dev/null differ diff --git a/1-js/02-first-steps/14-function-basics/function_basics@2x.png b/1-js/02-first-steps/14-function-basics/function_basics@2x.png deleted file mode 100644 index 4d64f985..00000000 Binary files a/1-js/02-first-steps/14-function-basics/function_basics@2x.png and /dev/null differ diff --git a/1-js/02-first-steps/15-function-expressions-arrows/article.md b/1-js/02-first-steps/15-function-expressions-arrows/article.md index b4ea19ba..d7f0f99c 100644 --- a/1-js/02-first-steps/15-function-expressions-arrows/article.md +++ b/1-js/02-first-steps/15-function-expressions-arrows/article.md @@ -22,7 +22,6 @@ 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`. - The meaning of these code samples is the same: "create a function and put it into the variable `sayHi`". We can even print out that value using `alert`: @@ -41,7 +40,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. -It is a special value of course, in the sense that we can call it like `sayHi()`. +Surely, a function is a special values, 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. @@ -61,21 +60,21 @@ sayHi(); // Hello // this still works too (why wouldn't it) Here's what happens above in detail: 1. The Function Declaration `(1)` creates the function and puts it into the variable named `sayHi`. -2. Line `(2)` copies it into the variable `func`. - - Please note again: there are no parentheses after `sayHi`. If there were, then `func = sayHi()` would write *the result of the call* `sayHi()` into `func`, not *the function* `sayHi` itself. +2. Line `(2)` copies it into the variable `func`. Please note again: there are no parentheses after `sayHi`. If there were, then `func = sayHi()` would write *the result of the call* `sayHi()` into `func`, not *the function* `sayHi` itself. 3. Now the function can be called as both `sayHi()` and `func()`. Note that we could also have used a Function Expression to declare `sayHi`, in the first line: ```js -let sayHi = function() { ... }; +let sayHi = function() { + alert( "Hello" ); +}; let func = sayHi; // ... ``` -Everything would work the same. Even more obvious what's going on, right? +Everything would work the same. ````smart header="Why is there a semicolon at the end?" @@ -93,7 +92,7 @@ let sayHi = function() { The answer is simple: - There's no need for `;` at the end of code blocks and syntax structures that use them like `if { ... }`, `for { }`, `function f { }` etc. -- A Function Expression is used inside the statement: `let sayHi = ...;`, as a value. It's not a code block. The semicolon `;` is recommended at the end of statements, no matter what is the value. So the semicolon here is not related to the Function Expression itself in any way, it just terminates the statement. +- 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. ```` ## Callback functions @@ -133,11 +132,11 @@ function showCancel() { ask("Do you agree?", showOk, showCancel); ``` -Before we explore how we can write it in a much shorter way, let's note that in the browser (and on the server-side in some cases) such functions are quite popular. The major difference between a real-life implementation and the example above is that real-life functions use more complex ways to interact with the user than a simple `confirm`. In the browser, such a function usually draws a nice-looking question window. But that's another story. +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. -**The arguments of `ask` are called *callback functions* or just *callbacks*.** +**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 the "yes" answer, and `showCancel` for the "no" answer. +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: @@ -156,12 +155,10 @@ ask( */!* ``` - Here, functions are declared right inside the `ask(...)` call. They have no name, and so are called *anonymous*. Such functions are not accessible outside of `ask` (because they are not assigned to variables), but that's just what we want here. Such code appears in our scripts very naturally, it's in the spirit of JavaScript. - ```smart header="A function is a value representing an \"action\"" Regular values like strings or numbers represent the *data*. @@ -175,7 +172,7 @@ We can pass it between variables and run when we want. Let's formulate the key differences between Function Declarations and Expressions. -First, the syntax: how to see what is what in the code. +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. @@ -186,7 +183,7 @@ First, the syntax: how to see what is what in the code. } ``` - *Function Expression:* a function, created inside an expression or inside another syntax construct. Here, the function is created at the right side of the "assignment expression" `=`: - + ```js // Function Expression let sum = function(a, b) { @@ -196,19 +193,19 @@ First, the syntax: how to see what is what in the code. The more subtle difference is *when* a function is created by the JavaScript engine. -**A Function Expression is created when the execution reaches it and is usable from then on.** +**A Function Expression is created when the execution reaches it and is usable only from that moment.** Once the execution flow passes to the right side of the assignment `let sum = function…` -- here we go, the function is created and can be used (assigned, called, etc. ) from now on. Function Declarations are different. -**A Function Declaration is usable in the whole script/code block.** +**A Function Declaration can be called earlier than it is defined.** -In other words, when JavaScript *prepares* to run the script or a code block, it first looks for Function Declarations in it and creates the functions. We can think of it as an "initialization stage". +For example, a global Function Declaration is visible in the whole script, no matter where it is. -And after all of the Function Declarations are processed, the execution goes on. +That's due to internal algorithms. When JavaScript prepares to run the script, it first looks for global Function Declarations in it and creates the functions. We can think of it as an "initialization stage". -As a result, a function declared as a Function Declaration can be called earlier than it is defined. +And after all Function Declarations are processed, the code is executed. So it has access to these functions. For example, this works: @@ -224,7 +221,7 @@ function sayHi(name) { The Function Declaration `sayHi` is created when JavaScript is preparing to start the script and is visible everywhere in it. -...If it was a Function Expression, then it wouldn't work: +...If it were a Function Expression, then it wouldn't work: ```js run refresh untrusted *!* @@ -238,13 +235,13 @@ let sayHi = function(name) { // (*) no magic any more Function Expressions are created when the execution reaches them. That would happen only in the line `(*)`. Too late. -**When a Function Declaration is made within a code block, it is visible everywhere inside that block. But not outside of it.** +Another special feature of Function Declarations is their block scope. -Sometimes that's handy to declare a local function only needed in that block alone. But that feature may also cause problems. +**In strict mode, when a Function Declaration is within a code block, it's visible everywhere inside that block. But not outside of it.** For instance, let's imagine that we need to declare a function `welcome()` depending on the `age` variable that we get during runtime. And then we plan to use it some time later. -The code below doesn't work: +If we use Function Declaration, it won't work as intended: ```js run let age = prompt("What is your age?", 18); @@ -292,7 +289,7 @@ if (age < 18) { } else { - function welcome() { // for age = 16, this "welcome" is never created + function welcome() { alert("Greetings!"); } } @@ -309,7 +306,7 @@ What can we do to make `welcome` visible outside of `if`? The correct approach would be to use a Function Expression and assign `welcome` to the variable that is declared outside of `if` and has the proper visibility. -Now it works as intended: +This code works as intended: ```js run let age = prompt("What is your age?", 18); @@ -350,12 +347,12 @@ welcome(); // ok now ``` -```smart header="When should you choose Function Declaration versus Function Expression?" -As a rule of thumb, when we need to declare a function, the first to consider is Function Declaration syntax, the one we used before. It gives more freedom in how to organize our code, because we can call such functions before they are declared. +```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. -It's also a little bit easier to look up `function f(…) {…}` in the code than `let f = function(…) {…}`. Function Declarations are more "eye-catching". +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 (we've seen an example above), then Function Expression should be used. +...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. ``` @@ -396,7 +393,7 @@ alert( sum(1, 2) ); // 3 ``` -If we have only one argument, then parentheses can be omitted, making that even shorter: +If we have only one argument, then parentheses around parameters can be omitted, making that even shorter: ```js run // same as @@ -456,7 +453,7 @@ alert( sum(1, 2) ); // 3 ```smart header="More to come" Here we praised arrow functions for brevity. But that's not all! Arrow functions have other interesting features. We'll return to them later in the chapter . -For now, we can already use them for one-line actions and callbacks. +For now, we can already use arrow functions for one-line actions and callbacks. ``` ## Summary @@ -467,7 +464,6 @@ For now, we can already use them for one-line actions and callbacks. - Function Declarations are processed before the code block is executed. They are visible everywhere in the block. - Function Expressions are created when the execution flow reaches them. - In most cases when we need to declare a function, a Function Declaration is preferable, because it is visible prior to the declaration itself. That gives us more flexibility in code organization, and is usually more readable. So we should use a Function Expression only when a Function Declaration is not fit for the task. We've seen a couple of examples of that in this chapter, and will see more in the future. diff --git a/1-js/02-first-steps/16-javascript-specials/article.md b/1-js/02-first-steps/16-javascript-specials/article.md index b421c4cb..b246a841 100644 --- a/1-js/02-first-steps/16-javascript-specials/article.md +++ b/1-js/02-first-steps/16-javascript-specials/article.md @@ -53,7 +53,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. +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. @@ -102,8 +102,8 @@ More in: and . We're using a browser as a working environment, so basic UI functions will be: -[`prompt(question[, default])`](mdn:api/Window/prompt) -: Ask a `question`, and return either what the visitor entered or `null` if they pressed "cancel". +[`prompt(question, [default])`](mdn:api/Window/prompt) +: Ask a `question`, and return either what the visitor entered or `null` if they clicked "cancel". [`confirm(question)`](mdn:api/Window/confirm) : Ask a `question` and suggest to choose between Ok and Cancel. The choice is returned as `true/false`. @@ -143,13 +143,13 @@ Assignments : There is a simple assignment: `a = b` and combined ones like `a *= 2`. Bitwise -: Bitwise operators work with integers on bit-level: see the [docs](mdn:/JavaScript/Reference/Operators/Bitwise_Operators) when they are needed. +: 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. -Ternary +Conditional : The only operator with three parameters: `cond ? resultA : resultB`. If `cond` is truthy, returns `resultA`, otherwise `resultB`. Logical operators -: Logical AND `&&` and OR `||` perform short-circuit evaluation and then return the value where it stopped. Logical NOT `!` converts the operand to boolean type and returns the inverse value. +: 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. 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: @@ -161,7 +161,7 @@ Comparisons Other comparisons convert to a number as well. - The strict equality operator `===` doesn't do the conversion: different types always mean different values for it, so: + The strict equality operator `===` doesn't do the conversion: different types always mean different values for it. Values `null` and `undefined` are special: they equal `==` each other and don't equal anything else. @@ -245,11 +245,9 @@ We covered three ways to create a function in JavaScript: let result = a + b; return result; - } + }; ``` - Function expressions can have a name, like `sum = function name(a, b)`, but that `name` is only visible inside that function. - 3. Arrow functions: ```js @@ -274,13 +272,7 @@ We covered three ways to create a function in JavaScript: - Parameters can have default values: `function sum(a = 1, b = 2) {...}`. - Functions always return something. If there's no `return` statement, then the result is `undefined`. - -| Function Declaration | Function Expression | -|----------------------|---------------------| -| visible in the whole code block | created when the execution reaches it | -| - | can have a name, visible only inside the function | - -More: see , . +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 eb943486..c777ae69 100644 --- a/1-js/03-code-quality/01-debugging-chrome/article.md +++ b/1-js/03-code-quality/01-debugging-chrome/article.md @@ -2,27 +2,27 @@ Before writing more complex code, let's talk about debugging. -All modern browsers and most other environments support "debugging" -- a special UI in developer tools that makes finding and fixing errors much easier. +[Debugging](https://en.wikipedia.org/wiki/Debugging) is the process of finding and fixing errors within a script. All modern browsers and most other environments support debugging tools -- a special UI in developer tools that makes debugging much easier. It also allows to trace the code step by step to see what exactly is going on. -We'll be using Chrome here, because it's probably the most feature-rich in this aspect. +We'll be using Chrome here, because it has enough features, most other browsers have a similar process`. -## The "sources" pane +## The "Sources" panel Your Chrome version may look a little bit different, but it still should be obvious what's there. - Open the [example page](debugging/index.html) in Chrome. - Turn on developer tools with `key:F12` (Mac: `key:Cmd+Opt+I`). -- Select the `sources` pane. +- Select the `Sources` panel. Here's what you should see if you are doing it for the first time: -![](chrome-open-sources.png) +![](chrome-open-sources.svg) The toggler button opens the tab with files. -Let's click it and select `index.html` and then `hello.js` in the tree view. Here's what should show up: +Let's click it and select `hello.js` in the tree view. Here's what should show up: -![](chrome-tabs.png) +![](chrome-tabs.svg) Here we can see three zones: @@ -34,13 +34,13 @@ Now you could click the same toggler -- continue the execution, hotkey `key:F8`. : Resumes the execution. If there are no additional breakpoints, then the execution just continues and the debugger loses control. Here's what we can see after a click on it: - ![](chrome-sources-debugger-trace-1.png) + ![](chrome-sources-debugger-trace-1.svg) - The execution has resumed, reached another breakpoint inside `say()` and paused there. Take a look at the "Call stack" at the right. It has increased by one more call. We're inside `say()` now. + 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. @@ -147,23 +147,23 @@ There are buttons for it at the top of the right pane. Let's engage them. ```smart header="Continue to here" Right click on a line of code opens the context menu with a great option called "Continue to here". -That's handy when we want to move multiple steps forward, but we're too lazy to set a breakpoint. +That's handy when we want to move multiple steps forward to the line, but we're too lazy to set a breakpoint. ``` ## Logging -To output something to console, there's `console.log` function. +To output something to console from our code, there's `console.log` function. For instance, this outputs values from `0` to `4` to console: ```js run // open console to see for (let i = 0; i < 5; i++) { - console.log("value", i); + console.log("value,", i); } ``` -Regular users don't see that output, it is in the console. To see it, either open the Console tab of developer tools or press `key:Esc` while in another tab: that opens the console at the bottom. +Regular users don't see that output, it is in the console. To see it, either open the Console panel of developer tools or press `key:Esc` while in another panel: that opens the console at the bottom. If we have enough logging in our code, then we can see what's going on from the records, without the debugger. @@ -174,10 +174,10 @@ As we can see, there are three main ways to pause a script: 2. The `debugger` statements. 3. An error (if dev tools are open and the button is "on"). -Then we can examine variables and step on to see where the execution goes wrong. +When paused, we can debug - examine variables and trace the code to see where the execution goes wrong. There are many more options in developer tools than covered here. The full manual is at . The information from this chapter is enough to begin debugging, but later, especially if you do a lot of browser stuff, please go there and look through more advanced capabilities of developer tools. -Oh, and also you can click at various places of dev tools and just see what's showing up. That's probably the fastest route to learn dev tools. Don't forget about the right click as well! +Oh, and also you can click at various places of dev tools and just see what's showing up. That's probably the fastest route to learn dev tools. Don't forget about the right click and context menus! diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png deleted file mode 100644 index efa3c19d..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg new file mode 100644 index 00000000..73bcc484 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg @@ -0,0 +1 @@ +open sources \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png deleted file mode 100644 index e184bdd0..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png deleted file mode 100644 index 2fe449c9..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg new file mode 100644 index 00000000..580bf638 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg @@ -0,0 +1 @@ +here's the listbreakpoints \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png deleted file mode 100644 index e4abc89d..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png deleted file mode 100644 index 98b22e77..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg new file mode 100644 index 00000000..39f4f917 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png deleted file mode 100644 index 3269a80f..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-console@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png deleted file mode 100644 index 719293d2..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg new file mode 100644 index 00000000..40df82a3 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause.svg @@ -0,0 +1 @@ +213jump to the outer functionwatch expressionscurrent variables \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png deleted file mode 100644 index 5c22ab36..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-pause@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png deleted file mode 100644 index 1848ccfa..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg new file mode 100644 index 00000000..dc96ea4f --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg @@ -0,0 +1 @@ +nested calls \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png deleted file mode 100644 index fcabf722..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png deleted file mode 100644 index ff91c531..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg new file mode 100644 index 00000000..4c762923 --- /dev/null +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs.svg @@ -0,0 +1 @@ +213 \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png deleted file mode 100644 index 09b10bf4..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome-tabs@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png b/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png deleted file mode 100644 index 95399c7b..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png deleted file mode 100644 index d9d576ec..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_break_error@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png deleted file mode 100644 index 0482bbed..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png deleted file mode 100644 index fc65ed3f..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png deleted file mode 100644 index ac8fb1ff..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png deleted file mode 100644 index d6eadbe6..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_break@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png deleted file mode 100644 index 22fb9a5d..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png deleted file mode 100644 index eba2b9bf..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_breakpoint@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png deleted file mode 100644 index 0f29946c..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png b/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png deleted file mode 100644 index 7a16ea1c..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/chrome_sources_buttons@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/console_error.png b/1-js/03-code-quality/01-debugging-chrome/console_error.png deleted file mode 100644 index ccf1b515..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/console_error.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png b/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png deleted file mode 100644 index 4ab2fcea..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/console_error@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage1.png b/1-js/03-code-quality/01-debugging-chrome/manage1.png deleted file mode 100644 index f624a1fd..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage1.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png deleted file mode 100644 index 3f3c8116..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage1@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage2.png b/1-js/03-code-quality/01-debugging-chrome/manage2.png deleted file mode 100644 index a038e310..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage2.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png deleted file mode 100644 index 904280e4..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage2@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage3.png b/1-js/03-code-quality/01-debugging-chrome/manage3.png deleted file mode 100644 index 94bd7b31..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage3.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png deleted file mode 100644 index 3a988aa2..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage3@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage4.png b/1-js/03-code-quality/01-debugging-chrome/manage4.png deleted file mode 100644 index 04f57c72..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage4.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png deleted file mode 100644 index d8758709..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage4@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage5.png b/1-js/03-code-quality/01-debugging-chrome/manage5.png deleted file mode 100644 index 55355c37..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage5.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png deleted file mode 100644 index c08f8a68..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage5@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage6.png b/1-js/03-code-quality/01-debugging-chrome/manage6.png deleted file mode 100644 index cdd9bd0e..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage6.png and /dev/null differ diff --git a/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png b/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png deleted file mode 100644 index 52dad975..00000000 Binary files a/1-js/03-code-quality/01-debugging-chrome/manage6@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/02-coding-style/article.md b/1-js/03-code-quality/02-coding-style/article.md index 290028a8..bdcfec54 100644 --- a/1-js/03-code-quality/02-coding-style/article.md +++ b/1-js/03-code-quality/02-coding-style/article.md @@ -2,13 +2,13 @@ Our code must be as clean and easy to read as possible. -That is actually the art of programming -- to take a complex task and code it in a way that is both correct and human-readable. +That is actually the art of programming -- to take a complex task and code it in a way that is both correct and human-readable. A good code style greatly assists in that. ## Syntax -Here is a cheatsheet with some suggested rules (see below for more details): +Here is a cheat sheet with some suggested rules (see below for more details): -![](code-style.png) +![](code-style.svg) -![](figure-bracket-style.png) - -In summary: -- For very short code, one line is acceptable. For example: `if (cond) return null`. -- But a separate line for each statement in brackets is usually easier to read. +For a very brief code, one line is allowed, e.g. `if (cond) return null`. But a code block (the last variant) is usually more readable. ### Line Length -No one likes to read a long horizontal line of code. It's best practice to split them up and limit the length of your lines. +No one likes to read a long horizontal line of code. It's best practice to split them. + +For example: +```js +// backtick quotes ` allow to split the string into multiple lines +let str = ` + Ecma International's TC39 is a group of JavaScript developers, + implementers, academics, and more, collaborating with the community + to maintain and evolve the definition of JavaScript. +`; +``` + +And, for `if` statements: + +```js +if ( + id === 123 && + moonPhase === 'Waning Gibbous' && + zodiacSign === 'Libra' +) { + letTheSorceryBegin(); +} +``` The maximum line length should be agreed upon at the team-level. It's usually 80 or 120 characters. @@ -88,9 +112,9 @@ There are two types of indents: - **Horizontal indents: 2 or 4 spaces.** - A horizontal indentation is made using either 2 or 4 spaces or the "Tab" symbol. Which one to choose is an old holy war. Spaces are more common nowadays. + A horizontal indentation is made using either 2 or 4 spaces or the horizontal tab symbol (key `key:Tab`). Which one to choose is an old holy war. Spaces are more common nowadays. - One advantage of spaces over tabs is that spaces allow more flexible configurations of indents than the "Tab" symbol. + One advantage of spaces over tabs is that spaces allow more flexible configurations of indents than the tab symbol. For instance, we can align the arguments with the opening bracket, like this: @@ -127,15 +151,15 @@ There are two types of indents: A semicolon should be present after each statement, even if it could possibly be skipped. -There are languages where a semicolon is truly optional and it is rarely used. In JavaScript, though, there are cases where a line break is not interpreted as a semicolon, leaving the code vulnerable to errors. +There are languages where a semicolon is truly optional and it is rarely used. In JavaScript, though, there are cases where a line break is not interpreted as a semicolon, leaving the code vulnerable to errors. See more about that in the chapter . -As you become more mature as a programmer, you may choose a no-semicolon style like [StandardJS](https://standardjs.com/). Until then, it's best to use semicolons to avoid possible pitfalls. +If you're an experienced JavaScript programmer, you may choose a no-semicolon code style like [StandardJS](https://standardjs.com/). Otherwise, it's best to use semicolons to avoid possible pitfalls. The majority of developers put semicolons. ### Nesting Levels Try to avoid nesting code too many levels deep. -Sometimes it's a good idea to use the ["continue"](info:while-for#continue) directive in a loop to avoid extra nesting. +For example, in the loop, it's sometimes a good idea to use the [`continue`](info:while-for#continue) directive to avoid extra nesting. For example, instead of adding a nested `if` conditional like this: @@ -197,13 +221,13 @@ function pow(x, n) { } ``` -The second one is more readable because the "edge case" of `n < 0` is handled early on. Once the check is done we can move on to the "main" code flow without the need for additional nesting. +The second one is more readable because the "special case" of `n < 0` is handled early on. Once the check is done we can move on to the "main" code flow without the need for additional nesting. ## Function Placement If you are writing several "helper" functions and the code that uses them, there are three ways to organize the functions. -1. Functions declared above the code that uses them: +1. Declare the functions *above* the code that uses them: ```js // *!*function declarations*/!* @@ -249,15 +273,15 @@ If you are writing several "helper" functions and the code that uses them, there Most of time, the second variant is preferred. -That's because when reading code, we first want to know *what it does*. If the code goes first, then it provides that information. Then, maybe we won't need to read the functions at all, especially if their names are descriptive of what they actually do. +That's because when reading code, we first want to know *what it does*. If the code goes first, then it becomes clear from the start. Then, maybe we won't need to read the functions at all, especially if their names are descriptive of what they actually do. ## Style Guides -A style guide contains general rules about "how to write" code, e.g. which quotes to use, how many spaces to indent, where to put line breaks, etc. A lot of minor things. +A style guide contains general rules about "how to write" code, e.g. which quotes to use, how many spaces to indent, the maximal line length, etc. A lot of minor things. When all members of a team use the same style guide, the code looks uniform, regardless of which team member wrote it. -Of course, a team can always write their own style guide. Most of the time though, there's no need to. There are many existing tried and true options to choose from, so adopting one of these is usually your best bet. +Of course, a team can always write their own style guide, but usually there's no need to. There are many existing guides to choose from. Some popular choices: @@ -267,15 +291,15 @@ Some popular choices: - [StandardJS](https://standardjs.com/) - (plus many more) -If you're a novice developer, start with the cheatsheet at the beginning of this chapter. Once you've mastered that you can browse other style guides to pick up common principles and decide which one you like best. +If you're a novice developer, start with the cheat sheet at the beginning of this chapter. Then you can browse other style guides to pick up more ideas and decide which one you like best. ## Automated Linters -Linters are tools that can automatically check the style of your code and make suggestions for refactoring. +Linters are tools that can automatically check the style of your code and make improving suggestions. -The great thing about them is that style-checking can also find some bugs, like typos in variable or function names. Because of this feature, installing a linter is recommended even if you don't want to stick to one particular "code style". +The great thing about them is that style-checking can also find some bugs, like typos in variable or function names. Because of this feature, using a linter is recommended even if you don't want to stick to one particular "code style". -Here are the most well-known linting tools: +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. @@ -287,7 +311,7 @@ Most linters are integrated with many popular editors: just enable the plugin in For instance, for ESLint you should do the following: -1. Install [Node.JS](https://nodejs.org/). +1. Install [Node.js](https://nodejs.org/). 2. Install ESLint with the command `npm install -g eslint` (npm is a JavaScript package installer). 3. Create a config file named `.eslintrc` in the root of your JavaScript project (in the folder that contains all your files). 4. Install/enable the plugin for your editor that integrates with ESLint. The majority of editors have one. @@ -304,8 +328,8 @@ Here's an example of an `.eslintrc` file: }, "rules": { "no-console": 0, - }, - "indent": 2 + "indent": ["warning", 2] + } } ``` @@ -317,8 +341,8 @@ Also certain IDEs have built-in linting, which is convenient but not as customiz ## Summary -All syntax rules described in this chapter (and in the style guides referenced) aim to increase the readability of your code, but all of them are debatable. +All syntax rules described in this chapter (and in the style guides referenced) aim to increase the readability of your code. All of them are debatable. -When we think about writing "better" code, the questions we should ask are, "What makes the code more readable and easier to understand?" and "What can help us avoid errors?" These are the main things to keep in mind when choosing and debating code styles. +When we think about writing "better" code, the questions we should ask ourselves are: "What makes the code more readable and easier to understand?" and "What can help us avoid errors?" These are the main things to keep in mind when choosing and debating code styles. Reading popular style guides will allow you to keep up to date with the latest ideas about code style trends and best practices. diff --git a/1-js/03-code-quality/02-coding-style/code-style.png b/1-js/03-code-quality/02-coding-style/code-style.png deleted file mode 100644 index 8c457f5e..00000000 Binary files a/1-js/03-code-quality/02-coding-style/code-style.png and /dev/null differ diff --git a/1-js/03-code-quality/02-coding-style/code-style.svg b/1-js/03-code-quality/02-coding-style/code-style.svg new file mode 100644 index 00000000..4cb4a893 --- /dev/null +++ b/1-js/03-code-quality/02-coding-style/code-style.svg @@ -0,0 +1 @@ +2No space between the function name and parentheses between the parentheses and the parameterIndentation 2 spacesA space after for/if/while…} else { without a line breakSpaces around a nested callAn empty line between logical blocksLines are not very longA semicolon ; is mandatorySpaces around operatorsCurly brace { on the same line, after a spaceA space between parametersA space between parameters \ No newline at end of file diff --git a/1-js/03-code-quality/02-coding-style/code-style@2x.png b/1-js/03-code-quality/02-coding-style/code-style@2x.png deleted file mode 100644 index 6789e6f6..00000000 Binary files a/1-js/03-code-quality/02-coding-style/code-style@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/02-coding-style/figure-bracket-style.png b/1-js/03-code-quality/02-coding-style/figure-bracket-style.png deleted file mode 100644 index 9800b1c4..00000000 Binary files a/1-js/03-code-quality/02-coding-style/figure-bracket-style.png and /dev/null differ diff --git a/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png b/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png deleted file mode 100644 index 8e917e9b..00000000 Binary files a/1-js/03-code-quality/02-coding-style/figure-bracket-style@2x.png and /dev/null differ diff --git a/1-js/03-code-quality/03-comments/article.md b/1-js/03-code-quality/03-comments/article.md index 930ff929..29ba701f 100644 --- a/1-js/03-code-quality/03-comments/article.md +++ b/1-js/03-code-quality/03-comments/article.md @@ -4,7 +4,7 @@ As we know from the chapter , comments can be single-line: start We normally use them to describe how and why the code works. -From the first sight, commenting might be obvious, but novices in programming usually get it wrong. +At first sight, commenting might be obvious, but novices in programming often use them wrongly. ## Bad comments @@ -18,7 +18,7 @@ complex; code; ``` -But in good code the amount of such "explanatory" comments should be minimal. Seriously, code should be easy to understand without them. +But in good code, the amount of such "explanatory" comments should be minimal. Seriously, the code should be easy to understand without them. There's a great rule about that: "if the code is so unclear that it requires a comment, then maybe it should be rewritten instead". @@ -120,9 +120,9 @@ In reality, we can't totally avoid "explanatory" comments. There are complex alg So, explanatory comments are usually bad. Which comments are good? Describe the architecture -: Provide a high-level overview of components, how they interact, what's the control flow in various situations... In short -- the bird's eye view of the code. There's a special diagram language [UML](http://wikipedia.org/wiki/Unified_Modeling_Language) for high-level architecture diagrams. Definitely worth studying. +: Provide a high-level overview of components, how they interact, what's the control flow in various situations... In short -- the bird's eye view of the code. There's a special language [UML](http://wikipedia.org/wiki/Unified_Modeling_Language) to build high-level architecture diagrams explaining the code. Definitely worth studying. -Document a function usage +Document function parameters and usage : There's a special syntax [JSDoc](http://en.wikipedia.org/wiki/JSDoc) to document a function: usage, parameters, returned value. For instance: @@ -175,6 +175,6 @@ Good comments allow us to maintain the code well, come back to it after a delay **Avoid comments:** - That tell "how code works" and "what it does". -- Put them only if it's impossible to make the code so simple and self-descriptive that it doesn't require those. +- Put them in only if it's impossible to make the code so simple and self-descriptive that it doesn't require them. Comments are also used for auto-documenting tools like JSDoc3: they read them and generate HTML-docs (or docs in another format). diff --git a/1-js/03-code-quality/04-ninja-code/article.md b/1-js/03-code-quality/04-ninja-code/article.md index 9019242f..7846f6e2 100644 --- a/1-js/03-code-quality/04-ninja-code/article.md +++ b/1-js/03-code-quality/04-ninja-code/article.md @@ -137,7 +137,7 @@ Instead, reuse existing names. Just write new values into them. In a function try to use only variables passed as parameters. -That would make it really hard to identify what's exactly in the variable *now*. And also where it comes from. A person with weak intuition would have to analyze the code line-by-line and track the changes through every code branch. +That would make it really hard to identify what's exactly in the variable *now*. And also where it comes from. The purpose is to develop the intuition and memory of a person reading the code. A person with weak intuition would have to analyze the code line-by-line and track the changes through every code branch. **An advanced variant of the approach is to covertly (!) replace the value with something alike in the middle of a loop or a function.** @@ -155,7 +155,7 @@ function ninjaFunction(elem) { A fellow programmer who wants to work with `elem` in the second half of the function will be surprised... Only during the debugging, after examining the code they will find out that they're working with a clone! -Seen in code regularly. Deadly effective even against an experienced ninja. +Seen in code regularly. Deadly effective even against an experienced ninja. ## Underscores for fun @@ -169,8 +169,7 @@ A smart ninja puts underscores at one spot of code and evades them at other plac Let everyone see how magnificent your entities are! Names like `superElement`, `megaFrame` and `niceItem` will definitely enlighten a reader. -Indeed, from one hand, something is written: `super..`, `mega..`, `nice..` But from the other hand -- that brings no details. A reader may decide to look for a hidden meaning and meditate for an hour or two. - +Indeed, from one hand, something is written: `super..`, `mega..`, `nice..` But from the other hand -- that brings no details. A reader may decide to look for a hidden meaning and meditate for an hour or two of their paid working time. ## Overlap outer variables @@ -180,7 +179,7 @@ When in the light, can't see anything in the darkness.
When in the darkness, can see everything in the light. ``` -Use same names for variables inside and outside a function. As simple. No efforts required. +Use same names for variables inside and outside a function. As simple. No efforts to invent new names. ```js let *!*user*/!* = authenticateUser(); diff --git a/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md index 7b58f0bf..4d0571b9 100644 --- a/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md +++ b/1-js/03-code-quality/05-testing-mocha/3-pow-test-wrong/solution.md @@ -4,7 +4,7 @@ What we have here is actually 3 tests, but layed out as a single function with 3 Sometimes it's easier to write this way, but if an error occurs, it's much less obvious what went wrong. -If an error happens inside a complex execution flow, then we'll have to figure out the data at that point. We'll actually have to *debug the test*. +If an error happens in the middle of a complex execution flow, then we'll have to figure out the data at that point. We'll actually have to *debug the test*. It would be much better to break the test into multiple `it` blocks with clearly written inputs and outputs. diff --git a/1-js/03-code-quality/05-testing-mocha/article.md b/1-js/03-code-quality/05-testing-mocha/article.md index a22f44aa..ca639fb6 100644 --- a/1-js/03-code-quality/05-testing-mocha/article.md +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -1,8 +1,6 @@ -# Automated testing with mocha +# Automated testing with Mocha -Automated testing will be used in further tasks. - -It's actually a part of the "educational minimum" of a developer. +Automated testing will be used in further tasks, and it's also widely used in real projects. ## Why we need tests? @@ -20,15 +18,15 @@ For instance, we're creating a function `f`. Wrote some code, testing: `f(1)` wo That's very typical. When we develop something, we keep a lot of possible use cases in mind. But it's hard to expect a programmer to check all of them manually after every change. So it becomes easy to fix one thing and break another one. -**Automated testing means that tests are written separately, in addition to the code. They can be executed easily and check all the main use cases.** +**Automated testing means that tests are written separately, in addition to the code. They run our functions in various ways and compare results with the expected.** ## Behavior Driven Development (BDD) -Let's use a technique named [Behavior Driven Development](http://en.wikipedia.org/wiki/Behavior-driven_development) or, in short, BDD. That approach is used among many projects. BDD is not just about testing. That's more. +Let's start with a technique named [Behavior Driven Development](http://en.wikipedia.org/wiki/Behavior-driven_development) or, in short, BDD. **BDD is three things in one: tests AND documentation AND examples.** -Enough words. Let's see the example. +To understand BDD, we'll examine a practical case of development. ## Development of "pow": the spec @@ -38,7 +36,7 @@ That task is just an example: there's the `**` operator in JavaScript that can d Before creating the code of `pow`, we can imagine what the function should do and describe it. -Such description is called a *specification* or, in short, a spec, and looks like this: +Such description is called a *specification* or, in short, a spec, and contains descriptions of use cases together with tests for them, like this: ```js describe("pow", function() { @@ -53,17 +51,17 @@ describe("pow", function() { A spec has three main building blocks that you can see above: `describe("title", function() { ... })` -: What functionality we're describing. Uses to group "workers" -- the `it` blocks. In our case we're describing the function `pow`. +: What functionality we're describing. In our case we're describing the function `pow`. Used to group "workers" -- the `it` blocks. -`it("title", function() { ... })` +`it("use case description", function() { ... })` : In the title of `it` we *in a human-readable way* describe the particular use case, and the second argument is a function that tests it. `assert.equal(value1, value2)` : The code inside `it` block, if the implementation is correct, should execute without errors. - Functions `assert.*` are used to check whether `pow` works as expected. Right here we're using one of them -- `assert.equal`, it compares arguments and yields an error if they are not equal. Here it checks that the result of `pow(2, 3)` equals `8`. + Functions `assert.*` are used to check whether `pow` works as expected. Right here we're using one of them -- `assert.equal`, it compares arguments and yields an error if they are not equal. Here it checks that the result of `pow(2, 3)` equals `8`. There are other types of comparisons and checks, that we'll add later. - There are other types of comparisons and checks that we'll see further. +The specification can be executed, and it will run the test specified in `it` block. We'll see that later. ## The development flow @@ -71,7 +69,7 @@ The flow of development usually looks like this: 1. An initial spec is written, with tests for the most basic functionality. 2. An initial implementation is created. -3. To check whether it works, we run the testing framework [Mocha](http://mochajs.org/) (more details soon) that runs the spec. Errors are displayed. We make corrections until everything works. +3. To check whether it works, we run the testing framework [Mocha](http://mochajs.org/) (more details soon) that runs the spec. While the functionality is not complete, errors are displayed. We make corrections until everything works. 4. Now we have a working initial implementation with tests. 5. We add more use cases to the spec, probably not yet supported by the implementations. Tests start to fail. 6. Go to 3, update the implementation till tests give no errors. @@ -79,7 +77,9 @@ The flow of development usually looks like this: So, the development is *iterative*. We write the spec, implement it, make sure tests pass, then write more tests, make sure they work etc. At the end we have both a working implementation and tests for it. -In our case, the first step is complete: we have an initial spec for `pow`. So let's make an implementation. But before that let's make a "zero" run of the spec, just to see that tests are working (they will all fail). +Let's see this development flow in our practical case. + +The first step is already complete: we have an initial spec for `pow`. Now, before making the implementaton, let's use few JavaScript libraries to run the tests, just to see that they are working (they will all fail). ## The spec in action @@ -110,14 +110,14 @@ The result: As of now, the test fails, there's an error. That's logical: we have an empty function code in `pow`, so `pow(2,3)` returns `undefined` instead of `8`. -For the future, let's note that there are advanced test-runners, like [karma](https://karma-runner.github.io/) and others. So it's generally not a problem to setup many different tests. +For the future, let's note that there are more high-level test-runners, like [karma](https://karma-runner.github.io/) and others, that make it easy to autorun many different tests. ## Initial implementation Let's make a simple implementation of `pow`, for tests to pass: ```js -function pow() { +function pow(x, n) { return 8; // :) we cheat! } ``` @@ -132,7 +132,7 @@ What we've done is definitely a cheat. The function does not work: an attempt to ...But the situation is quite typical, it happens in practice. Tests pass, but the function works wrong. Our spec is imperfect. We need to add more use cases to it. -Let's add one more test to see if `pow(3, 4) = 81`. +Let's add one more test to check that `pow(3, 4) = 81`. We can select one of two ways to organize the test here: @@ -296,7 +296,7 @@ Testing finished – after all tests (after) [edit src="beforeafter" title="Open the example in the sandbox."] -Usually, `beforeEach/afterEach` (`before/after`) are used to perform initialization, zero out counters or do something else between the tests (or test groups). +Usually, `beforeEach/afterEach` and `before/after` are used to perform initialization, zero out counters or do something else between the tests (or test groups). ```` ## Extending the spec @@ -336,10 +336,9 @@ The result with new tests: The newly added tests fail, because our implementation does not support them. That's how BDD is done: first we write failing tests, and then make an implementation for them. ```smart header="Other assertions" - Please note the assertion `assert.isNaN`: it checks for `NaN`. -There are other assertions in Chai as well, for instance: +There are other assertions in [Chai](http://chaijs.com) as well, for instance: - `assert.equal(value1, value2)` -- checks the equality `value1 == value2`. - `assert.strictEqual(value1, value2)` -- checks the strict equality `value1 === value2`. @@ -380,9 +379,9 @@ In BDD, the spec goes first, followed by implementation. At the end we have both The spec can be used in three ways: -1. **Tests** guarantee that the code works correctly. -2. **Docs** -- the titles of `describe` and `it` tell what the function does. -3. **Examples** -- the tests are actually working examples showing how a function can be used. +1. As **Tests** - they guarantee that the code works correctly. +2. As **Docs** -- the titles of `describe` and `it` tell what the function does. +3. As **Examples** -- the tests are actually working examples showing how a function can be used. With the spec, we can safely improve, change, even rewrite the function from scratch and make sure it still works right. @@ -390,23 +389,21 @@ That's especially important in large projects when a function is used in many pl Without tests, people have two ways: -1. To perform the change, no matter what. And then our users meet bugs and report them. If we can afford that. -2. Or people become afraid to modify such functions, if the punishment for errors is harsh. Then it becomes old, overgrown with cobwebs, no one wants to get into it, and that's not good. +1. To perform the change, no matter what. And then our users meet bugs, as we probably fail to check something manually. +2. Or, if the punishment for errors is harsh, as there are no tests, people become afraid to modify such functions, and then the code becomes outdated, no one wants to get into it. Not good for development. -**Automatically tested code is contrary to that!** +**Automatic testing helps to avoid these problems!** -If the project is covered with tests, there's just no such problem. We can run tests and see a lot of checks made in a matter of seconds. +If the project is covered with tests, there's just no such problem. After any changes, we can run tests and see a lot of checks made in a matter of seconds. **Besides, a well-tested code has better architecture.** -Naturally, that's because it's easier to change and improve it. But not only that. +Naturally, that's because auto-tested code is easier to modify and improve. But there's also another reason. To write tests, the code should be organized in such a way that every function has a clearly described task, well-defined input and output. That means a good architecture from the beginning. In real life that's sometimes not that easy. Sometimes it's difficult to write a spec before the actual code, because it's not yet clear how it should behave. But in general writing tests makes development faster and more stable. -## What now? - Later in the tutorial you will meet many tasks with tests baked-in. So you'll see more practical examples. Writing tests requires good JavaScript knowledge. But we're just starting to learn it. So, to settle down everything, as of now you're not required to write tests, but you should already be able to read them even if they are a little bit more complex than in this chapter. diff --git a/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html b/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html index d82a79dc..e8d6be23 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html +++ b/1-js/03-code-quality/05-testing-mocha/pow-2.view/index.html @@ -20,7 +20,7 @@ diff --git a/1-js/03-code-quality/06-polyfills/article.md b/1-js/03-code-quality/06-polyfills/article.md index 907730fd..b399fd42 100644 --- a/1-js/03-code-quality/06-polyfills/article.md +++ b/1-js/03-code-quality/06-polyfills/article.md @@ -19,21 +19,20 @@ Here Babel comes to the rescue. Actually, there are two parts in Babel: -1. First, the transpiler program, which rewrites the code. The developer runs it on their own computer. It rewrites the code into the older standard. And then the code is delivered to the website for users. Modern project build system like [webpack](http://webpack.github.io/) or [brunch](http://brunch.io/) provide means to run transpiler automatically on every code change, so that doesn't involve any time loss from our side. +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. 2. Second, the polyfill. - The transpiler rewrites the code, so syntax features are covered. But for new functions we need to write a special script that implements them. JavaScript is a highly dynamic language, scripts may not just add new functions, but also modify built-in ones, so that they behave according to the modern standard. + 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. - There's a term "polyfill" for scripts that "fill in" the gap and add missing implementations. + A script that updates/adds new functions is called "polyfill". It "fills in" the gap and adds missing implementations. Two interesting polyfills are: - - [babel polyfill](https://babeljs.io/docs/usage/polyfill/) that supports a lot, but is big. - - [polyfill.io](http://polyfill.io) service that allows to load/construct polyfills on-demand, depending on the features we need. + - [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. -So, we need to setup the transpiler and add the polyfill for old engines to support modern features. - -If we orient towards modern engines and do not use features except those supported everywhere, then we don't need to use Babel. +So, if we're going to use modern language features, a transpiler and a polyfill are necessary. ## Examples in the tutorial @@ -49,9 +48,7 @@ Examples that use modern JS will work only if your browser supports it. ```` ```offline -As you're reading the offline version, examples are not runnable. But they usually work :) +As you're reading the offline version, in PDF examples are not runnable. In EPUB some of them can run. ``` -[Chrome Canary](https://www.google.com/chrome/browser/canary.html) is good for all examples, but other modern browsers are mostly fine too. - -Note that on production we can use Babel to translate the code into suitable for less recent browsers, so there will be no such limitation, the code will run everywhere. +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. diff --git a/1-js/04-object-basics/01-object/article.md b/1-js/04-object-basics/01-object/article.md index f15a3368..120e8dde 100644 --- a/1-js/04-object-basics/01-object/article.md +++ b/1-js/04-object-basics/01-object/article.md @@ -9,7 +9,7 @@ An object can be created with figure brackets `{…}` with an optional list of * We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the key. It's easy to find a file by its name or add/remove a file. -![](object.png) +![](object.svg) An empty object ("empty cabinet") can be created using one of two syntaxes: @@ -18,7 +18,7 @@ let user = new Object(); // "object constructor" syntax let user = {}; // "object literal" syntax ``` -![](object-user-empty.png) +![](object-user-empty.svg) Usually, the figure brackets `{...}` are used. That declaration is called an *object literal*. @@ -42,14 +42,14 @@ In the `user` object, there are two properties: The resulting `user` object can be imagined as a cabinet with two signed files labeled "name" and "age". -![user object](object-user.png) +![user object](object-user.svg) We can add, remove and read files from it any time. Property values are accessible using the dot notation: ```js -// get fields of the object: +// get property values of the object: alert( user.name ); // John alert( user.age ); // 30 ``` @@ -60,7 +60,7 @@ The value can be of any type. Let's add a boolean one: user.isAdmin = true; ``` -![user object 2](object-user-isadmin.png) +![user object 2](object-user-isadmin.svg) To remove a property, we can use `delete` operator: @@ -68,7 +68,7 @@ To remove a property, we can use `delete` operator: delete user.age; ``` -![user object 3](object-user-delete.png) +![user object 3](object-user-delete.svg) We can also use multiword property names, but then they must be quoted: @@ -80,7 +80,7 @@ let user = { }; ``` -![](object-user-props.png) +![](object-user-props.svg) The last property in the list may end with a comma: @@ -105,7 +105,6 @@ That's because the dot requires the key to be a valid variable identifier. That There's an alternative "square bracket notation" that works with any string: - ```js run let user = {}; @@ -130,7 +129,7 @@ let key = "likes birds"; user[key] = true; ``` -Here, the variable `key` may be calculated at run-time or depend on the user input. And then we use it to access the property. That gives us a great deal of flexibility. The dot notation cannot be used in a similar way. +Here, the variable `key` may be calculated at run-time or depend on the user input. And then we use it to access the property. That gives us a great deal of flexibility. For instance: @@ -146,6 +145,17 @@ let key = prompt("What do you want to know about the user?", "name"); alert( user[key] ); // John (if enter "name") ``` +The dot notation cannot be used in a similar way: + +```js run +let user = { + name: "John", + age: 30 +}; + +let key = "name"; +alert( user.key ) // undefined +``` ### Computed properties @@ -222,10 +232,11 @@ 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). +In that case the visitor may choose `__proto__` as the key, and the assignment logic will be ruined (as shown above). There is a way to make objects treat `__proto__` as a regular property, which we'll cover later, but first we need to know more about objects. -There's also another data structure [Map](info:map-set-weakmap-weakset), that we'll learn in the chapter , which supports arbitrary keys. + +There's also another data structure [Map](info:map-set), that we'll learn in the chapter , which supports arbitrary keys. ```` @@ -311,7 +322,7 @@ alert( *!*key*/!* in user ); // true, takes the name from key and checks for suc ``` ````smart header="Using \"in\" for properties that store `undefined`" -Usually, the strict comparison `"=== undefined"` check works fine. But there's a special case when it fails, but `"in"` works correctly. +Usually, the strict comparison `"=== undefined"` check the property existance just fine. But there's a special case when it fails, but `"in"` works correctly. It's when an object property exists, but stores `undefined`: @@ -331,7 +342,6 @@ In the code above, the property `obj.test` technically exists. So the `in` opera 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. ```` - ## The "for..in" loop 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. @@ -464,7 +474,7 @@ let phrase = message; As a result we have two independent variables, each one is storing the string `"Hello!"`. -![](variable-copy-value.png) +![](variable-copy-value.svg) Objects are not like that. @@ -478,7 +488,7 @@ let user = { }; ``` -![](variable-contains-reference.png) +![](variable-contains-reference.svg) Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it. @@ -496,7 +506,7 @@ let admin = user; // copy the reference Now we have two variables, each one with the reference to the same object: -![](variable-copy-reference.png) +![](variable-copy-reference.svg) We can use any variable to access the cabinet and modify its contents: @@ -520,7 +530,7 @@ The equality `==` and strict equality `===` operators for objects work exactly t **Two objects are equal only if they are the same object.** -For instance, two variables reference the same object, they are equal: +For instance, if two variables reference the same object, they are equal: ```js run let a = {}; @@ -559,7 +569,7 @@ user.age = 25; // (*) alert(user.age); // 25 ``` -It might seem that the line `(*)` would cause an error, but no, there's totally no problem. That's because `const` fixes the value of `user` itself. And here `user` stores the reference to the same object all the time. The line `(*)` goes *inside* the object, it doesn't reassign `user`. +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: @@ -616,7 +626,7 @@ Also we can use the method [Object.assign](mdn:js/Object/assign) for that. The syntax is: ```js -Object.assign(dest[, src1, src2, src3...]) +Object.assign(dest, [src1, src2, src3...]) ``` - Arguments `dest`, and `src1, ..., srcN` (can be as many as needed) are objects. diff --git a/1-js/04-object-basics/01-object/object-user-delete.png b/1-js/04-object-basics/01-object/object-user-delete.png deleted file mode 100644 index 688158f9..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-delete.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-delete.svg b/1-js/04-object-basics/01-object/object-user-delete.svg new file mode 100644 index 00000000..87753bb1 --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-delete.svg @@ -0,0 +1 @@ +nameisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-delete@2x.png b/1-js/04-object-basics/01-object/object-user-delete@2x.png deleted file mode 100644 index e1ef6554..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-delete@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-empty.png b/1-js/04-object-basics/01-object/object-user-empty.png deleted file mode 100644 index 80fdc0d3..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-empty.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-empty.svg b/1-js/04-object-basics/01-object/object-user-empty.svg new file mode 100644 index 00000000..df684a0a --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-empty.svg @@ -0,0 +1 @@ +emptyuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-empty@2x.png b/1-js/04-object-basics/01-object/object-user-empty@2x.png deleted file mode 100644 index 8db894cb..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-empty@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.png b/1-js/04-object-basics/01-object/object-user-isadmin.png deleted file mode 100644 index 4e76eeb7..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-isadmin.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-isadmin.svg b/1-js/04-object-basics/01-object/object-user-isadmin.svg new file mode 100644 index 00000000..c7a24cc9 --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-isadmin.svg @@ -0,0 +1 @@ +nameageisAdminuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-isadmin@2x.png b/1-js/04-object-basics/01-object/object-user-isadmin@2x.png deleted file mode 100644 index b4097769..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-isadmin@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-props.png b/1-js/04-object-basics/01-object/object-user-props.png deleted file mode 100644 index 2bfdfabd..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-props.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user-props.svg b/1-js/04-object-basics/01-object/object-user-props.svg new file mode 100644 index 00000000..cb9afc1a --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user-props.svg @@ -0,0 +1 @@ +nameagelikes birdsuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user-props@2x.png b/1-js/04-object-basics/01-object/object-user-props@2x.png deleted file mode 100644 index 4935b59c..00000000 Binary files a/1-js/04-object-basics/01-object/object-user-props@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user.png b/1-js/04-object-basics/01-object/object-user.png deleted file mode 100644 index 16179209..00000000 Binary files a/1-js/04-object-basics/01-object/object-user.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object-user.svg b/1-js/04-object-basics/01-object/object-user.svg new file mode 100644 index 00000000..69f7efd9 --- /dev/null +++ b/1-js/04-object-basics/01-object/object-user.svg @@ -0,0 +1 @@ +nameageuser \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object-user@2x.png b/1-js/04-object-basics/01-object/object-user@2x.png deleted file mode 100644 index 72038953..00000000 Binary files a/1-js/04-object-basics/01-object/object-user@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object.png b/1-js/04-object-basics/01-object/object.png deleted file mode 100644 index f94d094a..00000000 Binary files a/1-js/04-object-basics/01-object/object.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/object.svg b/1-js/04-object-basics/01-object/object.svg new file mode 100644 index 00000000..5a4a49fb --- /dev/null +++ b/1-js/04-object-basics/01-object/object.svg @@ -0,0 +1 @@ +key1key2key3 \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/object@2x.png b/1-js/04-object-basics/01-object/object@2x.png deleted file mode 100644 index 003c2f6e..00000000 Binary files a/1-js/04-object-basics/01-object/object@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-contains-reference.png b/1-js/04-object-basics/01-object/variable-contains-reference.png deleted file mode 100644 index c417ac1c..00000000 Binary files a/1-js/04-object-basics/01-object/variable-contains-reference.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-contains-reference.svg b/1-js/04-object-basics/01-object/variable-contains-reference.svg new file mode 100644 index 00000000..dedb7eaa --- /dev/null +++ b/1-js/04-object-basics/01-object/variable-contains-reference.svg @@ -0,0 +1 @@ +username \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-contains-reference@2x.png b/1-js/04-object-basics/01-object/variable-contains-reference@2x.png deleted file mode 100644 index 0b877009..00000000 Binary files a/1-js/04-object-basics/01-object/variable-contains-reference@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.png b/1-js/04-object-basics/01-object/variable-copy-reference.png deleted file mode 100644 index 919a4f57..00000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-reference.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.svg b/1-js/04-object-basics/01-object/variable-copy-reference.svg new file mode 100644 index 00000000..f212c085 --- /dev/null +++ b/1-js/04-object-basics/01-object/variable-copy-reference.svg @@ -0,0 +1 @@ +useradminname \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-copy-reference@2x.png b/1-js/04-object-basics/01-object/variable-copy-reference@2x.png deleted file mode 100644 index 7a35fb15..00000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-reference@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-value.png b/1-js/04-object-basics/01-object/variable-copy-value.png deleted file mode 100644 index e5c330c3..00000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-value.png and /dev/null differ diff --git a/1-js/04-object-basics/01-object/variable-copy-value.svg b/1-js/04-object-basics/01-object/variable-copy-value.svg new file mode 100644 index 00000000..37b1fe86 --- /dev/null +++ b/1-js/04-object-basics/01-object/variable-copy-value.svg @@ -0,0 +1 @@ +"Hello!"message"Hello!"phras e \ No newline at end of file diff --git a/1-js/04-object-basics/01-object/variable-copy-value@2x.png b/1-js/04-object-basics/01-object/variable-copy-value@2x.png deleted file mode 100644 index 98edf5c8..00000000 Binary files a/1-js/04-object-basics/01-object/variable-copy-value@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/article.md b/1-js/04-object-basics/02-garbage-collection/article.md index 27682ef4..620cbc21 100644 --- a/1-js/04-object-basics/02-garbage-collection/article.md +++ b/1-js/04-object-basics/02-garbage-collection/article.md @@ -38,7 +38,7 @@ let user = { }; ``` -![](memory-user-john.png) +![](memory-user-john.svg) Here the arrow depicts an object reference. The global variable `"user"` references the object `{name: "John"}` (we'll call it John for brevity). The `"name"` property of John stores a primitive, so it's painted inside the object. @@ -48,7 +48,7 @@ If the value of `user` is overwritten, the reference is lost: user = null; ``` -![](memory-user-john-lost.png) +![](memory-user-john-lost.svg) Now John becomes unreachable. There's no way to access it, no references to it. Garbage collector will junk the data and free the memory. @@ -67,7 +67,7 @@ let admin = user; */!* ``` -![](memory-user-john-admin.png) +![](memory-user-john-admin.svg) Now if we do the same: ```js @@ -102,7 +102,7 @@ Function `marry` "marries" two objects by giving them references to each other a The resulting memory structure: -![](family.png) +![](family.svg) As of now, all objects are reachable. @@ -113,19 +113,19 @@ delete family.father; delete family.mother.husband; ``` -![](family-delete-refs.png) +![](family-delete-refs.svg) It's not enough to delete only one of these two references, because all objects would still be reachable. But if we delete both, then we can see that John has no incoming reference any more: -![](family-no-father.png) +![](family-no-father.svg) Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible. After garbage collection: -![](family-no-father-2.png) +![](family-no-father-2.svg) ## Unreachable island @@ -139,7 +139,7 @@ family = null; The in-memory picture becomes: -![](family-no-family.png) +![](family-no-family.svg) This example demonstrates how important the concept of reachability is. @@ -161,25 +161,25 @@ The following "garbage collection" steps are regularly performed: For instance, let our object structure look like this: -![](garbage-collection-1.png) +![](garbage-collection-1.svg) We can clearly see an "unreachable island" to the right side. Now let's see how "mark-and-sweep" garbage collector deals with it. The first step marks the roots: -![](garbage-collection-2.png) +![](garbage-collection-2.svg) Then their references are marked: -![](garbage-collection-3.png) +![](garbage-collection-3.svg) ...And their references, while possible: -![](garbage-collection-4.png) +![](garbage-collection-4.svg) Now the objects that could not be visited in the process are considered unreachable and will be removed: -![](garbage-collection-5.png) +![](garbage-collection-5.svg) That's the concept of how garbage collection works. @@ -207,6 +207,6 @@ A general book "The Garbage Collection Handbook: The Art of Automatic Memory Man If you are familiar with low-level programming, the more detailed information about V8 garbage collector is in the article [A tour of V8: Garbage Collection](http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection). -[V8 blog](http://v8project.blogspot.com/) also publishes articles about changes in memory management from time to time. Naturally, to learn the garbage collection, you'd better prepare by learning about V8 internals in general and read the blog of [Vyacheslav Egorov](http://mrale.ph) who worked as one of V8 engineers. I'm saying: "V8", because it is best covered with articles in the internet. For other engines, many approaches are similar, but garbage collection differs in many aspects. +[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. In-depth knowledge of engines is good when you need low-level optimizations. It would be wise to plan that as the next step after you're familiar with the language. diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png deleted file mode 100644 index 1447f15f..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg b/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg new file mode 100644 index 00000000..e30bf7e5 --- /dev/null +++ b/1-js/04-object-basics/02-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/02-garbage-collection/family-delete-refs@2x.png b/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png deleted file mode 100644 index 67d95ba0..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-delete-refs@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.png b/1-js/04-object-basics/02-garbage-collection/family-no-family.png deleted file mode 100644 index 95844adc..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-family.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg b/1-js/04-object-basics/02-garbage-collection/family-no-family.svg new file mode 100644 index 00000000..cb945855 --- /dev/null +++ b/1-js/04-object-basics/02-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-family@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png deleted file mode 100644 index 40cc0e74..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-family@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png b/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png deleted file mode 100644 index 63001b7a..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg b/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg new file mode 100644 index 00000000..37255e57 --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg @@ -0,0 +1 @@ +Objectfamilyname: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png deleted file mode 100644 index 44cfdddb..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father-2@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.png b/1-js/04-object-basics/02-garbage-collection/family-no-father.png deleted file mode 100644 index 1038b7e4..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg b/1-js/04-object-basics/02-garbage-collection/family-no-father.svg new file mode 100644 index 00000000..8e7f3025 --- /dev/null +++ b/1-js/04-object-basics/02-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/02-garbage-collection/family-no-father@2x.png b/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png deleted file mode 100644 index f2b13765..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family-no-father@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family.png b/1-js/04-object-basics/02-garbage-collection/family.png deleted file mode 100644 index b2fec706..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/family.svg b/1-js/04-object-basics/02-garbage-collection/family.svg new file mode 100644 index 00000000..37613260 --- /dev/null +++ b/1-js/04-object-basics/02-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/02-garbage-collection/family@2x.png b/1-js/04-object-basics/02-garbage-collection/family@2x.png deleted file mode 100644 index 646355c9..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/family@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png deleted file mode 100644 index 501798a3..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg new file mode 100644 index 00000000..50697c4b --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png deleted file mode 100644 index 91114279..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png deleted file mode 100644 index 4d1cb84a..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg new file mode 100644 index 00000000..dc086832 --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png deleted file mode 100644 index ca2db9ab..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png deleted file mode 100644 index e60b92c0..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg new file mode 100644 index 00000000..d5faadb3 --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png deleted file mode 100644 index c2e79d7b..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png deleted file mode 100644 index 79c935c0..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg new file mode 100644 index 00000000..37364776 --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg @@ -0,0 +1 @@ +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png deleted file mode 100644 index e3b04097..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png deleted file mode 100644 index 28625adc..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg new file mode 100644 index 00000000..49e9c62b --- /dev/null +++ b/1-js/04-object-basics/02-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/garbage-collection-5@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png deleted file mode 100644 index af39f0cd..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection.png deleted file mode 100644 index 013d0a48..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png b/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png deleted file mode 100644 index 681f29e4..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/garbage-collection@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png deleted file mode 100644 index b19f9953..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg new file mode 100644 index 00000000..9ad1d88b --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg @@ -0,0 +1 @@ +username: "John"Objectadmin<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png deleted file mode 100644 index ffe90716..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png deleted file mode 100644 index 1a143778..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg new file mode 100644 index 00000000..e75b8d46 --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg @@ -0,0 +1 @@ +name: "John"Objectuser: null<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png deleted file mode 100644 index 54a90a92..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john.png deleted file mode 100644 index 8ac34e21..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john.png and /dev/null differ diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg b/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg new file mode 100644 index 00000000..f051391d --- /dev/null +++ b/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg @@ -0,0 +1 @@ +username: "John"Object<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png b/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png deleted file mode 100644 index 5f0c8741..00000000 Binary files a/1-js/04-object-basics/02-garbage-collection/memory-user-john@2x.png and /dev/null differ diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/03-symbol/article.md index 8323d664..a17f85fe 100644 --- a/1-js/04-object-basics/03-symbol/article.md +++ b/1-js/04-object-basics/03-symbol/article.md @@ -3,11 +3,11 @@ By specification, object property keys may be either of string type, or of symbol type. Not numbers, not booleans, only strings or symbols, these two types. -Till now we've only seen strings. Now let's see the advantages that symbols can give us. +Till now we've been using only strings. Now let's see the benefits that symbols can give us. ## Symbols -"Symbol" value represents a unique identifier. +A "symbol" represents a unique identifier. A value of this type can be created using `Symbol()`: @@ -16,7 +16,7 @@ A value of this type can be created using `Symbol()`: let id = Symbol(); ``` -We can also give symbol a description (also called a symbol name), mostly useful for debugging purposes: +Upon creation, we can give symbol a description (also called a symbol name), mostly useful for debugging purposes: ```js run // id is a symbol with the description "id" @@ -50,9 +50,9 @@ 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 occasionally convert one into another. +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 call `.toString()` on it, like here: +If we really want to show a symbol, we need to explicitly call `.toString()` on it, like here: ```js run let id = Symbol("id"); *!* @@ -60,7 +60,7 @@ alert(id.toString()); // Symbol(id), now it works */!* ``` -Or get `symbol.description` property to get the description only: +Or get `symbol.description` property to show the description only: ```js run let id = Symbol("id"); *!* @@ -72,23 +72,29 @@ alert(id.description); // id ## "Hidden" properties -Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. +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 want to store an "identifier" for the object `user`, we can use a symbol as a key for it: +For instance, if we're working with `user` objects, that belong to a third-party code. We'd like to add identifiers to them. + +Let's use a symbol key for it: ```js run -let user = { name: "John" }; +let user = { // belongs to another code + name: "John" +}; + let id = Symbol("id"); -user[id] = "ID Value"; +user[id] = 1; + alert( user[id] ); // we can access the data using the symbol as the key ``` What's the benefit of using `Symbol("id")` over a string `"id"`? -Let's make the example a bit deeper to see that. +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. -Imagine that another script wants to have its own "id" property inside `user`, for its own purposes. That may be another JavaScript library, so the scripts are completely unaware of each other. +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. Then that script can create its own `Symbol("id")`, like this: @@ -99,25 +105,25 @@ let id = Symbol("id"); user[id] = "Their id value"; ``` -There will be no conflict, because symbols are always different, even if they have the same name. +There will be no conflict between our and their identifiers, because symbols are always different, even if they have the same name. -Now note that if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: +...But if we used a string `"id"` instead of a symbol for the same purpose, then there *would* be a conflict: ```js run let user = { name: "John" }; -// our script uses "id" property -user.id = "ID Value"; +// Our script uses "id" property +user.id = "Our id value"; -// ...if later another script the uses "id" for its purposes... +// ...Another script also wants "id" for its purposes... user.id = "Their id value" -// boom! overwritten! it did not mean to harm the colleague, but did it! +// Boom! overwritten by another script! ``` ### Symbols in a literal -If we want to use a symbol in an object literal, we need square brackets. +If we want to use a symbol in an object literal `{...}`, we need square brackets around it. Like this: @@ -127,7 +133,7 @@ let id = Symbol("id"); let user = { name: "John", *!* - [id]: 123 // not just "id: 123" + [id]: 123 // not "id: 123" */!* }; ``` @@ -155,7 +161,7 @@ for (let key in user) alert(key); // name, age (no symbols) alert( "Direct: " + user[id] ); ``` -That's a part of the general "hiding" concept. If another script or a library loops over our object, it won't unexpectedly access a symbolic property. +`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. In contrast, [Object.assign](mdn:js/Object/assign) copies both string and symbol properties: @@ -190,13 +196,11 @@ alert( obj[0] ); // test (same property) ## Global symbols -As we've seen, usually all symbols are different, even if they have the same names. But sometimes we want same-named symbols to be same entities. - -For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. +As we've seen, usually all symbols are different, even if they have the same name. But sometimes we want same-named symbols to be same entities. For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. To achieve that, there exists a *global symbol registry*. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol. -In order to create or read a symbol in the registry, use `Symbol.for(key)`. +In order to read (create if absent) a symbol from the registry, use `Symbol.for(key)`. That call checks the global registry, and if there's a symbol described as `key`, then returns it, otherwise creates a new symbol `Symbol(key)` and stores it in the registry by the given `key`. @@ -206,7 +210,7 @@ For instance: // read from the global registry let id = Symbol.for("id"); // if the symbol did not exist, it is created -// read it again +// read it again (maybe from another part of the code) let idAgain = Symbol.for("id"); // the same symbol @@ -228,22 +232,29 @@ For global symbols, not only `Symbol.for(key)` returns a symbol by name, but the For instance: ```js run +// get symbol by name let sym = Symbol.for("name"); let sym2 = Symbol.for("id"); -// get name from symbol +// get name by symbol alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id ``` The `Symbol.keyFor` internally uses the global symbol registry to look up the key for the symbol. So it doesn't work for non-global symbols. If the symbol is not global, it won't be able to find it and return `undefined`. +That said, any symbols have `description` property. + For instance: ```js run -alert( Symbol.keyFor(Symbol.for("name")) ); // name, global symbol +let globalSymbol = Symbol.for("name"); +let localSymbol = Symbol("name"); -alert( Symbol.keyFor(Symbol("name2")) ); // undefined, the argument isn't a global symbol +alert( Symbol.keyFor(globalSymbol) ); // name, global symbol +alert( Symbol.keyFor(localSymbol) ); // undefined, not global + +alert( localSymbol.description ); // name ``` ## System symbols @@ -266,17 +277,17 @@ Other symbols will also become familiar when we study the corresponding language `Symbol` is a primitive type for unique identifiers. -Symbols are created with `Symbol()` call with an optional description. +Symbols are created with `Symbol()` call with an optional description (name). -Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` return exactly the same symbol. +Symbols are always different values, even if they have the same name. If we want same-named symbols to be equal, then we should use the global registry: `Symbol.for(key)` returns (creates if needed) a global symbol with `key` as the name. Multiple calls of `Symbol.for` with the same `key` return exactly the same symbol. Symbols have two main use cases: 1. "Hidden" object properties. - If we want to add a property into an object that "belongs" to another script or a library, we can create a symbol and use it as a property key. A symbolic property does not appear in `for..in`, so it won't be occasionally listed. Also it won't be accessed directly, because another script does not have our symbol, so it will not occasionally intervene into its actions. + 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 methods and syntax constructs adhere to a common agreement that they are. And the one who explicitly calls the aforementioned methods probably understands well what he's doing. +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. diff --git a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md b/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md index e2e87de7..5589b655 100644 --- a/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md +++ b/1-js/04-object-basics/04-object-methods/2-check-syntax/solution.md @@ -15,13 +15,13 @@ The error message in most browsers does not give understanding what went wrong. **The error appears because a semicolon is missing after `user = {...}`.** -JavaScript does not assume a semicolon before a bracket `(user.go)()`, so it reads the code like: +JavaScript does not auto-insert a semicolon before a bracket `(user.go)()`, so it reads the code like: ```js no-beautify let user = { go:... }(user.go)() ``` -Then we can also see that such a joint expression is syntactically a call of the object `{ go: ... }` as a function with the argument `(user.go)`. And that also happens on the same line with `let user`, so the `user` object has not yet even been defined, hence the error. +Then we can also see that such a joint expression is syntactically a call of the object `{ go: ... }` as a function with the argument `(user.go)`. And that also happens on the same line with `let user`, so the `user` object has not yet even been defined, hence the error. If we insert the semicolon, all is fine: @@ -35,9 +35,3 @@ let user = { ``` Please note that brackets around `(user.go)` do nothing here. Usually they setup the order of operations, but here the dot `.` works first anyway, so there's no effect. Only the semicolon thing matters. - - - - - - diff --git a/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md b/1-js/04-object-basics/04-object-methods/4-object-property-this/solution.md index f5773ec2..ea00c970 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 @@ -14,11 +14,11 @@ let user = makeUser(); alert( user.ref.name ); // Error: Cannot read property 'name' of undefined ``` -That's because rules that set `this` do not look at object literals. +That's because rules that set `this` do not look at object definition. Only the moment of call matters. -Here the value of `this` inside `makeUser()` is `undefined`, because it is called as a function, not as a method. +Here the value of `this` inside `makeUser()` is `undefined`, because it is called as a function, not as a method with "dot" syntax. -And the object literal itself has no effect on `this`. The value of `this` is one for the whole function, code blocks and object literals do not affect it. +The value of `this` is one for the whole function, code blocks and object literals do not affect it. So `ref: this` actually takes current `this` of the function. @@ -42,5 +42,3 @@ alert( user.ref().name ); // John ``` Now it works, because `user.ref()` is a method. And the value of `this` is set to the object before dot `.`. - - diff --git a/1-js/04-object-basics/04-object-methods/article.md b/1-js/04-object-basics/04-object-methods/article.md index 5ef5b24e..3d83a224 100644 --- a/1-js/04-object-basics/04-object-methods/article.md +++ b/1-js/04-object-basics/04-object-methods/article.md @@ -15,7 +15,7 @@ Actions are represented in JavaScript by functions in properties. ## Method examples -For the start, let's teach the `user` to say hello: +For a start, let's teach the `user` to say hello: ```js run let user = { @@ -63,7 +63,7 @@ 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". -OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E.Gamma, R.Helm, R.Johnson, J.Vissides or "Object-Oriented Analysis and Design with Applications" by G.Booch, and more. We'll scratch the surface of that topic later in the chapter . +OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E.Gamma, R.Helm, R.Johnson, J.Vissides or "Object-Oriented Analysis and Design with Applications" by G.Booch, and more. ``` ### Method shorthand @@ -72,14 +72,14 @@ There exists a shorter syntax for methods in an object literal: ```js // these objects do the same -let user = { +user = { sayHi: function() { alert("Hello"); } }; // method shorthand looks better, right? -let user = { +user = { *!* sayHi() { // same as "sayHi: function()" */!* @@ -111,6 +111,7 @@ let user = { sayHi() { *!* + // "this" is the "current object" alert(this.name); */!* } @@ -166,7 +167,7 @@ If we used `this.name` instead of `user.name` inside the `alert`, then the code ## "this" is not bound -In JavaScript, "this" keyword behaves unlike most other programming languages. First, it can be used in any function. +In JavaScript, "this" keyword behaves unlike most other programming languages. It can be used in any function. There's no syntax error in the code like that: @@ -176,9 +177,9 @@ function sayHi() { } ``` -The value of `this` is evaluated during the run-time. And it can be anything. +The value of `this` is evaluated during the run-time, depending on the context. -For instance, the same function may have different "this" when called from different objects: +For instance, here the same function is assigned to two different objects and has different "this" in the calls: ```js run let user = { name: "John" }; @@ -189,7 +190,7 @@ function sayHi() { } *!* -// use the same functions in two objects +// use the same function in two objects user.f = sayHi; admin.f = sayHi; */!* @@ -202,7 +203,10 @@ admin.f(); // Admin (this == admin) admin['f'](); // Admin (dot or square brackets access the method – doesn't matter) ``` -Actually, we can call the function without an object at all: +The rule is simple: if `obj.f()` is called, then `this` is `obj` during the call of `f`. So it's either `user` or `admin` in the example above. + +````smart header="Calling without an object: `this == undefined`" +We can even call the function without an object at all: ```js run function sayHi() { @@ -216,7 +220,8 @@ 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. -Please note that usually a call of a function that uses `this` without an object is not normal, but rather a programming mistake. If a function has `this`, then it is usually meant to be called in the context of an object. +Usually such call is an 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. @@ -253,11 +258,11 @@ user.hi(); // John (the simple call works) */!* ``` -On the last line there is a ternary operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`. +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`. -The method is immediately called with parentheses `()`. But it doesn't work right! +Then the method is immediately called with parentheses `()`. But it doesn't work correctly! -You can see that the call results in an error, because the value of `"this"` inside the call becomes `undefined`. +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 @@ -302,7 +307,7 @@ The Reference Type is a "specification type". We can't explicitly use it, but it The value of Reference Type is a three-value combination `(base, name, strict)`, where: - `base` is the object. -- `name` is the property. +- `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: @@ -314,6 +319,8 @@ The result of a property access `user.hi` is not a function, but a value of Refe 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). @@ -347,7 +354,7 @@ That's a special feature of arrow functions, it's useful when we actually do not The value of `this` is defined at run-time. - When a function is declared, it may use `this`, but that `this` has no value until the function is called. -- That function can be copied between objects. +- A function can be copied between objects. - When a function is called in the "method" syntax: `object.method()`, the value of `this` during the call is `object`. Please note that arrow functions are special: they have no `this`. When `this` is accessed inside an arrow function, it is taken from outside. diff --git a/1-js/04-object-basics/05-object-toprimitive/article.md b/1-js/04-object-basics/05-object-toprimitive/article.md index a44cf4f4..ca449e5f 100644 --- a/1-js/04-object-basics/05-object-toprimitive/article.md +++ b/1-js/04-object-basics/05-object-toprimitive/article.md @@ -3,28 +3,22 @@ What happens when objects are added `obj1 + obj2`, subtracted `obj1 - obj2` or printed using `alert(obj)`? -There are special methods in objects that do the conversion. +In 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 close it. +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. -For objects, there's no to-boolean conversion, because all objects are `true` in a boolean context. So there are only string and numeric conversions. - -The numeric conversion happens when we subtract objects or apply mathematical functions. For instance, `Date` objects (to be covered in the chapter ) can be subtracted, and the result of `date1 - date2` is the time difference between two dates. - -As for the string conversion -- it usually happens when we output an object like `alert(obj)` and in similar contexts. +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 -When an object is used in the context where a primitive is required, for instance, in an `alert` or mathematical operations, it's converted to a primitive value using the `ToPrimitive` algorithm ([specification](https://tc39.github.io/ecma262/#sec-toprimitive)). +We can fine-tune string and numeric conversion, using special object methods. -That algorithm allows us to customize the conversion using a special object method. - -Depending on the context, the conversion has a so-called "hint". - -There are three variants: +There are three variants of type conversion, so-called "hints", described in the [specification](https://tc39.github.io/ecma262/#sec-toprimitive): `"string"` -: When an operation expects a string, for object-to-string conversions, like `alert`: +: For an object-to-string conversion, when we're doing an operation on an object that expects a string, like `alert`: ```js // output @@ -35,7 +29,7 @@ There are three variants: ``` `"number"` -: When an operation expects a number, for object-to-number conversions, like maths: +: For an object-to-number conversion, like when we're doing maths: ```js // explicit conversion @@ -52,7 +46,7 @@ There are three variants: `"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. + 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 @@ -70,7 +64,7 @@ Please note -- there are only three hints. It's that simple. There is no "boolea **To do the conversion, JavaScript tries to find and call three object methods:** -1. Call `obj[Symbol.toPrimitive](hint)` if the method exists, +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"` @@ -82,9 +76,9 @@ Let's start from the first method. There's a built-in symbol named `Symbol.toPri ```js obj[Symbol.toPrimitive] = function(hint) { - // return a primitive value + // must return a primitive value // hint = one of "string", "number", "default" -} +}; ``` For instance, here `user` object implements it: @@ -142,7 +136,9 @@ alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500 ``` -Often we want a single "catch-all" place to handle all primitive conversions. In this case we can implement `toString` only, like this: +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 = { @@ -159,34 +155,40 @@ alert(user + 500); // toString -> John500 In the absence of `Symbol.toPrimitive` and `valueOf`, `toString` will handle all primitive conversions. - -## ToPrimitive and ToString/ToNumber +## 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.** +The only mandatory thing: these methods must return a primitive, not an object. -An operation that initiated the conversion gets that primitive, and then continues to work with it, applying further conversions if necessary. +```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) perform `ToNumber` conversion: +- Mathematical operations, except binary plus, convert the primitive to a number: ```js run let obj = { - toString() { // toString handles all conversions in the absence of other methods + // toString handles all conversions in the absence of other methods + toString() { return "2"; } }; - alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2 + alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number ``` -- Binary plus checks the primitive -- if it's a string, then it does concatenation, otherwise it performs `ToNumber` and works with numbers. - - String example: +- Binary plus will concatenate strings in the same situation: ```js run let obj = { toString() { @@ -194,32 +196,15 @@ For instance: } }; - alert(obj + 2); // 22 (ToPrimitive returned string => concatenation) + alert(obj + 2); // 22 (conversion to primitive returned a string => concatenation) ``` - Number example: - ```js run - let obj = { - toString() { - return true; - } - }; - - alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber) - ``` - -```smart header="Historical notes" -For historical reasons, methods `toString` or `valueOf` *should* return a primitive: if any of them returns an object, then there's no error, but that object is ignored (like if the method didn't exist). - -In contrast, `Symbol.toPrimitive` *must* return a primitive, otherwise, there will be an error. -``` - ## Summary The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value. There are 3 types (hints) of it: -- `"string"` (for `alert` and other string conversions) +- `"string"` (for `alert` and other operations that need a string) - `"number"` (for maths) - `"default"` (few operators) diff --git a/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md b/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md index 3362b5b4..c2c44881 100644 --- a/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md +++ b/1-js/04-object-basics/06-constructor-new/3-accumulator/task.md @@ -17,8 +17,10 @@ Here's the demo of the code: ```js let accumulator = new Accumulator(1); // initial value 1 + accumulator.read(); // adds the user-entered value accumulator.read(); // adds the user-entered value + alert(accumulator.value); // shows the sum of these values ``` diff --git a/1-js/04-object-basics/06-constructor-new/article.md b/1-js/04-object-basics/06-constructor-new/article.md index eb452f2f..c4ccca66 100644 --- a/1-js/04-object-basics/06-constructor-new/article.md +++ b/1-js/04-object-basics/06-constructor-new/article.md @@ -27,7 +27,7 @@ alert(user.name); // Jack alert(user.isAdmin); // false ``` -When a function is executed as `new User(...)`, it does the following steps: +When a function is executed with `new`, it does the following steps: 1. A new empty object is created and assigned to `this`. 2. The function body executes. Usually it modifies `this`, adds new properties to it. @@ -51,7 +51,7 @@ function User(name) { } ``` -So the result of `new User("Jack")` is the same object as: +So `let user = new User("Jack")` gives the same result as: ```js let user = { @@ -83,7 +83,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. ```` -## Dual-syntax constructors: new.target +## Constructor mode test: new.target ```smart header="Advanced stuff" The syntax from this section is rarely used, skip it unless you want to know everything. @@ -109,7 +109,9 @@ new User(); // function User { ... } */!* ``` -That can be used to allow both `new` and regular calls to work the same. That is, create the same object: +That can be used inside the function to know whether it was called with `new`, "in constructor mode", or without it, "in regular mode". + +We can also make both `new` and regular calls to do the same, like this: ```js run function User(name) { @@ -146,10 +148,10 @@ function BigUser() { this.name = "John"; - return { name: "Godzilla" }; // <-- returns an object + return { name: "Godzilla" }; // <-- returns this object } -alert( new BigUser().name ); // Godzilla, got that object ^^ +alert( new BigUser().name ); // Godzilla, got that object ``` And here's an example with an empty `return` (or we could place a primitive after it, doesn't matter): @@ -159,10 +161,7 @@ function SmallUser() { this.name = "John"; - return; // finishes the execution, returns this - - // ... - + return; // <-- returns this } alert( new SmallUser().name ); // John @@ -213,6 +212,8 @@ john = { */ ``` +To create complex objects, there's a more advanced syntax, [classes](info:classes), that we'll cover later. + ## Summary - Constructor functions or, briefly, constructors, are regular functions, but there's a common agreement to name them with capital letter first. diff --git a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md index a169f776..fd22a465 100644 --- a/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md +++ b/1-js/05-data-types/01-primitives-methods/1-string-new-property/solution.md @@ -6,26 +6,19 @@ let str = "Hello"; str.test = 5; // (*) -alert(str.test); +alert(str.test); ``` -There may be two kinds of result: -1. `undefined` -2. An error. +Depending on whether you have `use strict` or not, the result may be: +1. `undefined` (no strict mode) +2. An error (strict mode). Why? Let's replay what's happening at line `(*)`: 1. When a property of `str` is accessed, a "wrapper object" is created. -2. The operation with the property is carried out on it. So, the object gets the `test` property. -3. The operation finishes and the "wrapper object" disappears. - -So, on the last line, `str` has no trace of the property. A new wrapper object for every object operation on a string. - -Some browsers though may decide to further limit the programmer and disallow to assign properties to primitives at all. That's why in practice we can also see errors at line `(*)`. It's a little bit farther from the specification though. +2. In strict mode, writing into it is an error. +3. Otherwise, the operation with the property is carried on, the object gets the `test` property, but after that the "wrapper object" disappears, so in the last line `str` has no trace of the property. **This example clearly shows that primitives are not objects.** -They just can not store data. - -All property/method operations are performed with the help of temporary objects. - +They can't store additional data. diff --git a/1-js/05-data-types/01-primitives-methods/article.md b/1-js/05-data-types/01-primitives-methods/article.md index a40fdf55..b358b27e 100644 --- a/1-js/05-data-types/01-primitives-methods/article.md +++ b/1-js/05-data-types/01-primitives-methods/article.md @@ -1,8 +1,6 @@ # Methods of primitives -JavaScript allows us to work with primitives (strings, numbers, etc.) as if they were objects. - -They also provide methods to call as such. We will study those soon, but first we'll see how it works because, of course, primitives are not objects (and here we will make it even clearer). +JavaScript allows us to work with primitives (strings, numbers, etc.) as if they were objects. They also provide methods to call as such. We will study those soon, but first we'll see how it works because, of course, primitives are not objects (and here we will make it even clearer). Let's look at the key distinctions between primitives and objects. @@ -14,7 +12,7 @@ A primitive An object - Is capable of storing multiple values as properties. -- Can be created with `{}`, for instance: `{name: "John", age: 30}`. There are other kinds of objects in JavaScript; functions, for example, are objects. +- Can be created with `{}`, for instance: `{name: "John", age: 30}`. There are other kinds of objects in JavaScript: functions, for example, are objects. One of the best things about objects is that we can store a function as one of its properties. @@ -35,7 +33,7 @@ Many built-in objects already exist, such as those that work with dates, errors, But, these features come with a cost! -Objects are "heavier" than primitives. They require additional resources to support the internal machinery. But as properties and methods are very useful in programming, JavaScript engines try to optimize them to reduce the additional burden. +Objects are "heavier" than primitives. They require additional resources to support the internal machinery. ## A primitive as an object @@ -48,7 +46,7 @@ The solution looks a little bit awkward, but here it is: 1. Primitives are still primitive. A single value, as desired. 2. The language allows access to methods and properties of strings, numbers, booleans and symbols. -3. When this happens, a special "object wrapper" is created that provides the extra functionality, and then is destroyed. +3. In order for that to work, a special "object wrapper" that provides the extra functionality is created, and then is destroyed. The "object wrappers" are different for each primitive type and are called: `String`, `Number`, `Boolean` and `Symbol`. Thus, they provide different sets of methods. @@ -84,25 +82,25 @@ We'll see more specific methods in chapters and . ````warn header="Constructors `String/Number/Boolean` are for internal use only" -Some languages like Java allow us to create "wrapper objects" for primitives explicitly using a syntax like `new Number(1)` or `new Boolean(false)`. +Some languages like Java allow us to explicitly create "wrapper objects" for primitives using a syntax like `new Number(1)` or `new Boolean(false)`. In JavaScript, that's also possible for historical reasons, but highly **unrecommended**. Things will go crazy in several places. For instance: ```js run -alert( typeof 1 ); // "number" +alert( typeof 0 ); // "number" -alert( typeof new Number(1) ); // "object"! +alert( typeof new Number(0) ); // "object"! ``` -And because what follows, `zero`, is an object, the alert will show up: +Objects are always truthy in `if`, so here the alert will show up: ```js run let zero = new Number(0); if (zero) { // zero is true, because it's an object - alert( "zero is truthy?!?" ); + alert( "zero is truthy!?!" ); } ``` diff --git a/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js index 219fa806..6bd0123d 100644 --- a/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js +++ b/1-js/05-data-types/02-number/3-repeat-until-number/_js.view/test.js @@ -18,7 +18,7 @@ describe("readNumber", function() { assert.strictEqual(readNumber(), 0); }); - it("continues the loop unti meets a number", function() { + it("continues the loop until meets a number", function() { prompt.onCall(0).returns("not a number"); prompt.onCall(1).returns("not a number again"); prompt.onCall(2).returns("1"); @@ -35,4 +35,4 @@ describe("readNumber", function() { assert.isNull(readNumber()); }); -}); \ No newline at end of file +}); diff --git a/1-js/05-data-types/02-number/9-random-int-min-max/task.md b/1-js/05-data-types/02-number/9-random-int-min-max/task.md index 29341b2a..4ac7b5fb 100644 --- a/1-js/05-data-types/02-number/9-random-int-min-max/task.md +++ b/1-js/05-data-types/02-number/9-random-int-min-max/task.md @@ -12,9 +12,9 @@ Any number from the interval `min..max` must appear with the same probability. Examples of its work: ```js -alert( random(1, 5) ); // 1 -alert( random(1, 5) ); // 3 -alert( random(1, 5) ); // 5 +alert( randomInteger(1, 5) ); // 1 +alert( randomInteger(1, 5) ); // 3 +alert( randomInteger(1, 5) ); // 5 ``` You can use the solution of the [previous task](info:task/random-min-max) as the base. diff --git a/1-js/05-data-types/02-number/article.md b/1-js/05-data-types/02-number/article.md index 7ca012c2..634baf15 100644 --- a/1-js/05-data-types/02-number/article.md +++ b/1-js/05-data-types/02-number/article.md @@ -1,8 +1,8 @@ # Numbers -All numbers in JavaScript are stored in 64-bit format [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), also known as "double precision". +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". -Let's recap and expand upon what we currently know about them. +Let's expand upon what we currently know about them. ## More ways to write a number @@ -12,7 +12,7 @@ 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. +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. In JavaScript, we shorten a number by appending the letter `"e"` to the number and specifying the zeroes count: @@ -177,7 +177,7 @@ There are two ways to do so: ## Imprecise calculations -Internally, a number is represented in 64-bit format [IEEE-754](http://en.wikipedia.org/wiki/IEEE_754-1985), so there are exactly 64 bits to store a number: 52 of them are used to store the digits, 11 of them store the position of the decimal point (they are zero for integer numbers), and 1 bit is for the sign. +Internally, a number is represented in 64-bit format [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754-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. If a number is too big, it would overflow the 64-bit storage, potentially giving an infinity: @@ -205,7 +205,7 @@ Ouch! There are more consequences than an incorrect comparison here. Imagine you But why does this happen? -A number is stored in memory in its binary form, a sequence of ones and zeroes. But fractions like `0.1`, `0.2` that look simple in the decimal numeric system are actually unending fractions in their binary form. +A number is stored in memory in its binary form, a sequence of bits - ones and zeroes. But fractions like `0.1`, `0.2` that look simple in the decimal numeric system are actually unending fractions in their binary form. In other words, what is `0.1`? It is one divided by ten `1/10`, one-tenth. In decimal numeral system such numbers are easily representable. Compare it to one-third: `1/3`. It becomes an endless fraction `0.33333(3)`. @@ -213,7 +213,7 @@ So, division by powers `10` is guaranteed to work well in the decimal system, bu There's just no way to store *exactly 0.1* or *exactly 0.2* using the binary system, just like there is no way to store one-third as a decimal fraction. -The numeric format IEEE-754 solves this by rounding to the nearest possible number. These rounding rules normally don't allow us to see that "tiny precision loss", so the number shows up as `0.3`. But beware, the loss still exists. +The numeric format IEEE-754 solves this by rounding to the nearest possible number. These rounding rules normally don't allow us to see that "tiny precision loss", but it exists. We can see this in action: ```js run @@ -326,7 +326,7 @@ Please note that an empty or a space-only string is treated as `0` in all numeri 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: 1. It works with `NaN`: `Object.is(NaN, NaN) === true`, that's a good thing. -2. Values `0` and `-0` are different: `Object.is(0, -0) === false`, it rarely matters, but these values technically are different. +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. In all other cases, `Object.is(a, b)` is the same as `a === b`. 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 4809cf12..f7a332d0 100644 --- a/1-js/05-data-types/03-string/1-ucfirst/solution.md +++ b/1-js/05-data-types/03-string/1-ucfirst/solution.md @@ -6,7 +6,7 @@ But we can make a new string based on the existing one, with the uppercased firs let newStr = str[0].toUpperCase() + str.slice(1); ``` -There's a small problem though. If `str` is empty, then `str[0]` is undefined, so we'll get an error. +There's a small problem though. If `str` is empty, then `str[0]` is `undefined`, and as `undefined` doesn't have the `toUpperCase()` method, we'll get an error. There are two variants here: @@ -15,7 +15,7 @@ There are two variants here: Here's the 2nd variant: -```js run +```js run demo function ucFirst(str) { if (!str) return str; diff --git a/1-js/05-data-types/03-string/1-ucfirst/task.md b/1-js/05-data-types/03-string/1-ucfirst/task.md index c0e6ecac..ed8a1e6a 100644 --- a/1-js/05-data-types/03-string/1-ucfirst/task.md +++ b/1-js/05-data-types/03-string/1-ucfirst/task.md @@ -2,7 +2,7 @@ importance: 5 --- -# Uppercast the first character +# Uppercase the first character Write a function `ucFirst(str)` that returns the string `str` with the uppercased first character, for instance: diff --git a/1-js/05-data-types/03-string/2-check-spam/solution.md b/1-js/05-data-types/03-string/2-check-spam/solution.md index 893c2649..de8dde57 100644 --- a/1-js/05-data-types/03-string/2-check-spam/solution.md +++ b/1-js/05-data-types/03-string/2-check-spam/solution.md @@ -1,6 +1,6 @@ To make the search case-insensitive, let's bring the string to lower case and then search: -```js run +```js run demo function checkSpam(str) { let lowerStr = str.toLowerCase(); diff --git a/1-js/05-data-types/03-string/2-check-spam/task.md b/1-js/05-data-types/03-string/2-check-spam/task.md index d073adc0..98b5dd8a 100644 --- a/1-js/05-data-types/03-string/2-check-spam/task.md +++ b/1-js/05-data-types/03-string/2-check-spam/task.md @@ -4,7 +4,7 @@ importance: 5 # Check for spam -Write a function `checkSpam(str)` that returns `true` if `str` contains 'viagra' or 'XXX', otherwise `false. +Write a function `checkSpam(str)` that returns `true` if `str` contains 'viagra' or 'XXX', otherwise `false`. The function must be case-insensitive: diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md index b8a8ac73..3bde8bd5 100644 --- a/1-js/05-data-types/03-string/article.md +++ b/1-js/05-data-types/03-string/article.md @@ -17,7 +17,7 @@ let double = "double-quoted"; let backticks = `backticks`; ``` -Single and double quotes are essentially the same. Backticks, however, allow us to embed any expression into the string, including function calls: +Single and double quotes are essentially the same. Backticks, however, allow us to embed any expression into the string, by wrapping it in `${…}`: ```js run function sum(a, b) { @@ -39,9 +39,12 @@ let guestList = `Guests: alert(guestList); // a list of guests, multiple lines ``` -If we try to use single or double quotes in the same way, there will be an error: +Looks natural, right? But single or double quotes do not work this way. + +If we use them and try to use multiple lines, there'll be an error: + ```js run -let guestList = "Guests: // Error: Unexpected token ILLEGAL +let guestList = "Guests: // Error: Unexpected token ILLEGAL * John"; ``` @@ -49,10 +52,9 @@ Single and double quotes come from ancient times of language creation when the n 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. - ## Special characters -It is still possible to create multiline strings with single quotes by using a so-called "newline character", written as `\n`, which denotes a line break: +It is still possible to create multiline strings with single and double quotes by using a so-called "newline character", written as `\n`, which denotes a line break: ```js run let guestList = "Guests:\n * John\n * Pete\n * Mary"; @@ -60,39 +62,45 @@ let guestList = "Guests:\n * John\n * Pete\n * Mary"; alert(guestList); // a multiline list of guests ``` -For example, these two lines describe the same: +For example, these two lines are equal, just written differently: ```js run -alert( "Hello\nWorld" ); // two lines using a "newline symbol" +let str1 = "Hello\nWorld"; // two lines using a "newline symbol" // two lines using a normal newline and backticks -alert( `Hello -World` ); +let str2 = `Hello +World`; + +alert(str1 == str2); // true ``` -There are other, less common "special" characters as well. Here's the list: +There are other, less common "special" characters. + +Here's the full list: | Character | Description | |-----------|-------------| -|`\b`|Backspace| -|`\f`|Form feed| |`\n`|New line| -|`\r`|Carriage return| +|`\r`|Carriage return: not used alone. Windows text files use a combination of two characters `\r\n` to represent a line break. | +|`\'`, `\"`|Quotes| +|`\\`|Backslash| |`\t`|Tab| -|`\uNNNN`|A unicode symbol with the hex code `NNNN`, for instance `\u00A9` -- is a unicode for the copyright symbol `©`. It must be exactly 4 hex digits. | -|`\u{NNNNNNNN}`|Some rare characters are encoded with two unicode symbols, taking up to 4 bytes. This long unicode requires braces around it.| +|`\b`, `\f`, `\v`| Backspace, Form Feed, Vertical Tab -- 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. | Examples with unicode: ```js run alert( "\u00A9" ); // © -alert( "\u{20331}" ); // 佫, a rare chinese hieroglyph (long unicode) +alert( "\u{20331}" ); // 佫, a rare Chinese hieroglyph (long unicode) alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long unicode) ``` All special characters start with a backslash character `\`. It is also called an "escape character". -We would also use it if we want to insert a quote into the string. +We might also use it if we wanted to insert a quote into the string. For instance: @@ -102,7 +110,7 @@ alert( 'I*!*\'*/!*m the Walrus!' ); // *!*I'm*/!* the Walrus! As you can see, we have to prepend the inner quote by the backslash `\'`, because otherwise it would indicate the string end. -Of course, that refers only to the quotes that are same as the enclosing ones. So, as a more elegant solution, we could switch to double quotes or backticks instead: +Of course, only 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: ```js run alert( `I'm the Walrus!` ); // I'm the Walrus! @@ -120,7 +128,6 @@ alert( `The backslash: \\` ); // The backslash: \ ## String length - The `length` property has the string length: ```js run @@ -189,7 +196,7 @@ For instance: ```js run let str = 'Hi'; -str = 'h' + str[1]; // replace the string +str = 'h' + str[1]; // replace the string alert( str ); // hi ``` @@ -242,10 +249,8 @@ let str = 'Widget with id'; alert( str.indexOf('id', 2) ) // 12 ``` - If we're interested in all occurrences, we can run `indexOf` in a loop. Every new call is made with the position after the previous match: - ```js run let str = 'As sly as a fox, as strong as an ox'; @@ -305,10 +310,11 @@ if (str.indexOf("Widget") != -1) { } ``` -````smart header="The bitwise NOT trick" +#### The bitwise NOT trick + One of the old tricks used here is the [bitwise NOT](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Bitwise_NOT) `~` operator. It converts the number to a 32-bit integer (removes the decimal part if exists) and then reverses all bits in its binary representation. -For 32-bit integers the call `~n` means exactly the same as `-(n+1)` (due to IEEE-754 format). +In practice, that means a simple thing: for 32-bit integers `~n` equals `-(n+1)`. For instance: @@ -321,9 +327,9 @@ alert( ~-1 ); // 0, the same as -(-1+1) */!* ``` -As we can see, `~n` is zero only if `n == -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 that the result of `indexOf` is not `-1`. In other words, when there is a match. +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: @@ -338,7 +344,10 @@ if (~str.indexOf("Widget")) { 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 @@ -355,15 +364,15 @@ alert( "Hello".includes("Bye") ); // false The optional second argument of `str.includes` is the position to start searching from: ```js run -alert( "Midget".includes("id") ); // true -alert( "Midget".includes("id", 3) ); // false, from position 3 there is no "id" +alert( "Widget".includes("id") ); // true +alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id" ``` The methods [str.startsWith](mdn:js/String/startsWith) and [str.endsWith](mdn:js/String/endsWith) do exactly what they say: ```js run alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid" -alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get" +alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get" ``` ## Getting a substring @@ -397,7 +406,6 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and alert( str.slice(-4, -1) ); // gif ``` - `str.substring(start [, end])` : Returns the part of the string *between* `start` and `end`. @@ -405,7 +413,6 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and For instance: - ```js run let str = "st*!*ring*/!*ify"; @@ -421,7 +428,6 @@ There are 3 methods in JavaScript to get a substring: `substring`, `substr` and Negative arguments are (unlike slice) not supported, they are treated as `0`. - `str.substr(start [, length])` : Returns the part of the string from `start`, with the given `length`. @@ -447,11 +453,10 @@ Let's recap these methods to avoid any confusion: | `substring(start, end)` | between `start` and `end` | negative values mean `0` | | `substr(start, length)` | from `start` get `length` characters | allows negative `start` | - ```smart header="Which one to choose?" All of them can do the job. Formally, `substr` has a minor drawback: it is described not in the core JavaScript specification, but in Annex B, which covers browser-only features that exist mainly for historical reasons. So, non-browser environments may fail to support it. But in practice it works everywhere. -The author finds themself using `slice` almost all the time. +Of the other two variants, `slice` is a little bit more flexible, it allows negative arguments and shorter to write. So, it's enough to remember solely `slice` of these three methods. ``` ## Comparing strings @@ -514,7 +519,7 @@ alert( str ); // ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ ``` -See? Capital characters go first, then a few special ones, then lowercase characters. +See? Capital characters go first, then a few special ones, then lowercase characters, and `Ö` near the end of the output. Now it becomes obvious why `a > Z`. @@ -523,10 +528,9 @@ The characters are compared by their numeric code. The greater code means that t - 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`. - ### Correct comparisons -The "right" algorithm to do string comparisons is more complex than it may seem, because alphabets are different for different languages. The same-looking letter may be located differently in different alphabets. +The "right" algorithm to do string comparisons is more complex than it may seem, because alphabets are different for different languages. So, the browser needs to know the language to compare. @@ -534,11 +538,11 @@ Luckily, all modern browsers (IE10- requires the additional library [Intl.JS](ht It provides a special method to compare strings in different languages, following their rules. -The call [str.localeCompare(str2)](mdn:js/String/localeCompare): +The call [str.localeCompare(str2)](mdn:js/String/localeCompare) returns an integer indicating whether `str` is less, equal or greater than `str2` according to the language rules: -- Returns `1` if `str` is greater than `str2` according to the language rules. -- Returns `-1` if `str` is less than `str2`. -- Returns `0` if they are equal. +- Returns a negative number if `str` is less than `str2`. +- Returns a positive number if `str` is greater than `str2`. +- Returns `0` if they are equivalent. For instance: @@ -546,19 +550,19 @@ For instance: alert( 'Österreich'.localeCompare('Zealand') ); // -1 ``` -This method actually has two additional arguments specified in [the documentation](mdn:js/String/localeCompare), which allows it to specify the language (by default taken from the environment) and setup additional rules like case sensitivity or should `"a"` and `"á"` be treated as the same etc. +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 of hieroglyphs characters or other rare symbols. +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 -Most symbols have a 2-byte code. Letters in most european languages, numbers, and even most hieroglyphs, have a 2-byte representation. +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". @@ -567,7 +571,7 @@ 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 +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! @@ -576,7 +580,7 @@ We actually have a single symbol in each of the strings above, but the `length` `String.fromCodePoint` and `str.codePointAt` are few rare methods that deal with surrogate pairs right. They recently appeared in the language. Before them, there were only [String.fromCharCode](mdn:js/String/fromCharCode) and [str.charCodeAt](mdn:js/String/charCodeAt). These methods are actually the same as `fromCodePoint/codePointAt`, but don't work with surrogate pairs. -But, for instance, getting a symbol can be tricky, because surrogate pairs are treated as two characters: +Getting a symbol can be tricky, because surrogate pairs are treated as two characters: ```js run alert( '𝒳'[0] ); // strange symbols... @@ -604,7 +608,7 @@ In many languages there are symbols that are composed of the base character with For instance, the letter `a` can be the base character for: `àáâäãåā`. Most common "composite" character have their own code in the UTF-16 table. But not all of them, because there are too many possible combinations. -To support arbitrary compositions, UTF-16 allows us to use several unicode characters. The base character and one or many "mark" characters that "decorate" it. +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 Ṡ. @@ -627,10 +631,12 @@ This provides great flexibility, but also an interesting problem: two characters For instance: ```js run -alert( 'S\u0307\u0323' ); // Ṩ, S + dot above + dot below -alert( 'S\u0323\u0307' ); // Ṩ, S + dot below + dot above +let s1 = 'S\u0307\u0323'; // Ṩ, S + dot above + dot below +let s2 = 'S\u0323\u0307'; // Ṩ, S + dot below + dot above -alert( 'S\u0307\u0323' == 'S\u0323\u0307' ); // false +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. @@ -649,14 +655,13 @@ 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. +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. +- There are 3 types of quotes. Backticks allow a string to span multiple lines and embed expressions `${…}`. - Strings in JavaScript are encoded using UTF-16. - We can use special characters like `\n` and insert letters by their unicode using `\u...`. - To get a character, use: `[]`. @@ -669,6 +674,6 @@ There are several other helpful methods in strings: - `str.trim()` -- removes ("trims") spaces from the beginning and end of the string. - `str.repeat(n)` -- repeats the string `n` times. -- ...and more. See the [manual](mdn:js/String) for details. +- ...and more to be found in the [manual](mdn:js/String). -Strings also have methods for doing search/replace with regular expressions. But that topic deserves a separate chapter, so we'll return to that later. +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 . 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 edf39289..daadf494 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 @@ -29,8 +29,8 @@ For instance, for `[-1, 2, 3, -9, 11]`: -9 -9 + 11 -// Starting from -11 --11 +// Starting from 11 +11 ``` The code is actually a nested loop: the external loop over array elements, and the internal counts subsums starting with the current element. 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 3e930079..16d14071 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 @@ -16,7 +16,7 @@ The array in the process: ```js no-beautify Jazz, Blues -Jazz, Bues, Rock-n-Roll +Jazz, Blues, Rock-n-Roll Jazz, Classics, Rock-n-Roll Classics, Rock-n-Roll Rap, Reggae, Classics, Rock-n-Roll diff --git a/1-js/05-data-types/04-array/array-pop.png b/1-js/05-data-types/04-array/array-pop.png deleted file mode 100644 index c4f80e85..00000000 Binary files a/1-js/05-data-types/04-array/array-pop.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-pop.svg b/1-js/05-data-types/04-array/array-pop.svg new file mode 100644 index 00000000..35191605 --- /dev/null +++ b/1-js/05-data-types/04-array/array-pop.svg @@ -0,0 +1 @@ +0123"Apple""Orange""Pear""Lemon"length = 4clear012"Apple""Orange""Pear"length = 3 \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-pop@2x.png b/1-js/05-data-types/04-array/array-pop@2x.png deleted file mode 100644 index f7179bbc..00000000 Binary files a/1-js/05-data-types/04-array/array-pop@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-shift.png b/1-js/05-data-types/04-array/array-shift.png deleted file mode 100644 index 093b16ed..00000000 Binary files a/1-js/05-data-types/04-array/array-shift.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-shift.svg b/1-js/05-data-types/04-array/array-shift.svg new file mode 100644 index 00000000..09236b9d --- /dev/null +++ b/1-js/05-data-types/04-array/array-shift.svg @@ -0,0 +1 @@ +123"Orange""Pear""Lemon"length = 423"Orange""Pear""Lemon"length = 3clearmove elements to the left0"Apple"012"Orange""Pear""Lemon"11 \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-shift@2x.png b/1-js/05-data-types/04-array/array-shift@2x.png deleted file mode 100644 index 3be433f7..00000000 Binary files a/1-js/05-data-types/04-array/array-shift@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-speed.png b/1-js/05-data-types/04-array/array-speed.png deleted file mode 100644 index f537417d..00000000 Binary files a/1-js/05-data-types/04-array/array-speed.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/array-speed.svg b/1-js/05-data-types/04-array/array-speed.svg new file mode 100644 index 00000000..5660cd5e --- /dev/null +++ b/1-js/05-data-types/04-array/array-speed.svg @@ -0,0 +1 @@ +0123popunshiftpushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/array-speed@2x.png b/1-js/05-data-types/04-array/array-speed@2x.png deleted file mode 100644 index 49e36d63..00000000 Binary files a/1-js/05-data-types/04-array/array-speed@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/article.md b/1-js/05-data-types/04-array/article.md index 2fad1601..7dc54bd4 100644 --- a/1-js/05-data-types/04-array/article.md +++ b/1-js/05-data-types/04-array/article.md @@ -1,12 +1,12 @@ -# Arrays +# Arrays Objects allow you to store keyed collections of values. That's fine. -But quite often we find that we need an *ordered collection*, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc. +But quite often we find that we need an *ordered collection*, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc. It is not convenient to use an object here, because it provides no methods to manage the order of elements. We can’t insert a new property “between” the existing ones. Objects are just not meant for such use. -There exists a special data structure named `Array`, to store ordered collections. +There exists a special data structure named `Array`, to store ordered collections. ## Declaration @@ -81,10 +81,10 @@ arr[3](); // hello ````smart header="Trailing comma" An array, just like an object, may end with a comma: -```js +```js let fruits = [ - "Apple", - "Orange", + "Apple", + "Orange", "Plum"*!*,*/!* ]; ``` @@ -95,18 +95,18 @@ The "trailing comma" style makes it easier to insert/remove items, because all l ## Methods pop/push, shift/unshift -A [queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) is one of most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations: +A [queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type)) is one of the most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations: - `push` appends an element to the end. - `shift` get an element from the beginning, advancing the queue, so that the 2nd element becomes the 1st. -![](queue.png) +![](queue.svg) Arrays support both operations. In practice we need it very often. For example, a queue of messages that need to be shown on-screen. -There's another use case for arrays -- the data structure named [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)). +There's another use case for arrays -- the data structure named [stack](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)). It supports two operations: @@ -117,11 +117,11 @@ So new elements are added or taken always from the "end". A stack is usually illustrated as a pack of cards: new cards are added to the top or taken from the top: -![](stack.png) +![](stack.svg) For stacks, the latest pushed item is received first, that's also called LIFO (Last-In-First-Out) principle. For queues, we have FIFO (First-In-First-Out). -Arrays in JavaScript can work both as a queue and as a stack. They allow 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). @@ -189,11 +189,11 @@ alert( fruits ); ## Internals -An array is a special kind of object. The square brackets used to access a property `arr[0]` actually come from the object syntax. Numbers are used as keys. +An array is a special kind of object. The square brackets used to access a property `arr[0]` actually come from the object syntax. That's essentially the same as `obj[key]`, where `arr` is the object, while numbers are used as keys. They extend objects providing special methods to work with ordered collections of data and also the `length` property. But at the core it's still an object. -Remember, there are only 7 basic types in JavaScript. Array is an object and thus behaves like an object. +Remember, there are only 7 basic types in JavaScript. Array is an object and thus behaves like an object. For instance, it is copied by reference: @@ -203,7 +203,7 @@ let fruits = ["Banana"] let arr = fruits; // copy by reference (two variables reference the same array) alert( arr === fruits ); // true - + arr.push("Pear"); // modify the array by reference alert( fruits ); // Banana, Pear - 2 items now @@ -229,7 +229,7 @@ But the engine will see that we're working with the array as with a regular obje The ways to misuse an array: -- Add a non-numeric property like `arr.test = 5`. +- Add a non-numeric property like `arr.test = 5`. - Make holes, like: add `arr[0]` and then `arr[1000]` (and nothing between them). - Fill the array in the reverse order, like `arr[1000]`, `arr[999]` and so on. @@ -239,7 +239,7 @@ Please think of arrays as special structures to work with the *ordered data*. Th Methods `push/pop` run fast, while `shift/unshift` are slow. -![](array-speed.png) +![](array-speed.svg) Why is it faster to work with the end of an array than with its beginning? Let's see what happens during the execution: @@ -255,7 +255,7 @@ The `shift` operation must do 3 things: 2. Move all elements to the left, renumber them from the index `1` to `0`, from `2` to `1` and so on. 3. Update the `length` property. -![](array-shift.png) +![](array-shift.svg) **The more elements in the array, the more time to move them, more in-memory operations.** @@ -269,7 +269,7 @@ The actions for the `pop` operation: fruits.pop(); // take 1 element from the end ``` -![](array-pop.png) +![](array-pop.svg) **The `pop` method does not need to move anything, because other elements keep their indexes. That's why it's blazingly fast.** @@ -296,7 +296,7 @@ let fruits = ["Apple", "Orange", "Plum"]; // iterates over array elements for (let fruit of fruits) { - alert( fruit ); + alert( fruit ); } ``` @@ -320,7 +320,7 @@ But that's actually a bad idea. There are potential problems with it: There are so-called "array-like" objects in the browser and in other environments, that *look like arrays*. That is, they have `length` and indexes properties, but they may also have other non-numeric properties and methods, which we usually don't need. The `for..in` loop will list them though. So if we need to work with array-like objects, then these "extra" properties can become a problem. -2. The `for..in` loop is optimized for generic objects, not arrays, and thus is 10-100 times slower. Of course, it's still very fast. The speedup may only matter in bottlenecks or seem irrelevant. But still we should be aware of the difference. +2. The `for..in` loop is optimized for generic objects, not arrays, and thus is 10-100 times slower. Of course, it's still very fast. The speedup may only matter in bottlenecks. But still we should be aware of the difference. Generally, we shouldn't use `for..in` for arrays. @@ -338,7 +338,7 @@ fruits[123] = "Apple"; alert( fruits.length ); // 124 ``` -Note that we usually don't use arrays like that. +Note that we usually don't use arrays like that. Another interesting thing about the `length` property is that it's writable. @@ -385,7 +385,7 @@ To evade such surprises, we usually use square brackets, unless we really know w ## Multidimensional arrays -Arrays can have items that are also arrays. We can use it for multidimensional arrays, to store matrices: +Arrays can have items that are also arrays. We can use it for multidimensional arrays, for example to store matrices: ```js run let matrix = [ @@ -394,7 +394,7 @@ let matrix = [ [7, 8, 9] ]; -alert( matrix[1][1] ); // the central element +alert( matrix[1][1] ); // 5, the central element ``` ## toString @@ -445,7 +445,7 @@ Array is a special kind of object, suited to storing and managing ordered data i The call to `new Array(number)` creates an array with the given length, but without elements. -- The `length` property is the array length or, to be precise, its last numeric index plus one. It is auto-adjusted by array methods. +- The `length` property is the array length or, to be precise, its last numeric index plus one. It is auto-adjusted by array methods. - If we shorten `length` manually, the array is truncated. We can use an array as a deque with the following operations: @@ -453,7 +453,7 @@ We can use an array as a deque with the following operations: - `push(...items)` adds `items` to the end. - `pop()` removes the element from the end and returns it. - `shift()` removes the element from the beginning and returns it. -- `unshift(...items)` adds items to the beginning. +- `unshift(...items)` adds `items` to the beginning. To loop over the elements of the array: - `for (let i=0; i. - diff --git a/1-js/05-data-types/04-array/queue.png b/1-js/05-data-types/04-array/queue.png deleted file mode 100644 index d2eb466a..00000000 Binary files a/1-js/05-data-types/04-array/queue.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/queue.svg b/1-js/05-data-types/04-array/queue.svg new file mode 100644 index 00000000..0ed2f1cd --- /dev/null +++ b/1-js/05-data-types/04-array/queue.svg @@ -0,0 +1 @@ +pushshift \ No newline at end of file diff --git a/1-js/05-data-types/04-array/queue@2x.png b/1-js/05-data-types/04-array/queue@2x.png deleted file mode 100644 index 823afaa5..00000000 Binary files a/1-js/05-data-types/04-array/queue@2x.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/stack.png b/1-js/05-data-types/04-array/stack.png deleted file mode 100644 index 6feb0c94..00000000 Binary files a/1-js/05-data-types/04-array/stack.png and /dev/null differ diff --git a/1-js/05-data-types/04-array/stack.svg b/1-js/05-data-types/04-array/stack.svg new file mode 100644 index 00000000..dcc600e7 --- /dev/null +++ b/1-js/05-data-types/04-array/stack.svg @@ -0,0 +1 @@ +pushpop \ No newline at end of file diff --git a/1-js/05-data-types/04-array/stack@2x.png b/1-js/05-data-types/04-array/stack@2x.png deleted file mode 100644 index 79f6f8d9..00000000 Binary files a/1-js/05-data-types/04-array/stack@2x.png and /dev/null differ diff --git a/1-js/05-data-types/05-array-methods/10-average-age/task.md b/1-js/05-data-types/05-array-methods/10-average-age/task.md index a991c156..bf5f85df 100644 --- a/1-js/05-data-types/05-array-methods/10-average-age/task.md +++ b/1-js/05-data-types/05-array-methods/10-average-age/task.md @@ -4,7 +4,7 @@ importance: 4 # Get average age -Write the function `getAverageAge(users)` that gets an array of objects with property `age` and gets the average. +Write the function `getAverageAge(users)` that gets an array of objects with property `age` and returns the average age. The formula for the average is `(age1 + age2 + ... + ageN) / N`. @@ -19,4 +19,3 @@ let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28 ``` - diff --git a/1-js/05-data-types/05-array-methods/11-array-unique/solution.md b/1-js/05-data-types/05-array-methods/11-array-unique/solution.md index 32d3b267..b9d627a0 100644 --- a/1-js/05-data-types/05-array-methods/11-array-unique/solution.md +++ b/1-js/05-data-types/05-array-methods/11-array-unique/solution.md @@ -36,4 +36,4 @@ So if `arr.length` is `10000` we'll have something like `10000*10000` = 100 mill So the solution is only good for small arrays. -Further in the chapter we'll see how to optimize it. +Further in the chapter we'll see how to optimize it. diff --git a/1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/solution.js b/1-js/05-data-types/05-array-methods/6-calculator-extendable/_js.view/solution.js index 50c40e80..45ef1619 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 @@ -1,6 +1,6 @@ function Calculator() { - let methods = { + this.methods = { "-": (a, b) => a - b, "+": (a, b) => a + b }; @@ -12,14 +12,14 @@ function Calculator() { op = split[1], b = +split[2] - if (!methods[op] || isNaN(a) || isNaN(b)) { + if (!this.methods[op] || isNaN(a) || isNaN(b)) { return NaN; } - return methods[op](a, b); + return this.methods[op](a, b); } this.addMethod = function(name, func) { - methods[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 41178663..982d3f9e 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,3 +1,5 @@ -- Please note how methods are stored. They are simply added to the internal object. +- 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/6-calculator-extendable/task.md b/1-js/05-data-types/05-array-methods/6-calculator-extendable/task.md index cc5453ce..e0d302f4 100644 --- a/1-js/05-data-types/05-array-methods/6-calculator-extendable/task.md +++ b/1-js/05-data-types/05-array-methods/6-calculator-extendable/task.md @@ -31,6 +31,6 @@ The task consists of two parts. alert( result ); // 8 ``` -- No brackets or complex expressions in this task. +- No parentheses or complex expressions in this task. - The numbers and the operator are delimited with exactly one space. - There may be error handling if you'd like to add it. diff --git a/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md b/1-js/05-data-types/05-array-methods/8-sort-objects/solution.md index f5684a6d..9f1ade70 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,5 +1,5 @@ ```js run no-beautify -function sortByName(arr) { +function sortByAge(arr) { arr.sort((a, b) => a.age > b.age ? 1 : -1); } @@ -7,11 +7,12 @@ let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; -let arr = [ john, pete, mary ]; +let arr = [ pete, john, mary ]; -sortByName(arr); +sortByAge(arr); // now sorted is: [john, mary, pete] alert(arr[0].name); // John +alert(arr[1].name); // Mary alert(arr[2].name); // Pete ``` diff --git a/1-js/05-data-types/05-array-methods/8-sort-objects/task.md b/1-js/05-data-types/05-array-methods/8-sort-objects/task.md index fae6bcbe..9a215c9f 100644 --- a/1-js/05-data-types/05-array-methods/8-sort-objects/task.md +++ b/1-js/05-data-types/05-array-methods/8-sort-objects/task.md @@ -2,9 +2,9 @@ importance: 5 --- -# Sort objects +# Sort users by age -Write the function `sortByName(users)` that gets an array of objects with the `age` property and sorts them by `age`. +Write the function `sortByAge(users)` that gets an array of objects with the `age` property and sorts them by `age`. For instance: @@ -13,11 +13,12 @@ let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; -let arr = [ john, pete, mary ]; +let arr = [ pete, john, mary ]; -sortByName(arr); +sortByAge(arr); // now: [john, mary, pete] alert(arr[0].name); // John +alert(arr[1].name); // Mary alert(arr[2].name); // Pete ``` diff --git a/1-js/05-data-types/05-array-methods/9-shuffle/solution.md b/1-js/05-data-types/05-array-methods/9-shuffle/solution.md index a43715db..6674c444 100644 --- a/1-js/05-data-types/05-array-methods/9-shuffle/solution.md +++ b/1-js/05-data-types/05-array-methods/9-shuffle/solution.md @@ -45,7 +45,7 @@ for (let key in count) { } ``` -An example result (for V8, July 2017): +An example result (depends on JS engine): ```js 123: 250706 @@ -68,7 +68,13 @@ There are other good ways to do the task. For instance, there's a great algorith function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i - [array[i], array[j]] = [array[j], array[i]]; // swap elements + + // swap elements array[i] and array[j] + // we use "destructuring assignment" syntax to achieve that + // you'll find more details about that syntax in later chapters + // same can be written as: + // let t = array[i]; array[i] = array[j]; array[j] = t + [array[i], array[j]] = [array[j], array[i]]; } } ``` diff --git a/1-js/05-data-types/05-array-methods/article.md b/1-js/05-data-types/05-array-methods/article.md index 42bbd946..8e5b4efd 100644 --- a/1-js/05-data-types/05-array-methods/article.md +++ b/1-js/05-data-types/05-array-methods/article.md @@ -11,7 +11,7 @@ We already know methods that add and remove items from the beginning or the end: - `arr.shift()` -- extracts an item from the beginning, - `arr.unshift(...items)` -- adds items to the beginning. -Here are few others. +Here are a few others. ### splice @@ -36,7 +36,7 @@ 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: add, remove and insert elements. +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 syntax is: @@ -119,29 +119,28 @@ The method [arr.slice](mdn:js/Array/slice) is much simpler than similar-looking The syntax is: ```js -arr.slice(start, end) +arr.slice([start], [end]) ``` -It returns a new array containing all items from index `"start"` to `"end"` (not including `"end"`). Both `start` and `end` can be negative, in that case position from array end is assumed. +It returns a new array copying to it all items from index `start` to `end` (not including `end`). Both `start` and `end` can be negative, in that case position from array end is assumed. -It works like `str.slice`, but makes subarrays instead of substrings. +It's similar to a string method `str.slice`, but instead of substrings it makes subarrays. For instance: ```js run -let str = "test"; let arr = ["t", "e", "s", "t"]; -alert( str.slice(1, 3) ); // es -alert( arr.slice(1, 3) ); // e,s +alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3) -alert( str.slice(-2) ); // st -alert( arr.slice(-2) ); // s,t +alert( arr.slice(-2) ); // s,t (copy from -2 till the end) ``` +We can also call it without arguments: `arr.slice()` creates a copy of `arr`. That's often used to obtain a copy for further transformations that should not affect the original array. + ### concat -The method [arr.concat](mdn:js/Array/concat) joins the array with other arrays and/or items. +The method [arr.concat](mdn:js/Array/concat) creates a new array that includes values from other arrays and additional items. The syntax is: @@ -153,24 +152,24 @@ It accepts any number of arguments -- either arrays or values. The result is a new array containing items from `arr`, then `arg1`, `arg2` etc. -If an argument is an array or has `Symbol.isConcatSpreadable` property, then all its elements are copied. Otherwise, the argument itself is copied. +If an argument `argN` is an array, then all its elements are copied. Otherwise, the argument itself is copied. For instance: ```js run let arr = [1, 2]; -// merge arr with [3,4] +// create an array from: arr and [3,4] alert( arr.concat([3, 4])); // 1,2,3,4 -// merge arr with [3,4] and [5,6] +// create an array from: arr and [3,4] and [5,6] alert( arr.concat([3, 4], [5, 6])); // 1,2,3,4,5,6 -// merge arr with [3,4], then add values 5 and 6 +// create an array from: arr and [3,4], then add values 5 and 6 alert( arr.concat([3, 4], 5, 6)); // 1,2,3,4,5,6 ``` -Normally, it only copies elements from arrays ("spreads" them). Other objects, even if they look like arrays, added as a whole: +Normally, it only copies elements from arrays. Other objects, even if they look like arrays, added as a whole: ```js run let arr = [1, 2]; @@ -184,7 +183,7 @@ alert( arr.concat(arrayLike) ); // 1,2,[object Object] //[1, 2, arrayLike] ``` -...But if an array-like object has `Symbol.isConcatSpreadable` property, then its elements are added instead: +...But if an array-like object has a special property `Symbol.isConcatSpreadable` property, the it's treated as array by `concat`: its elements are added instead: ```js run let arr = [1, 2]; @@ -232,14 +231,14 @@ The result of the function (if it returns any) is thrown away and ignored. ## Searching in array -These are methods to search for something in an array. +Now let's cover methods that search in an array. ### indexOf/lastIndexOf and includes The methods [arr.indexOf](mdn:js/Array/indexOf), [arr.lastIndexOf](mdn:js/Array/lastIndexOf) and [arr.includes](mdn:js/Array/includes) have the same syntax and do essentially the same as their string counterparts, but operate on items instead of characters: -- `arr.indexOf(item, from)` looks for `item` starting from index `from`, and returns the index where it was found, otherwise `-1`. -- `arr.lastIndexOf(item, from)` -- same, but looks from right to left. +- `arr.indexOf(item, from)` -- looks for `item` starting from index `from`, and returns the index where it was found, otherwise `-1`. +- `arr.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. For instance: @@ -280,7 +279,7 @@ let result = arr.find(function(item, index, array) { }); ``` -The function is called repetitively for each element of the array: +The function is called for elements of the array, one after another: - `item` is the element. - `index` is its index. @@ -304,7 +303,7 @@ alert(user.name); // John In real life arrays of objects is a common thing, so the `find` method is very useful. -Note that in the example we provide to `find` the function `item => item.id == 1` with one argument. Other arguments of this function are rarely used. +Note that in the example we provide to `find` the function `item => item.id == 1` with one argument. That's typical, other arguments of this function are rarely used. The [arr.findIndex](mdn:js/Array/findIndex) method is essentially the same, but it returns the index where the element was found instead of the element itself and `-1` is returned when nothing is found. @@ -314,12 +313,12 @@ The `find` method looks for a single (first) element that makes the function ret If there may be many, we can use [arr.filter(fn)](mdn:js/Array/filter). -The syntax is similar to `find`, but filter continues to iterate for all array elements even if `true` is already returned: +The syntax is similar to `find`, but `filter` returns an array of all matching elements: ```js let results = arr.filter(function(item, index, array) { - // if true item is pushed to results and iteration continues - // returns empty array for complete falsy scenario + // if true item is pushed to results and the iteration continues + // returns empty array if nothing found }); ``` @@ -340,23 +339,22 @@ alert(someUsers.length); // 2 ## Transform an array -This section is about the methods transforming or reordering the array. - +Let's move on to methods that transform and reorder an array. ### map The [arr.map](mdn:js/Array/map) method is one of the most useful and often used. +It calls the function for each element of the array and returns the array of results. + The syntax is: ```js let result = arr.map(function(item, index, array) { // returns the new value instead of item -}) +}); ``` -It calls the function for each element of the array and returns the array of results. - For instance, here we transform each element into its length: ```js run @@ -366,14 +364,16 @@ alert(lengths); // 5,7,6 ### sort(fn) -The method [arr.sort](mdn:js/Array/sort) sorts the array *in place*. +The call to [arr.sort()](mdn:js/Array/sort) sorts the array *in place*, changing its element order. + +It also returns the sorted array, but the returned value is usually ignored, as `arr` itself is modified. For instance: ```js run let arr = [ 1, 2, 15 ]; -// the method reorders the content of arr (and returns it) +// the method reorders the content of arr arr.sort(); alert( arr ); // *!*1, 15, 2*/!* @@ -385,20 +385,20 @@ The order became `1, 15, 2`. Incorrect. But why? **The items are sorted as strings by default.** -Literally, all elements are converted to strings and then compared. So, the lexicographic ordering is applied and indeed `"2" > "15"`. +Literally, all elements are converted to strings for comparisons. For strings, lexicographic ordering is applied and indeed `"2" > "15"`. -To use our own sorting order, we need to supply a function of two arguments as the argument of `arr.sort()`. +To use our own sorting order, we need to supply a function as the argument of `arr.sort()`. -The function should work like this: +The function should compare two arbitrary values and return: ```js function compare(a, b) { - if (a > b) return 1; - if (a == b) return 0; - if (a < b) return -1; + if (a > b) return 1; // if the first value is greater than the second + if (a == b) return 0; // if values are equal + if (a < b) return -1; // if the first value is less than the second } ``` -For instance: +For instance, to sort as numbers: ```js run function compareNumeric(a, b) { @@ -418,9 +418,9 @@ alert(arr); // *!*1, 2, 15*/!* Now it works as intended. -Let's step aside and think what's happening. The `arr` can be array of anything, right? It may contain numbers or strings or html elements or whatever. We have a set of *something*. To sort it, we need an *ordering function* that knows how to compare its elements. The default is a string order. +Let's step aside and think 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 has a built-in implementation of sorting algorithm. We don't need to care how it exactly works (an optimized [quicksort](https://en.wikipedia.org/wiki/Quicksort) most of the time). It will walk the array, compare its elements using the provided function and reorder them, all we need is to provide the `fn` which does the comparison. +The `arr.sort(fn)` method implements a generic sorting algorithm. We don't need to care how it internally works (an optimized [quicksort](https://en.wikipedia.org/wiki/Quicksort) 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: @@ -430,7 +430,7 @@ By the way, if we ever want to know which elements are compared -- nothing preve }); ``` -The algorithm may compare an element multiple times in the process, but it tries to make as few comparisons as possible. +The algorithm may compare an element with multiple others in the process, but it tries to make as few comparisons as possible. ````smart header="A comparison function may return any number" @@ -454,7 +454,7 @@ Remember [arrow functions](info:function-expressions-arrows#arrow-functions)? We arr.sort( (a, b) => a - b ); ``` -This works exactly the same as the other, longer, version above. +This works exactly the same as the longer version above. ```` ### reverse @@ -474,7 +474,7 @@ It also returns the array `arr` after the reversal. ### split and join -Here's the situation from the real life. We are writing a messaging app, and the person enters the comma-delimited list of receivers: `John, Pete, Mary`. But for us an array of names would be much more comfortable than a single string. How to get it? +Here's the situation from real life. We are writing a messaging app, and the person enters the comma-delimited list of receivers: `John, Pete, Mary`. But for us an array of names would be much more comfortable than a single string. How to get it? The [str.split(delim)](mdn:js/String/split) method does exactly that. It splits the string into an array by the given delimiter `delim`. @@ -508,14 +508,14 @@ alert( str.split('') ); // t,e,s,t ``` ```` -The call [arr.join(separator)](mdn:js/Array/join) does the reverse to `split`. It creates a string of `arr` items glued by `separator` between them. +The call [arr.join(glue)](mdn:js/Array/join) does the reverse to `split`. It creates a string of `arr` items joined by `glue` between them. For instance: ```js run let arr = ['Bilbo', 'Gandalf', 'Nazgul']; -let str = arr.join(';'); +let str = arr.join(';'); // glue the array into a string using ; alert( str ); // Bilbo;Gandalf;Nazgul ``` @@ -533,22 +533,25 @@ The syntax is: ```js let value = arr.reduce(function(previousValue, item, index, array) { // ... -}, initial); +}, [initial]); ``` -The function is applied to the elements. You may notice the familiar arguments, starting from the 2nd: +The function is applied to all array elements one after another and "carries on" its result to the next call. +Arguments: + +- `previousValue` -- 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. -So far, like `forEach/map`. But there's one more argument: +As function is applied, the result of the previous function call is passed to the next one as the first argument. -- `previousValue` -- is the result of the previous function call, `initial` for the first call. +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`. The easiest way to grasp that is by example. -Here we get a sum of array in one line: +Here we get a sum of an array in one line: ```js run let arr = [1, 2, 3, 4, 5]; @@ -558,17 +561,17 @@ let result = arr.reduce((sum, current) => sum + current, 0); alert(result); // 15 ``` -Here we used the most common variant of `reduce` which uses only 2 arguments. +The function passed to `reduce` uses only 2 arguments, that's typically enough. Let's see the details of what's going on. -1. On the first run, `sum` is the initial value (the last argument of `reduce`), equals `0`, and `current` is the first array element, equals `1`. So the result is `1`. +1. On the first run, `sum` is the `initial` value (the last argument of `reduce`), equals `0`, and `current` is the first array element, equals `1`. So the function result is `1`. 2. On the second run, `sum = 1`, we add the second array element (`2`) to it and return. 3. On the 3rd run, `sum = 3` and we add one more element to it, and so on... The calculation flow: -![](reduce.png) +![](reduce.svg) Or in the form of a table, where each row represents a function call on the next array element: @@ -580,8 +583,7 @@ Or in the form of a table, where each row represents a function call on the next |the fourth call|`6`|`4`|`10`| |the fifth call|`10`|`5`|`15`| - -As we can see, the result of the previous call becomes the first argument of the next one. +Here we can clearly see how the result of the previous call becomes the first argument of the next one. We also can omit the initial value: @@ -653,7 +655,7 @@ arr.map(func, thisArg); The value of `thisArg` parameter becomes `this` for `func`. -For instance, here we use an object method as a filter and `thisArg` comes in handy: +For instance, here we use an object method as a filter and `thisArg` helps with that: ```js run let user = { @@ -681,7 +683,7 @@ In the call above, we use `user.younger` as a filter and also provide `user` as ## Summary -A cheatsheet of array methods: +A cheat sheet of array methods: - To add/remove elements: - `push(...items)` -- adds items to the end, @@ -697,7 +699,7 @@ A cheatsheet of array methods: - `includes(value)` -- returns `true` if the array has `value`, otherwise `false`. - `find/filter(func)` -- filter elements through the function, return first/all values that make it return `true`. - `findIndex` is like `find`, but returns the index instead of a value. - + - To iterate over elements: - `forEach(func)` -- calls `func` for every element, does not return anything. @@ -725,8 +727,8 @@ These methods are the most used ones, they cover 99% of use cases. But there are For the full list, see the [manual](mdn:js/Array). -From the first sight it may seem that there are so many methods, quite difficult to remember. But actually that's much easier than it seems. +From the first sight it may seem that there are so many methods, quite difficult to remember. But actually that's much easier. -Look through the cheatsheet just to be aware of them. Then solve the tasks of this chapter to practice, so that you have experience with array methods. +Look through the cheat sheet just to be aware of them. Then solve the tasks of this chapter to practice, so that you have experience with array methods. -Afterwards whenever you need to do something with an array, and you don't know how -- come here, look at the cheatsheet and find the right method. Examples will help you to write it correctly. Soon you'll automatically remember the methods, without specific efforts from your side. +Afterwards whenever you need to do something with an array, and you don't know how -- come here, look at the cheat sheet and find the right method. Examples will help you to write it correctly. Soon you'll automatically remember the methods, without specific efforts from your side. diff --git a/1-js/05-data-types/05-array-methods/reduce.png b/1-js/05-data-types/05-array-methods/reduce.png deleted file mode 100644 index 13d13536..00000000 Binary files a/1-js/05-data-types/05-array-methods/reduce.png and /dev/null differ diff --git a/1-js/05-data-types/05-array-methods/reduce.svg b/1-js/05-data-types/05-array-methods/reduce.svg new file mode 100644 index 00000000..47797bf7 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/reduce.svg @@ -0,0 +1 @@ +1sum 0 current 12sum 0+1 current 23sum 0+1+2 current 34sum 0+1+2+3 current 45sum 0+1+2+3+4 current 50+1+2+3+4+5 = 15 \ No newline at end of file diff --git a/1-js/05-data-types/05-array-methods/reduce@2x.png b/1-js/05-data-types/05-array-methods/reduce@2x.png deleted file mode 100644 index 5625c2b4..00000000 Binary files a/1-js/05-data-types/05-array-methods/reduce@2x.png and /dev/null differ diff --git a/1-js/05-data-types/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md index 7f72f5ca..80f067ca 100644 --- a/1-js/05-data-types/06-iterable/article.md +++ b/1-js/05-data-types/06-iterable/article.md @@ -3,9 +3,9 @@ *Iterable* objects is a generalization of arrays. That's a concept that allows 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 iterable also. As we'll see, many built-in operators and methods rely on them. +Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well. For instance, strings are also iterable. -If an object represents a collection (list, set) of something, then `for..of` is a great syntax to loop over it, so let's see how to make it work. +If an object isn't technically an array, but represents a collection (list, set) of something, then `for..of` is a great syntax to loop over it, so let's see how to make it work. ## Symbol.iterator @@ -31,9 +31,9 @@ To make the `range` iterable (and thus let `for..of` work) we need to add a meth 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` must be the new value. +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. -Here's the full implementation for `range`: +Here's the full implementation for `range` with remarks: ```js run let range = { @@ -68,10 +68,10 @@ for (let num of range) { } ``` -Please note the core feature of iterables: an important separation of concerns: +Please note the core feature of iterables: separation of concerns. - The `range` itself does not have the `next()` method. -- Instead, another object, a so-called "iterator" is created by the call to `range[Symbol.iterator]()`, and it handles the whole iteration. +- Instead, another object, a so-called "iterator" is created by the call to `range[Symbol.iterator]()`, and its `next()` generates values for the iteration. So, the iterator object is separate from the object it iterates over. @@ -105,7 +105,7 @@ for (let num of range) { Now `range[Symbol.iterator]()` returns the `range` object itself: it has the necessary `next()` method and remembers the current iteration progress in `this.current`. Shorter? Yes. And sometimes that's fine too. -The downside is that now it's impossible to have two `for..of` loops running over the object simultaneously: they'll share the iteration state, because there's only one iterator -- the object itself. But two parallel for-ofs is a rare thing, doable with some async scenarios. +The downside is that now it's impossible to have two `for..of` loops running over the object simultaneously: they'll share the iteration state, because there's only one iterator -- the object itself. But two parallel for-ofs is a rare thing, even in async scenarios. ```smart header="Infinite iterators" Infinite iterators are also possible. For instance, the `range` becomes infinite for `range.to = Infinity`. Or we can make an iterable object that generates an infinite sequence of pseudorandom numbers. Also can be useful. @@ -140,11 +140,9 @@ for (let char of str) { ## Calling an iterator explicitly -Normally, internals of iterables are hidden from the external code. There's a `for..of` loop, that works, that's all it needs to know. +For deeper understanding let's see how to use an iterator explicitly. -But to understand things a little bit deeper let's see how to create an iterator explicitly. - -We'll iterate over a string the same way as `for..of`, but with direct calls. This code gets a string iterator and calls it "manually": +We'll iterate over a string in exactlly 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"; @@ -170,7 +168,9 @@ There are two official terms that look similar, but are very different. Please m - *Iterables* are objects that implement the `Symbol.iterator` method, as described above. - *Array-likes* are objects that have indexes and `length`, so they look like arrays. -Naturally, these properties can combine. For instance, strings are both iterable (`for..of` works on them) and array-like (they have numeric indexes and `length`). +When we use JavaScript for practical tasks in browser or other environments, 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`). But an iterable may be not array-like. And vice versa an array-like may be not iterable. @@ -191,11 +191,11 @@ for (let item of arrayLike) {} */!* ``` -What do they have in common? Both iterables and array-likes are usually *not arrays*, they don't have `push`, `pop` etc. That's rather inconvenient if we have such an object and want to work with it as with an array. +Both iterables and array-likes are usually *not arrays*, they don't have `push`, `pop` etc. That's rather inconvenient if we have such an object and want to work with it as with an array. E.g. we would like to work with `range` using array methods. How to achieve that? ## Array.from -There's a universal method [Array.from](mdn:js/Array/from) that brings them together. It takes an iterable or array-like value and makes a "real" `Array` from it. Then we can call array methods on it. +There's a universal method [Array.from](mdn:js/Array/from) that takes an iterable or array-like value and makes a "real" `Array` from it. Then we can call array methods on it. For instance: @@ -227,7 +227,7 @@ The full syntax for `Array.from` allows to provide an optional "mapping" functio Array.from(obj[, mapFn, thisArg]) ``` -The second argument `mapFn` should be the function to apply to each element before adding to the array, and `thisArg` allows to set `this` for it. +The optional second argument `mapFn` can be a function that will be applied to each element before adding to the array, and `thisArg` allows to set `this` for it. For instance: @@ -281,7 +281,7 @@ let str = '𝒳😂𩷶'; alert( slice(str, 1, 3) ); // 😂𩷶 -// native method does not support surrogate pairs +// the native method does not support surrogate pairs alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs) ``` diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/article.md b/1-js/05-data-types/07-map-set-weakmap-weakset/article.md deleted file mode 100644 index 034ad22c..00000000 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/article.md +++ /dev/null @@ -1,458 +0,0 @@ - -# Map, Set, WeakMap and WeakSet - -Now we've learned about the following complex data structures: - -- Objects for storing keyed collections. -- Arrays for storing ordered collections. - -But that's not enough for real life. That's why `Map` and `Set` also exist. - -## Map - -[Map](mdn:js/Map) is a collection of keyed data items, just like an `Object`. But the main difference is that `Map` allows keys of any type. - -The main methods are: - -- `new Map()` -- creates the map. -- `map.set(key, value)` -- stores the value by the key. -- `map.get(key)` -- returns the value by the key, `undefined` if `key` doesn't exist in map. -- `map.has(key)` -- returns `true` if the `key` exists, `false` otherwise. -- `map.delete(key)` -- removes the value by the key. -- `map.clear()` -- clears the map -- `map.size` -- returns the current element count. - -For instance: - -```js run -let map = new Map(); - -map.set('1', 'str1'); // a string key -map.set(1, 'num1'); // a numeric key -map.set(true, 'bool1'); // a boolean key - -// remember the regular Object? it would convert keys to string -// Map keeps the type, so these two are different: -alert( map.get(1) ); // 'num1' -alert( map.get('1') ); // 'str1' - -alert( map.size ); // 3 -``` - -As we can see, unlike objects, keys are not converted to strings. Any type of key is possible. - -**Map can also use objects as keys.** - -For instance: -```js run -let john = { name: "John" }; - -// for every user, let's store their visits count -let visitsCountMap = new Map(); - -// john is the key for the map -visitsCountMap.set(john, 123); - -alert( visitsCountMap.get(john) ); // 123 -``` - -Using objects as keys is one of most notable and important `Map` features. For string keys, `Object` can be fine, but it would be difficult to replace the `Map` with a regular `Object` in the example above. - -In the old times, before `Map` existed, people added unique identifiers to objects for that: - -```js run -// we add the id field -let john = { name: "John", *!*id: 1*/!* }; - -let visitsCounts = {}; - -// now store the value by id -visitsCounts[john.id] = 123; - -alert( visitsCounts[john.id] ); // 123 -``` - -...But `Map` is much more elegant. - - -```smart header="How `Map` compares keys" -To test values for equivalence, `Map` uses the algorithm [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). It is roughly the same as strict equality `===`, but the difference is that `NaN` is considered equal to `NaN`. So `NaN` can be used as the key as well. - -This algorithm can't be changed or customized. -``` - - -````smart header="Chaining" - -Every `map.set` call returns the map itself, so we can "chain" the calls: - -```js -map.set('1', 'str1') - .set(1, 'num1') - .set(true, 'bool1'); -``` -```` - -## Map from Object - -When a `Map` is created, we can pass an array (or another iterable) with key-value pairs, like this: - -```js -// array of [key, value] pairs -let map = new Map([ - ['1', 'str1'], - [1, 'num1'], - [true, 'bool1'] -]); -``` - -There is a built-in method [Object.entries(obj)](mdn:js/Object/entries) that returns an array of key/value pairs for an object exactly in that format. - -So we can initialize a map from an object like this: - -```js -let map = new Map(Object.entries({ - name: "John", - age: 30 -})); -``` - -Here, `Object.entries` returns the array of key/value pairs: `[ ["name","John"], ["age", 30] ]`. That's what `Map` needs. - -## Iteration over Map - -For looping over a `map`, there are 3 methods: - -- `map.keys()` -- returns an iterable for keys, -- `map.values()` -- returns an iterable for values, -- `map.entries()` -- returns an iterable for entries `[key, value]`, it's used by default in `for..of`. - -For instance: - -```js run -let recipeMap = new Map([ - ['cucumber', 500], - ['tomatoes', 350], - ['onion', 50] -]); - -// iterate over keys (vegetables) -for (let vegetable of recipeMap.keys()) { - alert(vegetable); // cucumber, tomatoes, onion -} - -// iterate over values (amounts) -for (let amount of recipeMap.values()) { - alert(amount); // 500, 350, 50 -} - -// iterate over [key, value] entries -for (let entry of recipeMap) { // the same as of recipeMap.entries() - alert(entry); // cucumber,500 (and so on) -} -``` - -```smart header="The insertion order is used" -The iteration goes in the same order as the values were inserted. `Map` preserves this order, unlike a regular `Object`. -``` - -Besides that, `Map` has a built-in `forEach` method, similar to `Array`: - -```js -// runs the function for each (key, value) pair -recipeMap.forEach( (value, key, map) => { - alert(`${key}: ${value}`); // cucumber: 500 etc -}); -``` - - -## Set - -A `Set` is a collection of values, where each value may occur only once. - -Its main methods are: - -- `new Set(iterable)` -- creates the set, optionally from an array of values (any iterable will do). -- `set.add(value)` -- adds a value, returns the set itself. -- `set.delete(value)` -- removes the value, returns `true` if `value` existed at the moment of the call, otherwise `false`. -- `set.has(value)` -- returns `true` if the value exists in the set, otherwise `false`. -- `set.clear()` -- removes everything from the set. -- `set.size` -- is the elements count. - -For example, we have visitors coming, and we'd like to remember everyone. But repeated visits should not lead to duplicates. A visitor must be "counted" only once. - -`Set` is just the right thing for that: - -```js run -let set = new Set(); - -let john = { name: "John" }; -let pete = { name: "Pete" }; -let mary = { name: "Mary" }; - -// visits, some users come multiple times -set.add(john); -set.add(pete); -set.add(mary); -set.add(john); -set.add(mary); - -// set keeps only unique values -alert( set.size ); // 3 - -for (let user of set) { - alert(user.name); // John (then Pete and Mary) -} -``` - -The alternative to `Set` could be an array of users, and the code to check for duplicates on every insertion using [arr.find](mdn:js/Array/find). But the performance would be much worse, because this method walks through the whole array checking every element. `Set` is much better optimized internally for uniqueness checks. - -## Iteration over Set - -We can loop over a set either with `for..of` or using `forEach`: - -```js run -let set = new Set(["oranges", "apples", "bananas"]); - -for (let value of set) alert(value); - -// the same with forEach: -set.forEach((value, valueAgain, set) => { - alert(value); -}); -``` - -Note the funny thing. The `forEach` function in the `Set` has 3 arguments: a value, then *again a value*, and then the target object. Indeed, the same value appears in the arguments twice. - -That's for compatibility with `Map` where `forEach` has three arguments. Looks a bit strange, for sure. But 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`. - -## WeakMap and WeakSet - -`WeakSet` is a special kind of `Set` that does not prevent JavaScript from removing its items from memory. `WeakMap` is the same thing for `Map`. - -As we know from the chapter , JavaScript engine stores a value in memory while it is reachable (and can potentially be used). - -For instance: -```js -let john = { name: "John" }; - -// the object can be accessed, john is the reference to it - -// overwrite the reference -john = null; - -*!* -// the object will be removed from memory -*/!* -``` - -Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory. - -For instance, if we put an object into an array, then while the array is alive, the object will be alive as well, even if there are no other references to it. - -Like this: - -```js -let john = { name: "John" }; - -let array = [ john ]; - -john = null; // overwrite the reference - -*!* -// john is stored inside the array, so it won't be garbage-collected -// we can get it as array[0] -*/!* -``` - -Or, if we use an object as the key in a regular `Map`, then while the `Map` exists, that object exists as well. It occupies memory and may not be garbage collected. - -For instance: - -```js -let john = { name: "John" }; - -let map = new Map(); -map.set(john, "..."); - -john = null; // overwrite the reference - -*!* -// john is stored inside the map, -// we can get it by using map.keys() -*/!* -``` - -`WeakMap/WeakSet` are fundamentally different in this aspect. They do not prevent garbage-collection of key objects. - -Let's explain it starting with `WeakMap`. - -The first difference from `Map` is that `WeakMap` keys must be objects, not primitive values: - -```js run -let weakMap = new WeakMap(); - -let obj = {}; - -weakMap.set(obj, "ok"); // works fine (object key) - -*!* -// can't use a string as the key -weakMap.set("test", "Whoops"); // Error, because "test" is not an object -*/!* -``` - -Now, if we use an object as the key in it, and there are no other references to that object -- it will be removed from memory (and from the map) automatically. - -```js -let john = { name: "John" }; - -let weakMap = new WeakMap(); -weakMap.set(john, "..."); - -john = null; // overwrite the reference - -// john is removed from memory! -``` - -Compare it with the regular `Map` example above. Now if `john` only exists as the key of `WeakMap` -- it is to be automatically deleted. - -`WeakMap` does not support iteration and methods `keys()`, `values()`, `entries()`, so there's no way to get all keys or values from it. - -`WeakMap` has only the following methods: - -- `weakMap.get(key)` -- `weakMap.set(key, value)` -- `weakMap.delete(key)` -- `weakMap.has(key)` - -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 `WeakMap` as a whole are not supported. - -Now where do we need such thing? - -The idea of `WeakMap` is that we can store something for an object that should exist only while the object exists. But we do not force the object to live by the mere fact that we store something for it. - -```js -weakMap.set(john, "secret documents"); -// if john dies, secret documents will be destroyed automatically -``` - -That's useful for situations when we have a main storage for the objects somewhere and need to keep additional information, that is only relevant while the object lives. - -Let's look at an example. - -For instance, we have code that keeps a visit count for each user. The information is stored in a map: a user is the key and the visit count is the value. When a user leaves, we don't want to store their visit count anymore. - -One way would be to keep track of users, and when they leave -- clean up the map manually: - -```js run -let john = { name: "John" }; - -// map: user => visits count -let visitsCountMap = new Map(); - -// john is the key for the map -visitsCountMap.set(john, 123); - -// now john leaves us, we don't need him anymore -john = null; - -*!* -// but it's still in the map, we need to clean it! -*/!* -alert( visitsCountMap.size ); // 1 -// and john is also in the memory, because Map uses it as the key -``` - -Another way would be to use `WeakMap`: - -```js -let john = { name: "John" }; - -let visitsCountMap = new WeakMap(); - -visitsCountMap.set(john, 123); - -// now john leaves us, we don't need him anymore -john = null; - -// there are no references except WeakMap, -// so the object is removed both from the memory and from visitsCountMap automatically -``` - -With a regular `Map`, cleaning up after a user has left becomes a tedious task: we not only need to remove the user from its main storage (be it a variable or an array), but also need to clean up the additional stores like `visitsCountMap`. And it can become cumbersome in more complex cases when users are managed in one place of the code and the additional structure is in another place and is getting no information about removals. - -```summary -`WeakMap` can make things simpler, because it is cleaned up automatically. The information in it like visits count in the example above lives only while the key object exists. -``` - -`WeakSet` behaves similarly: - -- It is analogous to `Set`, but we may only add objects to `WeakSet` (not primitives). -- An object exists in the set while it is reachable from somewhere else. -- Like `Set`, it supports `add`, `has` and `delete`, but not `size`, `keys()` and no iterations. - -For instance, we can use it to keep track of whether a message is read: - -```js -let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} -]; - -// fill it with array elements (3 items) -let unreadSet = new WeakSet(messages); - -// use unreadSet to see whether a message is unread -alert(unreadSet.has(messages[1])); // true - -// remove it from the set after reading -unreadSet.delete(messages[1]); // true - -// and when we shift our messages history, the set is cleaned up automatically -messages.shift(); - -*!* -// no need to clean unreadSet, it now has 2 items -*/!* -// (though technically we don't know for sure when the JS engine clears it) -``` - -The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterations, and inability to get all current content. That may appear inconvenient, but 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 - -Regular collections: -- `Map` -- is a collection of keyed values. - - The differences from a regular `Object`: - - - Any keys, objects can be keys. - - Iterates in the insertion order. - - Additional convenient methods, the `size` property. - -- `Set` -- is a collection of unique values. - - - Unlike an array, does not allow to reorder elements. - - Keeps the insertion order. - -Collections that allow garbage-collection: - -- `WeakMap` -- a variant of `Map` that allows only objects as keys and removes them once they become inaccessible by other means. - - - It does not support operations on the structure as a whole: no `size`, no `clear()`, no iterations. - -- `WeakSet` -- is a variant of `Set` that only stores objects and removes them once they become inaccessible by other means. - - - Also does not support `size/clear()` and iterations. - -`WeakMap` and `WeakSet` are used as "secondary" data structures in addition to the "main" object storage. Once the object is removed from the main storage, if it is only found in the `WeakMap/WeakSet`, it will be cleaned up automatically. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js b/1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/solution.js rename to 1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/solution.js diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js b/1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/_js.view/test.js rename to 1-js/05-data-types/07-map-set/01-array-unique-map/_js.view/test.js diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/solution.md b/1-js/05-data-types/07-map-set/01-array-unique-map/solution.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/solution.md rename to 1-js/05-data-types/07-map-set/01-array-unique-map/solution.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md b/1-js/05-data-types/07-map-set/01-array-unique-map/task.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/01-array-unique-map/task.md rename to 1-js/05-data-types/07-map-set/01-array-unique-map/task.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js b/1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/solution.js rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/solution.js diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js b/1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/_js.view/test.js rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/_js.view/test.js diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/solution.md rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/task.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/02-filter-anagrams/task.md rename to 1-js/05-data-types/07-map-set/02-filter-anagrams/task.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md b/1-js/05-data-types/07-map-set/03-iterable-keys/solution.md similarity index 100% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/solution.md rename to 1-js/05-data-types/07-map-set/03-iterable-keys/solution.md diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md b/1-js/05-data-types/07-map-set/03-iterable-keys/task.md similarity index 63% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md rename to 1-js/05-data-types/07-map-set/03-iterable-keys/task.md index b1ccbd0a..25c74bfc 100644 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/03-iterable-keys/task.md +++ b/1-js/05-data-types/07-map-set/03-iterable-keys/task.md @@ -4,9 +4,9 @@ importance: 5 # Iterable keys -We want to get an array of `map.keys()` and go on working with it (apart from the map itself). +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`. -But there's a problem: +But that doesn't work: ```js run let map = new Map(); diff --git a/1-js/05-data-types/07-map-set/article.md b/1-js/05-data-types/07-map-set/article.md new file mode 100644 index 00000000..c4d7c21a --- /dev/null +++ b/1-js/05-data-types/07-map-set/article.md @@ -0,0 +1,324 @@ + +# Map and Set + +Now we've learned about the following complex data structures: + +- Objects for storing keyed collections. +- Arrays for storing ordered collections. + +But that's not enough for real life. That's why `Map` and `Set` also exist. + +## Map + +[Map](mdn:js/Map) is a collection of keyed data items, just like an `Object`. But the main difference is that `Map` allows keys of any type. + +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. + +For instance: + +```js run +let map = new Map(); + +map.set('1', 'str1'); // a string key +map.set(1, 'num1'); // a numeric key +map.set(true, 'bool1'); // a boolean key + +// remember the regular Object? it would convert keys to string +// Map keeps the type, so these two are different: +alert( map.get(1) ); // 'num1' +alert( map.get('1') ); // 'str1' + +alert( map.size ); // 3 +``` + +As we can see, unlike objects, keys are not converted to strings. Any type of key is possible. + +**Map can also use objects as keys.** + +For instance: + +```js run +let john = { name: "John" }; + +// for every user, let's store their visits count +let visitsCountMap = new Map(); + +// john is the key for the map +visitsCountMap.set(john, 123); + +alert( visitsCountMap.get(john) ); // 123 +``` + +Using objects as keys is one of most notable and important `Map` features. For string keys, `Object` can be fine, but not for object keys. + +Let's try: + +```js run +let john = { name: "John" }; + +let visitsCountObj = {}; // try to use an object + +visitsCountObj[john] = 123; // try to use john object as the key + +*!* +// That's what got written! +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. + +```smart header="How `Map` compares keys" +To test keys for equivalence, `Map` uses the algorithm [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). It is roughly the same as strict equality `===`, but the difference is that `NaN` is considered equal to `NaN`. So `NaN` can be used as the key as well. + +This algorithm can't be changed or customized. +``` + +````smart header="Chaining" +Every `map.set` call returns the map itself, so we can "chain" the calls: + +```js +map.set('1', 'str1') + .set(1, 'num1') + .set(true, 'bool1'); +``` +```` + + +## Iteration over Map + +For looping over a `map`, there are 3 methods: + +- `map.keys()` -- returns an iterable for keys, +- `map.values()` -- returns an iterable for values, +- `map.entries()` -- returns an iterable for entries `[key, value]`, it's used by default in `for..of`. + +For instance: + +```js run +let recipeMap = new Map([ + ['cucumber', 500], + ['tomatoes', 350], + ['onion', 50] +]); + +// iterate over keys (vegetables) +for (let vegetable of recipeMap.keys()) { + alert(vegetable); // cucumber, tomatoes, onion +} + +// iterate over values (amounts) +for (let amount of recipeMap.values()) { + alert(amount); // 500, 350, 50 +} + +// iterate over [key, value] entries +for (let entry of recipeMap) { // the same as of recipeMap.entries() + alert(entry); // cucumber,500 (and so on) +} +``` + +```smart header="The insertion order is used" +The iteration goes in the same order as the values were inserted. `Map` preserves this order, unlike a regular `Object`. +``` + +Besides that, `Map` has a built-in `forEach` method, similar to `Array`: + +```js +// runs the function for each (key, value) pair +recipeMap.forEach( (value, key, map) => { + alert(`${key}: ${value}`); // cucumber: 500 etc +}); +``` + +## Object.entries: Map from Object + +When a `Map` is created, we can pass an array (or another iterable) with key/value pairs for initialization, like this: + +```js run +// array of [key, value] pairs +let map = new Map([ + ['1', 'str1'], + [1, 'num1'], + [true, 'bool1'] +]); + +alert( map.get('1') ); // str1 +``` + +If we have a plain object, and we'd like to create a `Map` from it, then we can use built-in method [Object.entries(obj)](mdn:js/Object/entries) that returns an array of key/value pairs for an object exactly in that format. + +So we can create a map from an object like this: + +```js run +let obj = { + name: "John", + age: 30 +}; + +*!* +let map = new Map(Object.entries(obj)); +*/!* + +alert( map.get('name') ); // John +``` + +Here, `Object.entries` returns the array of key/value pairs: `[ ["name","John"], ["age", 30] ]`. That's what `Map` needs. + + +## Object.fromEntries: Object from Map + +We've just seen how to create `Map` from a plain object with `Object.entries(obj)`. + +There's `Object.fromEntries` method that does the reverse: given an array of `[key, value]` pairs, it creates an object from them: + +```js run +let prices = Object.fromEntries([ + ['banana', 1], + ['orange', 2], + ['meat', 4] +]); + +// now prices = { banana: 1, orange: 2, meat: 4 } + +alert(prices.orange); // 2 +``` + +We can use `Object.fromEntries` to get an plain object from `Map`. + +E.g. we store the data in a `Map`, but we need to pass it to a 3rd-party code that expects a plain object. + +Here we go: + +```js run +let map = new Map(); +map.set('banana', 1); +map.set('orange', 2); +map.set('meat', 4); + +*!* +let obj = Object.fromEntries(map.entries()); // make a plain object (*) +*/!* + +// done! +// obj = { banana: 1, orange: 2, meat: 4 } + +alert(obj.orange); // 2 +``` + +A call to `map.entries()` returns an array of key/value pairs, exactly in the right format for `Object.fromEntries`. + +We could also make line `(*)` shorter: +```js +let obj = Object.fromEntries(map); // omit .entries() +``` + +That's the same, because `Object.fromEntries` expects an iterable object as the argument. Not necessarily an array. And the standard iteration for `map` returns same key/value pairs as `map.entries()`. So we get a plain object with same key/values as the `map`. + +## Set + +A `Set` is a special type collection - "set of values" (without keys), where each value may occur only once. + +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. + +The main feature is that repeated calls of `set.add(value)` with the same value don't do anything. That's the reason why each value appears in a `Set` only once. + +For example, we have visitors coming, and we'd like to remember everyone. But repeated visits should not lead to duplicates. A visitor must be "counted" only once. + +`Set` is just the right thing for that: + +```js run +let set = new Set(); + +let john = { name: "John" }; +let pete = { name: "Pete" }; +let mary = { name: "Mary" }; + +// visits, some users come multiple times +set.add(john); +set.add(pete); +set.add(mary); +set.add(john); +set.add(mary); + +// set keeps only unique values +alert( set.size ); // 3 + +for (let user of set) { + alert(user.name); // John (then Pete and Mary) +} +``` + +The alternative to `Set` could be an array of users, and the code to check for duplicates on every insertion using [arr.find](mdn:js/Array/find). But the performance would be much worse, because this method walks through the whole array checking every element. `Set` is much better optimized internally for uniqueness checks. + +## Iteration over Set + +We can loop over a set either with `for..of` or using `forEach`: + +```js run +let set = new Set(["oranges", "apples", "bananas"]); + +for (let value of set) alert(value); + +// the same with forEach: +set.forEach((value, valueAgain, set) => { + alert(value); +}); +``` + +Note the funny thing. The 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. + +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`. + +## Summary + +`Map` -- is a collection of keyed values. + +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. + +The differences from a regular `Object`: + +- Any keys, objects can be keys. +- Additional convenient methods, the `size` property. + +`Set` -- is a collection of unique values. + +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. + +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/07-map-set-weakmap-weakset/04-recipients-read/solution.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md similarity index 52% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md rename to 1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md index ce56f593..f0c6ed45 100644 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/solution.md +++ b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/solution.md @@ -1,10 +1,10 @@ -The sane choice here is a `WeakSet`: +Let's store read messages in `WeakSet`: ```js let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} ]; let readMessages = new WeakSet(); @@ -27,9 +27,9 @@ messages.shift(); The `WeakSet` allows to store a set of messages and easily check for the existance of a message in it. -It cleans up itself automatically. The tradeoff is that we can't iterate over it. We can't get "all read messages" directly. But we can do it by iterating over all messages and filtering those that are in the set. +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. -P.S. Adding a property of our own to each message may be dangerous if messages are managed by someone else's code, but we can make it a symbol to evade conflicts. +Another, different solution could be to add a property like `message.isRead=true` to a message after it's read. As messages objects are managed by another code, that's generally discouraged, but we can use a symbolic property to avoid conflicts. Like this: ```js @@ -38,4 +38,6 @@ let isRead = Symbol("isRead"); messages[0][isRead] = true; ``` -Now even if someone else's code uses `for..in` loop for message properties, our secret flag won't appear. +Now third-party code probably won't see our extra property. + +Although symbols allow to lower the probability of problems, using `WeakSet` is better from the architectural point of view. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md similarity index 68% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md rename to 1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md index 7ec1faf1..c3d3bbc0 100644 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/04-recipients-read/task.md +++ b/1-js/05-data-types/08-weakmap-weakset/01-recipients-read/task.md @@ -8,9 +8,9 @@ There's an array of messages: ```js let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} ]; ``` @@ -20,4 +20,4 @@ Now, which data structure you could use to store information whether the message P.S. When a message is removed from `messages`, it should disappear from your structure as well. -P.P.S. We shouldn't modify message objects directly. If they are managed by someone else's code, then adding extra properties to them may have bad consequences. +P.P.S. We shouldn't modify message objects, add our properties to them. As they are managed by someone else's code, that may lead to bad consequences. diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/solution.md similarity index 61% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md rename to 1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/solution.md index 7f387b4d..2af0547c 100644 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/solution.md +++ b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/solution.md @@ -3,9 +3,9 @@ To store a date, we can use `WeakMap`: ```js let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} ]; let readMap = new WeakMap(); diff --git a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/task.md similarity index 54% rename from 1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md rename to 1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/task.md index 22b51a38..8e341c18 100644 --- a/1-js/05-data-types/07-map-set-weakmap-weakset/05-recipients-when-read/task.md +++ b/1-js/05-data-types/08-weakmap-weakset/02-recipients-when-read/task.md @@ -8,12 +8,14 @@ There's an array of messages as in the [previous task](info:task/recipients-read ```js let messages = [ - {text: "Hello", from: "John"}, - {text: "How goes?", from: "John"}, - {text: "See you soon", from: "Alice"} + {text: "Hello", from: "John"}, + {text: "How goes?", from: "John"}, + {text: "See you soon", from: "Alice"} ]; ``` The question now is: which data structure you'd suggest to store the information: "when the message was read?". -In the previous task we only needed to store the "yes/no" fact. Now we need to store the date and it, once again, should disappear if the message is gone. +In the previous task we only needed to store the "yes/no" fact. Now we need to store the date, and it should only remain in memory until the message is garbage collected. + +P.S. Dates can be stored as objects of built-in `Date` class, that we'll cover later. diff --git a/1-js/05-data-types/08-weakmap-weakset/article.md b/1-js/05-data-types/08-weakmap-weakset/article.md new file mode 100644 index 00000000..11ff9d5e --- /dev/null +++ b/1-js/05-data-types/08-weakmap-weakset/article.md @@ -0,0 +1,289 @@ +# 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). + +For instance: +```js +let john = { name: "John" }; + +// the object can be accessed, john is the reference to it + +// overwrite the reference +john = null; + +*!* +// the object will be removed from memory +*/!* +``` + +Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory. + +For instance, if we put an object into an array, then while the array is alive, the object will be alive as well, even if there are no other references to it. + +Like this: + +```js +let john = { name: "John" }; + +let array = [ john ]; + +john = null; // overwrite the reference + +*!* +// john is stored inside the array, so it won't be garbage-collected +// we can get it as array[0] +*/!* +``` + +Similar to that, if we use an object as the key in a regular `Map`, then while the `Map` exists, that object exists as well. It occupies memory and may not be garbage collected. + +For instance: + +```js +let john = { name: "John" }; + +let map = new Map(); +map.set(john, "..."); + +john = null; // overwrite the reference + +*!* +// john is stored inside the map, +// we can get it by using map.keys() +*/!* +``` + +`WeakMap` is fundamentally different in this aspect. It doesn't prevent garbage-collection of key objects. + +Let's see what it means on examples. + +## WeakMap + +The first difference from `Map` is that `WeakMap` keys must be objects, not primitive values: + +```js run +let weakMap = new WeakMap(); + +let obj = {}; + +weakMap.set(obj, "ok"); // works fine (object key) + +*!* +// can't use a string as the key +weakMap.set("test", "Whoops"); // Error, because "test" is not an object +*/!* +``` + +Now, if we use an object as the key in it, and there are no other references to that object -- it will be removed from memory (and from the map) automatically. + +```js +let john = { name: "John" }; + +let weakMap = new WeakMap(); +weakMap.set(john, "..."); + +john = null; // overwrite the reference + +// john is removed from memory! +``` + +Compare it with the regular `Map` example above. Now if `john` only exists as the key of `WeakMap` -- it will be automatically deleted from the map (and memory). + +`WeakMap` does not support iteration and methods `keys()`, `values()`, `entries()`, so there's no way to get all keys or values from it. + +`WeakMap` has only the following methods: + +- `weakMap.get(key)` +- `weakMap.set(key, value)` +- `weakMap.delete(key)` +- `weakMap.has(key)` + +Why such a limitation? That's for technical reasons. If an object has lost all other references (like `john` in the code above), then it is to be garbage-collected automatically. But technically it's not exactly specified *when the cleanup happens*. + +The JavaScript engine decides that. It may choose to perform the memory cleanup immediately or to wait and do the cleaning later when more deletions happen. So, technically the current element count of a `WeakMap` is not known. The engine may have cleaned it up or not, or did it partially. For that reason, methods that access all keys/values are not supported. + +Now where do we need such data structure? + +## Use case: additional data + +The main area of application for `WeakMap` is an *additional data storage*. + +If we're working with an object that "belongs" to another code, maybe even a third-party library, and would like to store some data associated with it, that should only exist while the object is alive - then `WeakMap` is exactly what's needed. + +We put the data to a `WeakMap`, using the object as the key, and when the object is garbage collected, that data will automatically disappear as well. + +```js +weakMap.set(john, "secret documents"); +// if john dies, secret documents will be destroyed automatically +``` + +Let's look at an example. + +For instance, we have code that keeps a visit count for users. The information is stored in a map: a user object is the key and the visit count is the value. When a user leaves (its object gets garbage collected), we don't want to store their visit count anymore. + +Here's an example of a counting function with `Map`: + +```js +// 📁 visitsCount.js +let visitsCountMap = new Map(); // map: user => visits count + +// increase the visits count +function countUser(user) { + let count = visitsCountMap.get(user) || 0; + visitsCountMap.set(user, count + 1); +} +``` + +And here's another part of the code, maybe another file using it: + +```js +// 📁 main.js +let john = { name: "John" }; + +countUser(john); // count his visits +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`. + +We need to clean `visitsCountMap` when we remove users, otherwise it will grow in memory indefinitely. Such cleaning can become a tedious task in complex architectures. + +We can avoid it by switching to `WeakMap` instead: + +```js +// 📁 visitsCount.js +let visitsCountMap = new WeakMap(); // weakmap: user => visits count + +// increase the visits count +function countUser(user) { + let count = visitsCountMap.get(user) || 0; + visitsCountMap.set(user, count + 1); +} +``` + +Now we don't have to clean `visitsCountMap`. After `john` object becomes unreachable by all means except as a key of `WeakMap`, it gets removed from memory, along with the information by that key from `WeakMap`. + +## Use case: caching + +Another common example is caching: when a function result should be remembered ("cached"), so that future calls on the same object reuse it. + +We can use `Map` to store results, like this: + +```js run +// 📁 cache.js +let cache = new Map(); + +// calculate and remember the result +function process(obj) { + if (!cache.has(obj)) { + let result = /* calculations of the result for */ obj; + + cache.set(obj, result); + } + + return cache.get(obj); +} + +*!* +// Now we use process() in another file: +*/!* + +// 📁 main.js +let obj = {/* let's say we have an object */}; + +let result1 = process(obj); // calculated + +// ...later, from another place of the code... +let result2 = process(obj); // remembered result taken from cache + +// ...later, when the object is not needed any more: +obj = null; + +alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!) +``` + +For multiple calls of `process(obj)` with the same object, it only calculates the result the first time, and then just takes it from `cache`. The downside is that we need to clean `cache` when the object is not needed any more. + +If we replace `Map` with `WeakMap`, then this problem disappears: the cached result will be removed from memory automatically after the object gets garbage collected. + +```js run +// 📁 cache.js +*!* +let cache = new WeakMap(); +*/!* + +// calculate and remember the result +function process(obj) { + if (!cache.has(obj)) { + let result = /* calculate the result for */ obj; + + cache.set(obj, result); + } + + return cache.get(obj); +} + +// 📁 main.js +let obj = {/* some object */}; + +let result1 = process(obj); +let result2 = process(obj); + +// ...later, when the object is not needed any more: +obj = null; + +// Can't get cache.size, as it's a WeakMap, +// but it's 0 or soon be 0 +// When obj gets garbage collected, cached data will be removed as well +``` + +## WeakSet + +`WeakSet` behaves similarly: + +- It is analogous to `Set`, but we may only add objects to `WeakSet` (not primitives). +- An object exists in the set while it is reachable from somewhere else. +- Like `Set`, it supports `add`, `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. + +For instance, we can add users to `WeakSet` to keep track of those who visited our site: + +```js run +let visitedSet = new WeakSet(); + +let john = { name: "John" }; +let pete = { name: "Pete" }; +let mary = { name: "Mary" }; + +visitedSet.add(john); // John visited us +visitedSet.add(pete); // Then Pete +visitedSet.add(john); // John again + +// visitedSet has 2 users now + +// check if John visited? +alert(visitedSet.has(john)); // true + +// check if Mary visited? +alert(visitedSet.has(mary)); // false + +john = null; + +// visitedSet will be cleaned automatically +``` + +The most notable limitation of `WeakMap` and `WeakSet` is the absence of iterations, and inability to get all current content. That may appear inconvenient, but does not prevent `WeakMap/WeakSet` from doing their main job -- be an "additional" storage of data for objects which are stored/managed at another place. + +## Summary + +`WeakMap` is `Map`-like collection that allows only objects as keys and removes them together with associated value once they become inaccessible by other means. + +`WeakSet` 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. + +`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. diff --git a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png deleted file mode 100644 index 80daca3d..00000000 Binary files a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex.png and /dev/null differ diff --git a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png b/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png deleted file mode 100644 index 66982a9f..00000000 Binary files a/1-js/05-data-types/09-destructuring-assignment/destructuring-complex@2x.png and /dev/null differ diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/solution.js rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/solution.js diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/_js.view/test.js rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/_js.view/test.js diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/solution.md b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/solution.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/solution.md rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/solution.md diff --git a/1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md b/1-js/05-data-types/09-keys-values-entries/01-sum-salaries/task.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/01-sum-salaries/task.md rename to 1-js/05-data-types/09-keys-values-entries/01-sum-salaries/task.md diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js b/1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/solution.js rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/solution.js diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js b/1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/_js.view/test.js rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/_js.view/test.js diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/solution.md b/1-js/05-data-types/09-keys-values-entries/02-count-properties/solution.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/solution.md rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/solution.md diff --git a/1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md b/1-js/05-data-types/09-keys-values-entries/02-count-properties/task.md similarity index 100% rename from 1-js/05-data-types/08-keys-values-entries/02-count-properties/task.md rename to 1-js/05-data-types/09-keys-values-entries/02-count-properties/task.md diff --git a/1-js/05-data-types/08-keys-values-entries/article.md b/1-js/05-data-types/09-keys-values-entries/article.md similarity index 58% rename from 1-js/05-data-types/08-keys-values-entries/article.md rename to 1-js/05-data-types/09-keys-values-entries/article.md index 66ca3ca9..ca0be768 100644 --- a/1-js/05-data-types/08-keys-values-entries/article.md +++ b/1-js/05-data-types/09-keys-values-entries/article.md @@ -1,11 +1,11 @@ # Object.keys, values, entries -Let's step away from the individual data structures and talk about the iterations over them. +Let's step away from the individual data structures and talk about the iterations over them. In the previous chapter we saw methods `map.keys()`, `map.values()`, `map.entries()`. -These methods are generic, there is a common agreement to use them for data structures. If we ever create a data structure of our own, we should implement them too. +These methods are generic, there is a common agreement to use them for data structures. If we ever create a data structure of our own, we should implement them too. They are supported for: @@ -23,7 +23,7 @@ For plain objects, the following methods are available: - [Object.values(obj)](mdn:js/Object/values) -- returns an array of values. - [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of `[key, value]` pairs. -...But please note the distinctions (compared to map for example): +Please note the distinctions (compared to map for example): | | Map | Object | |-------------|------------------|--------------| @@ -32,7 +32,7 @@ For plain objects, the following methods are available: The first difference is that we have to call `Object.keys(obj)`, and not `obj.keys()`. -Why so? The main reason is flexibility. Remember, objects are a base of all complex structures in JavaScript. So we may have an object of our own like `order` that implements its own `order.values()` method. And we still can call `Object.values(order)` on it. +Why so? The main reason is flexibility. Remember, objects are a base of all complex structures in JavaScript. So we may have an object of our own like `data` that implements its own `data.values()` method. And we still can call `Object.values(data)` on it. The second difference is that `Object.*` methods return "real" array objects, not just an iterable. That's mainly for historical reasons. @@ -63,8 +63,42 @@ for (let value of Object.values(user)) { } ``` -## Object.keys/values/entries ignore symbolic properties - +```warn header="Object.keys/values/entries ignore symbolic properties" Just like a `for..in` loop, these methods ignore properties that use `Symbol(...)` as keys. -Usually that's convenient. But if we want symbolic keys too, then there's a separate method [Object.getOwnPropertySymbols](mdn:js/Object/getOwnPropertySymbols) that returns an array of only symbolic keys. Also, the method [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) returns *all* keys. +Usually that's convenient. But if we want symbolic keys too, then there's a separate method [Object.getOwnPropertySymbols](mdn:js/Object/getOwnPropertySymbols) that returns an array of only symbolic keys. Also, there exist a method [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) that returns *all* keys. +``` + + +## Transforming objects + +Objects lack many methods that exist for arrays, e.g. `map`, `filter` and others. + +If we'd like to apply them, then we can use `Object.entries` followed `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`. +3. Use `Object.fromEntries(array)` on the resulting array to turn it back into an object. + +For example, we have an object with prices, and would like to double them: + +```js run +let prices = { + banana: 1, + orange: 2, + meat: 4, +}; + +*!* +let doublePrices = Object.fromEntries( + // convert to array, map, and then fromEntries gives back the object + Object.entries(prices).map(([key, value]) => [key, value * 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. diff --git a/1-js/05-data-types/10-date/1-new-date/solution.md b/1-js/05-data-types/10-date/1-new-date/solution.md deleted file mode 100644 index eb271a91..00000000 --- a/1-js/05-data-types/10-date/1-new-date/solution.md +++ /dev/null @@ -1,8 +0,0 @@ -The `new Date` constructor uses the local time zone by default. So the only important thing to remember is that months start from zero. - -So February has number 1. - -```js run -let d = new Date(2012, 1, 20, 3, 12); -alert( d ); -``` diff --git a/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/solution.md b/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/solution.md similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/1-destruct-user/solution.md rename to 1-js/05-data-types/10-destructuring-assignment/1-destruct-user/solution.md diff --git a/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/task.md b/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/task.md similarity index 76% rename from 1-js/05-data-types/09-destructuring-assignment/1-destruct-user/task.md rename to 1-js/05-data-types/10-destructuring-assignment/1-destruct-user/task.md index b2213323..b68db5c5 100644 --- a/1-js/05-data-types/09-destructuring-assignment/1-destruct-user/task.md +++ b/1-js/05-data-types/10-destructuring-assignment/1-destruct-user/task.md @@ -17,9 +17,9 @@ Write the destructuring assignment that reads: - `name` property into the variable `name`. - `years` property into the variable `age`. -- `isAdmin` property into the variable `isAdmin` (false if absent) +- `isAdmin` property into the variable `isAdmin` (false, if no such property) -The values after the assignment should be: +Here's an example of the values after your assignment: ```js let user = { name: "John", years: 30 }; diff --git a/1-js/05-data-types/09-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 similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/solution.js rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/solution.js diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/_js.view/test.js rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/_js.view/test.js diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/solution.md b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/solution.md similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/solution.md rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/solution.md diff --git a/1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md b/1-js/05-data-types/10-destructuring-assignment/6-max-salary/task.md similarity index 100% rename from 1-js/05-data-types/09-destructuring-assignment/6-max-salary/task.md rename to 1-js/05-data-types/10-destructuring-assignment/6-max-salary/task.md diff --git a/1-js/05-data-types/09-destructuring-assignment/article.md b/1-js/05-data-types/10-destructuring-assignment/article.md similarity index 75% rename from 1-js/05-data-types/09-destructuring-assignment/article.md rename to 1-js/05-data-types/10-destructuring-assignment/article.md index ff56c38e..dec2535a 100644 --- a/1-js/05-data-types/09-destructuring-assignment/article.md +++ b/1-js/05-data-types/10-destructuring-assignment/article.md @@ -2,9 +2,11 @@ The two most used data structures in JavaScript are `Object` and `Array`. -Objects allow us to pack many pieces of information into a single entity and arrays allow us to store ordered collections. So we can make an object or an array and handle it as a single entity, or maybe pass it to a function call. +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. -*Destructuring assignment* is a special syntax that allows us to "unpack" arrays or objects into a bunch of variables, as sometimes they are more convenient. Destructuring also works great with complex functions that have a lot of parameters, default values, and soon we'll see how these are handled too. +But when we pass those to a function, it may need not an object/array as a whole, but rather 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. ## Array destructuring @@ -16,6 +18,8 @@ let arr = ["Ilya", "Kantor"] *!* // destructuring assignment +// sets firstName = arr[0] +// and surname = arr[1] let [firstName, surname] = arr; */!* @@ -42,19 +46,19 @@ let surname = arr[1]; ``` ```` -````smart header="Ignore first elements" +````smart header="Ignore elements using commas" Unwanted elements of the array can also be thrown away via an extra comma: ```js run *!* -// first and second elements are not needed -let [, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; +// second element is not needed +let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; */!* alert( title ); // Consul ``` -In the code above, although the first and second elements of the array are skipped, the third one is assigned to `title`, and the rest are also skipped. +In the code above, the second element of the array is skipped, the third one is assigned to `title`, and the rest of the array items is also skipped (as there are no variables for them). ```` ````smart header="Works with any iterable on the right-side" @@ -111,7 +115,7 @@ user.set("name", "John"); user.set("age", "30"); *!* -for (let [key, value] of user.entries()) { +for (let [key, value] of user) { */!* alert(`${key}:${value}`); // name:John, then age:30 } @@ -209,7 +213,7 @@ alert(height); // 200 Properties `options.title`, `options.width` and `options.height` are assigned to the corresponding variables. The order does not matter. This works too: ```js -// changed the order of properties in let {...} +// changed the order in let {...} let {height, width, title} = { title: "Menu", height: 200, width: 100 } ``` @@ -258,7 +262,7 @@ alert(height); // 200 Just like with arrays or function parameters, default values can be any expressions or even function calls. They will be evaluated if the value is not provided. -The code below asks for width, but not the title. +In the code below `prompt` asks for `width`, but not for `title`: ```js run let options = { @@ -270,7 +274,7 @@ let {width = prompt("width?"), title = prompt("title?")} = options; */!* alert(title); // Menu -alert(width); // (whatever you the result of prompt is) +alert(width); // (whatever the result of prompt is) ``` We also can combine both the colon and equality: @@ -289,11 +293,26 @@ alert(w); // 100 alert(h); // 200 ``` -### The rest operator +If we have a complex object with many properties, we can extract only what we need: + +```js run +let options = { + title: "Menu", + width: 100, + height: 200 +}; + +// only extract title as a variable +let { title } = options; + +alert(title); // Menu +``` + +### The rest pattern "..." What if the object has more properties than we have variables? Can we take some and then assign the "rest" somewhere? -The specification for using the rest operator (three dots) here is almost in the standard, but most browsers do not support it yet. +We can use the rest pattern, just like we did with arrays. It's not supported by some older browsers (IE, use Babel to polyfill it), but works in modern ones. It looks like this: @@ -305,6 +324,8 @@ let options = { }; *!* +// title = property named title +// rest = object with the rest of properties let {title, ...rest} = options; */!* @@ -313,10 +334,8 @@ alert(rest.height); // 200 alert(rest.width); // 100 ``` - - -````smart header="Gotcha without `let`" -In the examples above variables were declared right before the assignment: `let {…} = {…}`. Of course, we could use existing variables too. But there's a catch. +````smart header="Gotcha if there's no `let`" +In the examples above variables were declared right in the assignment: `let {…} = {…}`. Of course, we could use existing variables too, without `let`. But there's a catch. This won't work: ```js run @@ -337,7 +356,9 @@ The problem is that JavaScript treats `{...}` in the main code flow (not inside } ``` -To show JavaScript that it's not a code block, we can wrap the whole assignment in parentheses `(...)`: +So here JavaScript assumes that we have a code block, that's why there's an error. We have destructuring instead. + +To show JavaScript that it's not a code block, we can wrap the expression in parentheses `(...)`: ```js run let title, width, height; @@ -347,14 +368,13 @@ let title, width, height; alert( title ); // Menu ``` - ```` ## Nested destructuring -If an object or an array contain other objects and arrays, we can use more complex left-side patterns to extract deeper portions. +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: +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: ```js run let options = { @@ -363,10 +383,10 @@ let options = { height: 200 }, items: ["Cake", "Donut"], - extra: true // something extra that we will not destruct + extra: true }; -// destructuring assignment on multiple lines for clarity +// destructuring assignment split in multiple lines for clarity let { size: { // put size here width, @@ -383,25 +403,17 @@ alert(item1); // Cake alert(item2); // Donut ``` -The whole `options` object except `extra` that was not mentioned, is assigned to corresponding variables. +The whole `options` object except `extra` that was not mentioned, is assigned to corresponding variables: -Note that `size` and `items` itself is not destructured. - -![](destructuring-complex.png) +![](destructuring-complex.svg) Finally, we have `width`, `height`, `item1`, `item2` and `title` from the default value. -That often happens with destructuring assignments. We have a complex object with many properties and want to extract only what we need. - -Even here it happens: -```js -// take size as a whole into a variable, ignore the rest -let { size } = options; -``` +Note that there are no variables for `size` and `items`, as we take their content instead. ## Smart function parameters -There are times when a function may have many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, items list and so on. +There are times when a function has many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, items list and so on. Here's a bad way to write such function: @@ -416,6 +428,7 @@ In real-life, the problem is how to remember the order of arguments. Usually IDE Like this? ```js +// undefined where default values are fine showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]) ``` @@ -467,29 +480,28 @@ function showMenu({ showMenu(options); ``` -The syntax is the same as for a destructuring assignment: +The full syntax is the same as for a destructuring assignment: ```js function({ - incomingProperty: parameterName = defaultValue + incomingProperty: varName = defaultValue ... }) ``` +Then, for an object of parameters, there will be a variable `varName` for property `incomingProperty`, with `defaultValue` by default. + Please note that such destructuring assumes that `showMenu()` does have an argument. If we want all values by default, then we should specify an empty object: ```js -showMenu({}); - +showMenu({}); // ok, all values are default showMenu(); // this would give an error ``` -We can fix this by making `{}` the default value for the whole destructuring thing: - +We can fix this by making `{}` the default value for the whole object of parameters: ```js run -// simplified parameters a bit for clarity -function showMenu(*!*{ title = "Menu", width = 100, height = 200 } = {}*/!*) { +function showMenu({ title = "Menu", width = 100, height = 200 }*!* = {}*/!*) { alert( `${title} ${width} ${height}` ); } @@ -501,14 +513,16 @@ In the code above, the whole arguments object is `{}` by default, so there's alw ## Summary - Destructuring assignment allows for instantly mapping an object or array onto many variables. -- The object syntax: +- The full object syntax: ```js - let {prop : varName = default, ...} = object + let {prop : varName = default, ...rest} = object ``` This means that property `prop` should go into the variable `varName` and, if no such property exists, then the `default` value should be used. -- The array syntax: + Object properties that have no mapping are copied to the `rest` object. + +- The full array syntax: ```js let [item1 = default, item2, ...rest] = array @@ -516,4 +530,4 @@ In the code above, the whole arguments object is `{}` by default, so there's alw The first item goes to `item1`; the second goes into `item2`, all the rest makes the array `rest`. -- For more complex cases, the left side must have the same structure as the right one. +- It's possible to extract data from nested arrays/objects, for that the left side must have the same structure as the right one. diff --git a/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg new file mode 100644 index 00000000..0b650020 --- /dev/null +++ b/1-js/05-data-types/10-destructuring-assignment/destructuring-complex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/05-data-types/11-date/1-new-date/solution.md b/1-js/05-data-types/11-date/1-new-date/solution.md new file mode 100644 index 00000000..9bb1d749 --- /dev/null +++ b/1-js/05-data-types/11-date/1-new-date/solution.md @@ -0,0 +1,8 @@ +The `new Date` constructor uses the local time zone. So the only important thing to remember is that months start from zero. + +So February has number 1. + +```js run +let d = new Date(2012, 1, 20, 3, 12); +alert( d ); +``` diff --git a/1-js/05-data-types/10-date/1-new-date/task.md b/1-js/05-data-types/11-date/1-new-date/task.md similarity index 100% rename from 1-js/05-data-types/10-date/1-new-date/task.md rename to 1-js/05-data-types/11-date/1-new-date/task.md diff --git a/1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js b/1-js/05-data-types/11-date/2-get-week-day/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/_js.view/solution.js rename to 1-js/05-data-types/11-date/2-get-week-day/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js b/1-js/05-data-types/11-date/2-get-week-day/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/_js.view/test.js rename to 1-js/05-data-types/11-date/2-get-week-day/_js.view/test.js diff --git a/1-js/05-data-types/10-date/2-get-week-day/solution.md b/1-js/05-data-types/11-date/2-get-week-day/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/solution.md rename to 1-js/05-data-types/11-date/2-get-week-day/solution.md diff --git a/1-js/05-data-types/10-date/2-get-week-day/task.md b/1-js/05-data-types/11-date/2-get-week-day/task.md similarity index 100% rename from 1-js/05-data-types/10-date/2-get-week-day/task.md rename to 1-js/05-data-types/11-date/2-get-week-day/task.md diff --git a/1-js/05-data-types/10-date/3-weekday/_js.view/solution.js b/1-js/05-data-types/11-date/3-weekday/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/_js.view/solution.js rename to 1-js/05-data-types/11-date/3-weekday/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/3-weekday/_js.view/test.js b/1-js/05-data-types/11-date/3-weekday/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/_js.view/test.js rename to 1-js/05-data-types/11-date/3-weekday/_js.view/test.js diff --git a/1-js/05-data-types/10-date/3-weekday/solution.md b/1-js/05-data-types/11-date/3-weekday/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/solution.md rename to 1-js/05-data-types/11-date/3-weekday/solution.md diff --git a/1-js/05-data-types/10-date/3-weekday/task.md b/1-js/05-data-types/11-date/3-weekday/task.md similarity index 100% rename from 1-js/05-data-types/10-date/3-weekday/task.md rename to 1-js/05-data-types/11-date/3-weekday/task.md diff --git a/1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js b/1-js/05-data-types/11-date/4-get-date-ago/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/4-get-date-ago/_js.view/solution.js rename to 1-js/05-data-types/11-date/4-get-date-ago/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js b/1-js/05-data-types/11-date/4-get-date-ago/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/4-get-date-ago/_js.view/test.js rename to 1-js/05-data-types/11-date/4-get-date-ago/_js.view/test.js diff --git a/1-js/05-data-types/10-date/4-get-date-ago/solution.md b/1-js/05-data-types/11-date/4-get-date-ago/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/4-get-date-ago/solution.md rename to 1-js/05-data-types/11-date/4-get-date-ago/solution.md diff --git a/1-js/05-data-types/10-date/4-get-date-ago/task.md b/1-js/05-data-types/11-date/4-get-date-ago/task.md similarity index 92% rename from 1-js/05-data-types/10-date/4-get-date-ago/task.md rename to 1-js/05-data-types/11-date/4-get-date-ago/task.md index 40dcd926..058d39c7 100644 --- a/1-js/05-data-types/10-date/4-get-date-ago/task.md +++ b/1-js/05-data-types/11-date/4-get-date-ago/task.md @@ -8,7 +8,7 @@ Create a function `getDateAgo(date, days)` to return the day of month `days` ago For instance, if today is 20th, then `getDateAgo(new Date(), 1)` should be 19th and `getDateAgo(new Date(), 2)` should be 18th. -Should also work over months/years reliably: +Should work reliably for `days=365` or more: ```js let date = new Date(2015, 0, 2); diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js b/1-js/05-data-types/11-date/5-last-day-of-month/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/_js.view/solution.js rename to 1-js/05-data-types/11-date/5-last-day-of-month/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js b/1-js/05-data-types/11-date/5-last-day-of-month/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/_js.view/test.js rename to 1-js/05-data-types/11-date/5-last-day-of-month/_js.view/test.js diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/solution.md b/1-js/05-data-types/11-date/5-last-day-of-month/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/solution.md rename to 1-js/05-data-types/11-date/5-last-day-of-month/solution.md diff --git a/1-js/05-data-types/10-date/5-last-day-of-month/task.md b/1-js/05-data-types/11-date/5-last-day-of-month/task.md similarity index 100% rename from 1-js/05-data-types/10-date/5-last-day-of-month/task.md rename to 1-js/05-data-types/11-date/5-last-day-of-month/task.md diff --git a/1-js/05-data-types/10-date/6-get-seconds-today/solution.md b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md similarity index 99% rename from 1-js/05-data-types/10-date/6-get-seconds-today/solution.md rename to 1-js/05-data-types/11-date/6-get-seconds-today/solution.md index 91903d90..a483afe9 100644 --- a/1-js/05-data-types/10-date/6-get-seconds-today/solution.md +++ b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md @@ -22,5 +22,5 @@ An alternative solution would be to get hours/minutes/seconds and convert them t function getSecondsToday() { let d = new Date(); return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); -}; +} ``` diff --git a/1-js/05-data-types/10-date/6-get-seconds-today/task.md b/1-js/05-data-types/11-date/6-get-seconds-today/task.md similarity index 100% rename from 1-js/05-data-types/10-date/6-get-seconds-today/task.md rename to 1-js/05-data-types/11-date/6-get-seconds-today/task.md diff --git a/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md b/1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/solution.md rename to 1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/solution.md diff --git a/1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md b/1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/task.md similarity index 100% rename from 1-js/05-data-types/10-date/7-get-seconds-to-tomorrow/task.md rename to 1-js/05-data-types/11-date/7-get-seconds-to-tomorrow/task.md diff --git a/1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js b/1-js/05-data-types/11-date/8-format-date-relative/_js.view/solution.js similarity index 100% rename from 1-js/05-data-types/10-date/8-format-date-relative/_js.view/solution.js rename to 1-js/05-data-types/11-date/8-format-date-relative/_js.view/solution.js diff --git a/1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js b/1-js/05-data-types/11-date/8-format-date-relative/_js.view/test.js similarity index 100% rename from 1-js/05-data-types/10-date/8-format-date-relative/_js.view/test.js rename to 1-js/05-data-types/11-date/8-format-date-relative/_js.view/test.js diff --git a/1-js/05-data-types/10-date/8-format-date-relative/solution.md b/1-js/05-data-types/11-date/8-format-date-relative/solution.md similarity index 100% rename from 1-js/05-data-types/10-date/8-format-date-relative/solution.md rename to 1-js/05-data-types/11-date/8-format-date-relative/solution.md diff --git a/1-js/05-data-types/10-date/8-format-date-relative/task.md b/1-js/05-data-types/11-date/8-format-date-relative/task.md similarity index 94% rename from 1-js/05-data-types/10-date/8-format-date-relative/task.md rename to 1-js/05-data-types/11-date/8-format-date-relative/task.md index 7b341ca2..4dc06737 100644 --- a/1-js/05-data-types/10-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.2016, 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/10-date/article.md b/1-js/05-data-types/11-date/article.md similarity index 87% rename from 1-js/05-data-types/10-date/article.md rename to 1-js/05-data-types/11-date/article.md index d4c71e57..4f80f752 100644 --- a/1-js/05-data-types/10-date/article.md +++ b/1-js/05-data-types/11-date/article.md @@ -29,18 +29,17 @@ To create a new `Date` object call `new Date()` with one of the following argume alert( Jan02_1970 ); ``` - The number of milliseconds that has passed since the beginning of 1970 is called a *timestamp*. + An integer number representing the number of milliseconds that has passed since the beginning of 1970 is called a *timestamp*. It's a lightweight numeric representation of a date. We can always create a date from a timestamp using `new Date(timestamp)` and convert the existing `Date` object to a timestamp using the `date.getTime()` method (see below). `new Date(datestring)` -: If there is a single argument, and it's a string, then it is parsed with the `Date.parse` algorithm (see below). - +: If there is a single argument, and it's a string, then it is parsed automatically. The algorithm is the same as `Date.parse` uses, we'll cover it later. ```js run let date = new Date("2017-01-26"); alert(date); - // The time portion of the date is assumed to be midnight GMT and + // The time is not set, so it's assumed to be midnight GMT and // is adjusted according to the timezone the code is run in // So the result could be // Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time) @@ -49,9 +48,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 two first arguments are obligatory. - - Note: +: 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 `month` count starts with `0` (Jan), up to `11` (Dec). @@ -74,7 +71,7 @@ To create a new `Date` object call `new Date()` with one of the following argume ## Access date components -There are many methods to access the year, month and so on from the `Date` object. But they can be easily remembered when categorized. +There are methods to access the year, month and so on from the `Date` object: [getFullYear()](mdn:js/Date/getFullYear) : Get the year (4 digits) @@ -133,12 +130,12 @@ Besides the given methods, there are two special ones that do not have a UTC-var The following methods allow to set date/time components: -- [`setFullYear(year [, month, date])`](mdn:js/Date/setFullYear) -- [`setMonth(month [, date])`](mdn:js/Date/setMonth) +- [`setFullYear(year, [month], [date])`](mdn:js/Date/setFullYear) +- [`setMonth(month, [date])`](mdn:js/Date/setMonth) - [`setDate(date)`](mdn:js/Date/setDate) -- [`setHours(hour [, min, sec, ms])`](mdn:js/Date/setHours) -- [`setMinutes(min [, sec, ms])`](mdn:js/Date/setMinutes) -- [`setSeconds(sec [, ms])`](mdn:js/Date/setSeconds) +- [`setHours(hour, [min], [sec], [ms])`](mdn:js/Date/setHours) +- [`setMinutes(min, [sec], [ms])`](mdn:js/Date/setMinutes) +- [`setSeconds(sec, [ms])`](mdn:js/Date/setSeconds) - [`setMilliseconds(ms)`](mdn:js/Date/setMilliseconds) - [`setTime(milliseconds)`](mdn:js/Date/setTime) (sets the whole date by milliseconds since 01.01.1970 UTC) @@ -217,21 +214,21 @@ The important side effect: dates can be subtracted, the result is their differen That can be used for time measurements: ```js run -let start = new Date(); // start counting +let start = new Date(); // start measuring time // do the job for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } -let end = new Date(); // done +let end = new Date(); // end measuring time alert( `The loop took ${end - start} ms` ); ``` ## Date.now() -If we only want to measure the difference, we don't need the `Date` object. +If we only want to measure time, we don't need the `Date` object. There's a special method `Date.now()` that returns the current timestamp. @@ -264,6 +261,8 @@ If we want a reliable benchmark of CPU-hungry function, we should be careful. For instance, let's measure two functions that calculate the difference between two dates: which one is faster? +Such performance measurements are often called "benchmarks". + ```js // we have date1 and date2, which function faster returns their difference in ms? function diffSubtract(date1, date2) { @@ -280,7 +279,7 @@ These two do exactly the same thing, but one of them uses an explicit `date.getT So, which one is faster? -The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it around 100000 times. +The first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it at least 100000 times. Let's measure: @@ -310,7 +309,7 @@ Wow! Using `getTime()` is so much faster! That's because there's no type convers Okay, we have something. But that's not a good benchmark yet. -Imagine that at the time of running `bench(diffSubtract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` the work has finished. +Imagine that at the time of running `bench(diffSubtract)` CPU was doing something in parallel, and it was taking resources. And by the time of running `bench(diffGetTime)` that work has finished. A pretty real scenario for a modern multi-process OS. @@ -318,7 +317,7 @@ As a result, the first benchmark will have less CPU resources than the second. T **For more reliable benchmarking, the whole pack of benchmarks should be rerun multiple times.** -Here's the code example: +For example, like this: ```js run function diffSubtract(date1, date2) { @@ -368,7 +367,7 @@ for (let i = 0; i < 10; i++) { ``` ```warn header="Be careful doing microbenchmarking" -Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all. +Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small, such as how an operator works, or a built-in function. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all. The great pack of articles about V8 can be found at . ``` @@ -415,7 +414,7 @@ alert(date); Note that unlike many other systems, timestamps in JavaScript are in milliseconds, not in seconds. -Also, sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point): +Sometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has [performance.now()](mdn:api/Performance/now) that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point): ```js run alert(`Loading started ${performance.now()}ms ago`); @@ -424,4 +423,4 @@ alert(`Loading started ${performance.now()}ms ago`); // more than 3 digits after the decimal point are precision errors, but only the first 3 are correct ``` -Node.JS has `microtime` module and other ways. Technically, any device and environment allows to get more precision, it's just not in `Date`. +Node.js has `microtime` module and other ways. Technically, almost any device and environment allows to get more precision, it's just not in `Date`. diff --git a/1-js/05-data-types/11-json/json-meetup.png b/1-js/05-data-types/11-json/json-meetup.png deleted file mode 100644 index 56595ce9..00000000 Binary files a/1-js/05-data-types/11-json/json-meetup.png and /dev/null differ diff --git a/1-js/05-data-types/11-json/json-meetup@2x.png b/1-js/05-data-types/11-json/json-meetup@2x.png deleted file mode 100644 index 7a1f4436..00000000 Binary files a/1-js/05-data-types/11-json/json-meetup@2x.png and /dev/null differ diff --git a/1-js/05-data-types/11-json/1-serialize-object/solution.md b/1-js/05-data-types/12-json/1-serialize-object/solution.md similarity index 100% rename from 1-js/05-data-types/11-json/1-serialize-object/solution.md rename to 1-js/05-data-types/12-json/1-serialize-object/solution.md diff --git a/1-js/05-data-types/11-json/1-serialize-object/task.md b/1-js/05-data-types/12-json/1-serialize-object/task.md similarity index 100% rename from 1-js/05-data-types/11-json/1-serialize-object/task.md rename to 1-js/05-data-types/12-json/1-serialize-object/task.md diff --git a/1-js/05-data-types/11-json/2-serialize-event-circular/solution.md b/1-js/05-data-types/12-json/2-serialize-event-circular/solution.md similarity index 100% rename from 1-js/05-data-types/11-json/2-serialize-event-circular/solution.md rename to 1-js/05-data-types/12-json/2-serialize-event-circular/solution.md diff --git a/1-js/05-data-types/11-json/2-serialize-event-circular/task.md b/1-js/05-data-types/12-json/2-serialize-event-circular/task.md similarity index 79% rename from 1-js/05-data-types/11-json/2-serialize-event-circular/task.md rename to 1-js/05-data-types/12-json/2-serialize-event-circular/task.md index 8b3963dd..3755a24a 100644 --- a/1-js/05-data-types/11-json/2-serialize-event-circular/task.md +++ b/1-js/05-data-types/12-json/2-serialize-event-circular/task.md @@ -6,7 +6,7 @@ importance: 5 In simple cases of circular references, we can exclude an offending property from serialization by its name. -But sometimes there are many backreferences. And names may be used both in circular references and normal properties. +But sometimes we can't just use the name, as it may be used both in circular references and normal properties. So we can check the property by its value. Write `replacer` function to stringify everything, but remove properties that reference `meetup`: @@ -22,7 +22,7 @@ let meetup = { }; *!* -// circular references +// circular references room.occupiedBy = meetup; meetup.self = meetup; */!* @@ -39,4 +39,3 @@ alert( JSON.stringify(meetup, function replacer(key, value) { } */ ``` - diff --git a/1-js/05-data-types/11-json/article.md b/1-js/05-data-types/12-json/article.md similarity index 89% rename from 1-js/05-data-types/11-json/article.md rename to 1-js/05-data-types/12-json/article.md index 0f526c59..2942cdd8 100644 --- a/1-js/05-data-types/11-json/article.md +++ b/1-js/05-data-types/12-json/article.md @@ -21,7 +21,7 @@ let user = { alert(user); // {name: "John", age: 30} ``` -...But in the process of development, new properties are added, old properties are renamed and removed. Updating such `toString` every time can become a pain. We could try to loop over properties in it, but what if the object is complex and has nested objects in properties? We'd need to implement their conversion as well. And, if we're sending the object over a network, then we also need to supply the code to "read" our object on the receiving side. +...But in the process of development, new properties are added, old properties are renamed and removed. Updating such `toString` every time can become a pain. We could try to loop over properties in it, but what if the object is complex and has nested objects in properties? We'd need to implement their conversion as well. Luckily, there's no need to write the code to handle all this. The task has been solved already. @@ -66,7 +66,7 @@ alert(json); The method `JSON.stringify(student)` takes the object and converts it into a string. -The resulting `json` string is a called *JSON-encoded* or *serialized* or *stringified* or *marshalled* object. We are ready to send it over the wire or put into a plain data store. +The resulting `json` string is called a *JSON-encoded* or *serialized* or *stringified* or *marshalled* object. We are ready to send it over the wire or put into a plain data store. Please note that a JSON-encoded object has several important differences from the object literal: @@ -76,7 +76,7 @@ Please note that a JSON-encoded object has several important differences from th `JSON.stringify` can be applied to primitives as well. -Natively supported JSON types are: +JSON supports following data types: - Objects `{ ... }` - Arrays `[ ... ]` @@ -100,7 +100,7 @@ alert( JSON.stringify(true) ); // true alert( JSON.stringify([1, 2, 3]) ); // [1,2,3] ``` -JSON is data-only cross-language specification, so some JavaScript-specific object properties are skipped by `JSON.stringify`. +JSON is data-only language-independent specification, so some JavaScript-specific object properties are skipped by `JSON.stringify`. Namely: @@ -170,7 +170,7 @@ JSON.stringify(meetup); // Error: Converting circular structure to JSON Here, the conversion fails, because of circular reference: `room.occupiedBy` references `meetup`, and `meetup.place` references `room`: -![](json-meetup.png) +![](json-meetup.svg) ## Excluding and transforming: replacer @@ -213,9 +213,9 @@ alert( JSON.stringify(meetup, *!*['title', 'participants']*/!*) ); // {"title":"Conference","participants":[{},{}]} ``` -Here we are probably too strict. The property list is applied to the whole object structure. So participants are empty, because `name` is not in the list. +Here we are probably too strict. The property list is applied to the whole object structure. So the objects in `participants` are empty, because `name` is not in the list. -Let's include every property except `room.occupiedBy` that would cause the circular reference: +Let's include in the list every property except `room.occupiedBy` that would cause the circular reference: ```js run let room = { @@ -244,7 +244,7 @@ Now everything except `occupiedBy` is serialized. But the list of properties is Fortunately, we can use a function instead of an array as the `replacer`. -The function will be called for every `(key, value)` pair and should return the "replaced" value, which will be used instead of the original one. +The function will be called for every `(key, value)` pair and should return the "replaced" value, which will be used instead of the original one. Or `undefined` if the value is to be skipped. In our case, we can return `value` "as is" for everything except `occupiedBy`. To ignore `occupiedBy`, the code below returns `undefined`: @@ -262,7 +262,7 @@ let meetup = { room.occupiedBy = meetup; // room references meetup alert( JSON.stringify(meetup, function replacer(key, value) { - alert(`${key}: ${value}`); // to see what replacer gets + alert(`${key}: ${value}`); return (key == 'occupiedBy') ? undefined : value; })); @@ -283,16 +283,16 @@ Please note that `replacer` function gets every key/value pair including nested The first call is special. It is made using a special "wrapper object": `{"": meetup}`. In other words, the first `(key, value)` pair has an empty key, and the value is the target object as a whole. That's why the first line is `":[object Object]"` in the example above. -The idea is to provide as much power for `replacer` as possible: it has a chance to analyze and replace/skip the whole object if necessary. +The idea is to provide as much power for `replacer` as possible: it has a chance to analyze and replace/skip even the whole object if necessary. -## Formatting: spacer +## Formatting: space -The third argument of `JSON.stringify(value, replacer, spaces)` is the number of spaces to use for pretty formatting. +The third argument of `JSON.stringify(value, replacer, space)` is the number of spaces to use for pretty formatting. -Previously, all stringified objects had no indents and extra spaces. That's fine if we want to send an object over a network. The `spacer` argument is used exclusively for a nice output. +Previously, all stringified objects had no indents and extra spaces. That's fine if we want to send an object over a network. The `space` argument is used exclusively for a nice output. -Here `spacer = 2` tells JavaScript to show nested objects on multiple lines, with indentation of 2 spaces inside an object: +Here `space = 2` tells JavaScript to show nested objects on multiple lines, with indentation of 2 spaces inside an object: ```js run let user = { @@ -328,7 +328,7 @@ alert(JSON.stringify(user, null, 2)); */ ``` -The `spaces` parameter is used solely for logging and nice-output purposes. +The `space` parameter is used solely for logging and nice-output purposes. ## Custom "toJSON" @@ -393,7 +393,7 @@ alert( JSON.stringify(meetup) ); */ ``` -As we can see, `toJSON` is used both for the direct call `JSON.stringify(room)` and for the nested object. +As we can see, `toJSON` is used both for the direct call `JSON.stringify(room)` and when `room` is nested in another encoded object. ## JSON.parse @@ -402,7 +402,7 @@ To decode a JSON-string, we need another method named [JSON.parse](mdn:js/JSON/p The syntax: ```js -let value = JSON.parse(str[, reviver]); +let value = JSON.parse(str, [reviver]); ``` str @@ -432,7 +432,7 @@ user = JSON.parse(user); alert( user.friends[1] ); // 1 ``` -The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the format. +The JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the same JSON format. Here are typical mistakes in hand-written JSON (sometimes we have to write it for debugging purposes): @@ -481,7 +481,7 @@ Whoops! An error! The value of `meetup.date` is a string, not a `Date` object. How could `JSON.parse` know that it should transform that string into a `Date`? -Let's pass to `JSON.parse` the reviving function that returns all values "as is", but `date` will become a `Date`: +Let's pass to `JSON.parse` the reviving function as the second argument, that returns all values "as is", but `date` will become a `Date`: ```js run let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; diff --git a/1-js/05-data-types/12-json/json-meetup.svg b/1-js/05-data-types/12-json/json-meetup.svg new file mode 100644 index 00000000..5dbb4a9a --- /dev/null +++ b/1-js/05-data-types/12-json/json-meetup.svg @@ -0,0 +1 @@ +number: 23title: "Conference"...placeoccupiedByparticipants \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md index 237b9ef9..3a281ef3 100644 --- a/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md +++ b/1-js/06-advanced-functions/01-recursion/01-sum-to/solution.md @@ -37,4 +37,4 @@ P.S. Naturally, the formula is the fastest solution. It uses only 3 operations f The loop variant is the second in terms of speed. In both the recursive and the loop variant we sum the same numbers. But the recursion involves nested calls and execution stack management. That also takes resources, so it's slower. -P.P.S. The standard describes a "tail call" optimization: if the recursive call is the very last one in the function (like in `sumTo` above), then the outer function will not need to resume the execution and we don't need to remember its execution context. In that case `sumTo(100000)` is countable. But if your JavaScript engine does not support it, there will be an error: maximum stack size exceeded, because there's usually a limitation on the total stack size. +P.P.S. Some engines support the "tail call" optimization: if a recursive call is the very last one in the function (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. diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png deleted file mode 100644 index ce72de3e..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg new file mode 100644 index 00000000..224735b6 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg @@ -0,0 +1 @@ +fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png deleted file mode 100644 index 39df3d86..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md index 91bcecc0..36524a45 100644 --- a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/solution.md @@ -29,7 +29,7 @@ Here we can see that the value of `fib(3)` is needed for both `fib(5)` and `fib( Here's the full recursion tree: -![fibonacci recursion tree](fibonacci-recursion-tree.png) +![fibonacci recursion tree](fibonacci-recursion-tree.svg) We can clearly notice that `fib(3)` is evaluated two times and `fib(2)` is evaluated three times. The total amount of computations grows much faster than `n`, making it enormous even for `n=77`. diff --git a/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md index 4e9de146..cfcbffea 100644 --- a/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md +++ b/1-js/06-advanced-functions/01-recursion/04-output-single-linked-list/solution.md @@ -43,7 +43,7 @@ function printList(list) { } ``` -...But that would be unwise. In the future we may need to extend a function, do something else with the list. If we change `list`, then we loose such ability. +...But that would be unwise. In the future we may need to extend a function, do something else with the list. If we change `list`, then we lose such ability. Talking about good variable names, `list` here is the list itself. The first element of it. And it should remain like that. That's clear and reliable. diff --git a/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md b/1-js/06-advanced-functions/01-recursion/05-output-single-linked-list-reverse/solution.md index a9ba0baf..4357ff20 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 @@ -37,7 +37,7 @@ The loop variant is also a little bit more complicated then the direct output. There is no way to get the last value in our `list`. We also can't "go back". -So what we can do is to first go through the items in the direct order and rememeber them in an array, and then output what we remembered in the reverse order: +So what we can do is to first go through the items in the direct order and remember them in an array, and then output what we remembered in the reverse order: ```js run let list = { diff --git a/1-js/06-advanced-functions/01-recursion/article.md b/1-js/06-advanced-functions/01-recursion/article.md index 036ae7f5..9d217653 100644 --- a/1-js/06-advanced-functions/01-recursion/article.md +++ b/1-js/06-advanced-functions/01-recursion/article.md @@ -70,7 +70,7 @@ pow(x, n) = We can also say that `pow` *recursively calls itself* till `n == 1`. -![recursive diagram of pow](recursion-pow.png) +![recursive diagram of pow](recursion-pow.svg) For example, to calculate `pow(2, 4)` the recursive variant does these steps: @@ -85,7 +85,7 @@ So, the recursion reduces a function call to a simpler one, and then -- to even ````smart header="Recursion is usually shorter" A recursive solution is usually shorter than an iterative one. -Here we can rewrite the same using the ternary `?` operator instead of `if` to make `pow(x, n)` more terse and still very readable: +Here we can rewrite the same using the conditional operator `?` instead of `if` to make `pow(x, n)` more terse and still very readable: ```js run function pow(x, n) { @@ -96,15 +96,15 @@ function pow(x, n) { The maximal number of nested calls (including the first one) is called *recursion depth*. In our case, it will be exactly `n`. -The maximal recursion depth is limited by JavaScript engine. We can make sure about 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases. +The maximal recursion depth is limited by JavaScript engine. We can rely on it being 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases. That limits the application of recursion, but it still remains very wide. There are many tasks where recursive way of thinking gives simpler code, easier to maintain. -## The execution stack +## The execution context and stack Now let's examine how recursive calls work. For that we'll look under the hood of functions. -The information about a function run is stored in its *execution context*. +The information about the process of execution of a running function is stored in its *execution context*. The [execution context](https://tc39.github.io/ecma262/#sec-execution-contexts) is an internal data structure that contains details about the execution of a function: where the control flow is now, the current variables, the value of `this` (we don't use it here) and few other internal details. @@ -319,25 +319,25 @@ let company = { In other words, a company has departments. - A department may have an array of staff. For instance, `sales` department has 2 employees: John and Alice. -- Or a department may split into subdepartments, like `development` has two branches: `sites` and `internals`. Each of them has the own staff. +- Or a department may split into subdepartments, like `development` has two branches: `sites` and `internals`. Each of them has their own staff. - It is also possible that when a subdepartment grows, it divides into subsubdepartments (or teams). For instance, the `sites` department in the future may be split into teams for `siteA` and `siteB`. And they, potentially, can split even more. That's not on the picture, just something to have in mind. Now let's say we want a function to get the sum of all salaries. How can we do that? -An iterative approach is not easy, because the structure is not simple. The first idea may be to make a `for` loop over `company` with nested subloop over 1st level departments. But then we need more nested subloops to iterate over the staff in 2nd level departments like `sites`. ...And then another subloop inside those for 3rd level departments that might appear in the future? Should we stop on level 3 or make 4 levels of loops? If we put 3-4 nested subloops in the code to traverse a single object, it becomes rather ugly. +An iterative approach is not easy, because the structure is not simple. The first idea may be to make a `for` loop over `company` with nested subloop over 1st level departments. But then we need more nested subloops to iterate over the staff in 2nd level departments like `sites`... And then another subloop inside those for 3rd level departments that might appear in the future? If we put 3-4 nested subloops in the code to traverse a single object, it becomes rather ugly. Let's try recursion. As we can see, when our function gets a department to sum, there are two possible cases: -1. Either it's a "simple" department with an *array of people* -- then we can sum the salaries in a simple loop. -2. Or it's *an object with `N` subdepartments* -- then we can make `N` recursive calls to get the sum for each of the subdeps and combine the results. +1. Either it's a "simple" department with an *array* of people -- then we can sum the salaries in a simple loop. +2. Or it's *an object* with `N` subdepartments -- then we can make `N` recursive calls to get the sum for each of the subdeps and combine the results. -The (1) is the base of recursion, the trivial case. +The 1st case is the base of recursion, the trivial case, when we get an array. -The (2) is the recursive step. A complex task is split into subtasks for smaller departments. They may in turn split again, but sooner or later the split will finish at (1). +The 2nd case when we get an object is the recursive step. A complex task is split into subtasks for smaller departments. They may in turn split again, but sooner or later the split will finish at (1). The algorithm is probably even easier to read from the code: @@ -373,7 +373,7 @@ The code is short and easy to understand (hopefully?). That's the power of recur Here's the diagram of calls: -![recursive salaries](recursive-salaries.png) +![recursive salaries](recursive-salaries.svg) We can easily see the principle: for an object `{...}` subcalls are made, while arrays `[...]` are the "leaves" of the recursion tree, they give immediate result. @@ -416,7 +416,7 @@ let arr = [obj1, obj2, obj3]; ...But there's a problem with arrays. The "delete element" and "insert element" operations are expensive. For instance, `arr.unshift(obj)` operation has to renumber all elements to make room for a new `obj`, and if the array is big, it takes time. Same with `arr.shift()`. -The only structural modifications that do not require mass-renumbering are those that operate with the end of array: `arr.push/pop`. So an array can be quite slow for big queues. +The only structural modifications that do not require mass-renumbering are those that operate with the end of array: `arr.push/pop`. So an array can be quite slow for big queues, when we have to work with the beginning. Alternatively, if we really need fast insertion/deletion, we can choose another data structure called a [linked list](https://en.wikipedia.org/wiki/Linked_list). @@ -444,7 +444,7 @@ let list = { Graphical representation of the list: -![linked list](linked-list.png) +![linked list](linked-list.svg) An alternative code for creation: @@ -464,7 +464,7 @@ let secondList = list.next.next; list.next.next = null; ``` -![linked list split](linked-list-split.png) +![linked list split](linked-list-split.svg) To join: @@ -488,7 +488,7 @@ list = { value: "new item", next: list }; */!* ``` -![linked list](linked-list-0.png) +![linked list](linked-list-0.svg) To remove a value from the middle, change `next` of the previous one: @@ -496,7 +496,7 @@ To remove a value from the middle, change `next` of the previous one: list.next = list.next.next; ``` -![linked list](linked-list-remove-1.png) +![linked list](linked-list-remove-1.svg) We made `list.next` jump over `1` to value `2`. The value `1` is now excluded from the chain. If it's not stored anywhere else, it will be automatically removed from the memory. @@ -506,14 +506,17 @@ Naturally, lists are not always better than arrays. Otherwise everyone would use The main drawback is that we can't easily access an element by its number. In an array that's easy: `arr[n]` is a direct reference. But in the list we need to start from the first item and go `next` `N` times to get the Nth element. -...But we don't always need such operations. For instance, when we need a queue or even a [deque](https://en.wikipedia.org/wiki/Double-ended_queue) -- the ordered structure that must allow very fast adding/removing elements from both ends. +...But we don't always need such operations. For instance, when we need a queue or even a [deque](https://en.wikipedia.org/wiki/Double-ended_queue) -- the ordered structure that must allow very fast adding/removing elements from both ends, but access to its middle is not needed. -Sometimes it's worth to add another variable named `tail` to track the last element of the list (and update it when adding/removing elements from the end). For large sets of elements the speed difference versus arrays is huge. +Lists can be enhanced: +- We can add property `prev` in addition to `next` to reference the previous element, to move back easily. +- We can also add a variable named `tail` referencing the last element of the list (and update it when adding/removing elements from the end). +- ...The data structure may vary according to our needs. ## Summary Terms: -- *Recursion* is a programming term that means a "self-calling" function. Such functions can be used to solve certain tasks in elegant ways. +- *Recursion* is a programming term that means calling a function from itself. Recursive functions can be used to solve tasks in elegant ways. When a function calls itself, that's called a *recursion step*. The *basis* of recursion is function arguments that make the task so simple that the function does not make further calls. diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0.png b/1-js/06-advanced-functions/01-recursion/linked-list-0.png deleted file mode 100644 index d2ebb4ad..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-0.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0.svg b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg new file mode 100644 index 00000000..f95e1bfc --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list-0.svg @@ -0,0 +1 @@ +value1nextvalue"new item"nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png deleted file mode 100644 index 2c02fdc2..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-0@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png deleted file mode 100644 index c5123f2e..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg new file mode 100644 index 00000000..3828ec08 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg @@ -0,0 +1 @@ +value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png deleted file mode 100644 index 24c990db..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.png b/1-js/06-advanced-functions/01-recursion/linked-list-split.png deleted file mode 100644 index f8e53641..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-split.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split.svg b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg new file mode 100644 index 00000000..3db14be2 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list-split.svg @@ -0,0 +1 @@ +value1nextvalue2nextvalue3nextvalue4nextnullnullsecondListlist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png deleted file mode 100644 index b8e750bb..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list-split@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.png b/1-js/06-advanced-functions/01-recursion/linked-list.png deleted file mode 100644 index bffa6960..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/linked-list.svg b/1-js/06-advanced-functions/01-recursion/linked-list.svg new file mode 100644 index 00000000..6dd22939 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/linked-list.svg @@ -0,0 +1 @@ +value1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/linked-list@2x.png b/1-js/06-advanced-functions/01-recursion/linked-list@2x.png deleted file mode 100644 index a4ff44ee..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/linked-list@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.png b/1-js/06-advanced-functions/01-recursion/recursion-pow.png deleted file mode 100644 index e2142a08..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursion-pow.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg new file mode 100644 index 00000000..8354cd15 --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg @@ -0,0 +1 @@ +pow(x,n)xx * pow(x, n-1)n == 1 ?YesNorecursive call until n==1 \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png b/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png deleted file mode 100644 index 36032173..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursion-pow@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.png b/1-js/06-advanced-functions/01-recursion/recursive-salaries.png deleted file mode 100644 index 3de7e309..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursive-salaries.png and /dev/null differ diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg new file mode 100644 index 00000000..207271cb --- /dev/null +++ b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png b/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png deleted file mode 100644 index 7d7b13fc..00000000 Binary files a/1-js/06-advanced-functions/01-recursion/recursive-salaries@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md b/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md index a98d8edd..a14f0fb7 100644 --- a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md +++ b/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md @@ -8,7 +8,7 @@ For instance: - `Object.assign(dest, src1, ..., srcN)` -- copies properties from `src1..N` into `dest`. - ...and so on. -In this chapter we'll learn how to do the same. And, more importantly, how to feel comfortable working with such functions and arrays. +In this chapter we'll learn how to do the same. And also, how to pass arrays to such functions as parameters. ## Rest parameters `...` @@ -25,7 +25,7 @@ alert( sum(1, 2, 3, 4, 5) ); There will be no error because of "excessive" arguments. But of course in the result only the first two will be counted. -The rest parameters can be mentioned in a function definition with three dots `...`. They literally mean "gather the remaining parameters into an array". +The rest of the parameters can be included in the function definition by using three dots `...` followed by the name of the array that will contain them. The dots literally mean "gather the remaining parameters into an array". For instance, to gather all arguments into array `args`: @@ -96,9 +96,7 @@ showName("Julius", "Caesar"); showName("Ilya"); ``` -In old times, rest parameters did not exist in the language, and using `arguments` was the only way to get all arguments of the function, no matter their total number. - -And it still works, we can use it today. +In old times, rest parameters did not exist in the language, and using `arguments` was the only way to get all arguments of the function. And it still works, we can find it in the old code. But the downside is that although `arguments` is both array-like and iterable, it's not an array. It does not support array methods, so we can't call `arguments.map(...)` for example. @@ -119,9 +117,10 @@ function f() { f(1); // 1 ``` -```` As we remember, arrow functions don't have their own `this`. Now we know they don't have the special `arguments` object either. +```` + ## Spread operator [#spread-operator] diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md index d0dbbeb1..d02c53b9 100644 --- a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md +++ b/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md @@ -1,7 +1,7 @@ # Function in if -Look at the code. What will be result of the call at the last line? +Look at the code. What will be the result of the call at the last line? ```js run let phrase = "Hello"; diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png deleted file mode 100644 index de84d79b..00000000 Binary files a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.png and /dev/null differ diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg new file mode 100644 index 00000000..5549a6a0 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg @@ -0,0 +1 @@ +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/lexenv-makearmy@2x.png b/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png deleted file mode 100644 index c5ad4b0d..00000000 Binary files a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md b/1-js/06-advanced-functions/03-closure/8-make-army/solution.md index f5afd426..0fb0b4a4 100644 --- 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 @@ -84,7 +84,7 @@ Now it works correctly, because every time the code block in `for (let i=0...) { 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.png) +![](lexenv-makearmy.svg) Here we rewrote `while` into `for`. diff --git a/1-js/06-advanced-functions/03-closure/article.md b/1-js/06-advanced-functions/03-closure/article.md index ac0e44c6..a39dfc95 100644 --- a/1-js/06-advanced-functions/03-closure/article.md +++ b/1-js/06-advanced-functions/03-closure/article.md @@ -1,9 +1,9 @@ # Closure -JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created at one moment, then copied to another variable or passed as an argument to another function and called from a totally different place later. +JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created dynamically, copied to another variable or passed as an argument to another function and called from a totally different place later. -We know that a function can access variables outside of it; this feature is used quite often. +We know that a function can access variables outside of it, this feature is used quite often. 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? @@ -63,33 +63,33 @@ Let's consider two situations to begin with, and then study the internal mechani To understand what's going on, let's first discuss what a "variable" actually is. -In JavaScript, every running function, code block, and the script as a whole have an associated object known as the *Lexical Environment*. +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 has all local variables as its properties (and some other information like the value of `this`). -2. A reference to the *outer lexical environment*, usually the one associated with the code lexically right outside of it (outside of the current curly brackets). +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. -**So, a "variable" is just a property of the special internal object, Environment Record. "To get or change a variable" means "to get or change a property of that object".** +**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.png) +![lexical environment](lexical-environment-global.svg) -This is a so-called global Lexical Environment, associated with the whole script. For browsers, all ` +```js run +if (!window.Promise) { + window.Promise = ... // custom implementation of the modern language feature +} +``` - - ``` +## Summary -4. And, a minor thing, but still: the value of `this` in the global scope is `window`. +- The global object holds variables that should be available everywhere. - ```js untrusted run no-strict refresh - alert(this); // window - ``` + 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`. -Why was it made like this? At the time of the language creation, the idea to merge multiple aspects into a single `window` object was to "make things simple". But since then many things changed. Tiny scripts became big applications that require proper architecture. - -Is it good that different scripts (possibly from different sources) see variables of each other? - -No, it's not, because it may lead to naming conflicts: the same variable name can be used in two scripts for different purposes, so they will conflict with each other. - -As of now, the multi-purpose `window` is considered a design mistake in the language. - -Luckily, there's a "road out of hell", called "Javascript modules". - -If we set `type="module"` attribute on a ` - ``` - -- Two modules that do not see variables of each other: - - ```html run - - - - ``` - -- And, the last minor thing, the top-level value of `this` in a module is `undefined` (why should it be `window` anyway?): - - ```html run - - ``` - -**Using ` -``` - -If you run it, the changes to `i` will show up after the whole count finishes. - -And if we use `setTimeout` to split it into pieces then changes are applied in-between the runs, so this looks better: - -```html run -
- - -``` - -Now the `
` shows increasing values of `i`. - ## Summary - Methods `setInterval(func, delay, ...args)` and `setTimeout(func, delay, ...args)` allow to run the `func` regularly/once after `delay` milliseconds. - To cancel the execution, we should call `clearInterval/clearTimeout` with the value returned by `setInterval/setTimeout`. -- Nested `setTimeout` calls is a more flexible alternative to `setInterval`. Also they can guarantee the minimal time *between* the executions. -- Zero-timeout scheduling `setTimeout(...,0)` is used to schedule the call "as soon as possible, but after the current code is complete". +- 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. -Some use cases of `setTimeout(...,0)`: -- To split CPU-hungry tasks into pieces, so that the script doesn't "hang" -- To let the browser do something else while the process is going on (paint the progress bar). - -Please note that all scheduling methods do not *guarantee* the exact delay. We should not rely on that in the scheduled code. +Please note that all scheduling methods do not *guarantee* the exact delay. For example, the in-browser timer may slow down for a lot of reasons: - The CPU is overloaded. - The browser tab is in the background mode. - The laptop is on battery. -All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and settings. +All that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and OS-level performance settings. diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png deleted file mode 100644 index ccafd7f1..00000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.png and /dev/null differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg new file mode 100644 index 00000000..9a214c54 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval.svg @@ -0,0 +1 @@ +func(1)func(2)func(3)100200300 \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png b/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png deleted file mode 100644 index 9fcd057e..00000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/setinterval-interval@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png deleted file mode 100644 index 094cf915..00000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.png and /dev/null differ diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg new file mode 100644 index 00000000..13b22a89 --- /dev/null +++ b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval.svg @@ -0,0 +1 @@ +func(1)func(2)func(3)100100 \ No newline at end of file diff --git a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png b/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png deleted file mode 100644 index bee58575..00000000 Binary files a/1-js/06-advanced-functions/08-settimeout-setinterval/settimeout-interval@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md index 716b4e1d..567c9ce7 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 @@ -12,20 +12,20 @@ Let's check the real-life application to better understand that requirement and **For instance, we want to track mouse movements.** -In browser we can setup a function to run at every mouse micro-movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms). +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). -**The tracking function should update some information on the web-page.** +**We'd like to update some information on the web-page when the pointer moves.** -Updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in making it more often than once per 100ms. +...But updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in updating more often than once per 100ms. -So we'll assign `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but `update()` will be called at maximum once per 100ms. +So we'll wrap it into the decorator: use `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but forward the call to `update()` at maximum once per 100ms. Visually, it will look like this: -1. For the first mouse movement the decorated variant passes the call to `update`. That's important, the user sees our reaction to their move immediately. +1. For the first mouse movement the decorated variant immediately passes the call to `update`. That's important, the user sees our reaction to their move immediately. 2. Then as the mouse moves on, until `100ms` nothing happens. The decorated variant ignores calls. -3. At the end of `100ms` -- one more `update` happens with the last coordinates. -4. Then, finally, the mouse stops somewhere. The decorated variant waits until `100ms` expire and then runs `update` with last coordinates. So, perhaps the most important, the final mouse coordinates are processed. +3. At the end of `100ms` -- one more `update` happens with the last coordinates. +4. Then, finally, the mouse stops somewhere. The decorated variant waits until `100ms` expire and then runs `update` with last coordinates. So, quite important, the final mouse coordinates are processed. A code example: 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 75c510d1..ceaf2b6a 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/article.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/article.md @@ -6,9 +6,9 @@ JavaScript gives exceptional flexibility when dealing with functions. They can b Let's say we have a function `slow(x)` which is CPU-heavy, but its results are stable. In other words, for the same `x` it always returns the same result. -If the function is called often, we may want to cache (remember) the results for different `x` to avoid spending extra-time on recalculations. +If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations. -But instead of adding that functionality into `slow()` we'll create a wrapper. As we'll see, there are many benefits of doing so. +But instead of adding that functionality into `slow()` we'll create a wrapper function, that adds caching. As we'll see, there are many benefits of doing so. Here's the code, and explanations follow: @@ -23,13 +23,13 @@ function cachingDecorator(func) { let cache = new Map(); return function(x) { - if (cache.has(x)) { // if the result is in the map - return cache.get(x); // return it + if (cache.has(x)) { // if there's such key in cache + return cache.get(x); // read the result from it } - let result = func(x); // otherwise call func + let result = func(x); // otherwise call func - cache.set(x, result); // and cache (remember) the result + cache.set(x, result); // and cache (remember) the result return result; }; } @@ -49,18 +49,16 @@ The idea is that we can call `cachingDecorator` for any function, and it will re By separating caching from the main function code we also keep the main code simpler. -Now let's get into details of how it works. - The result of `cachingDecorator(func)` is a "wrapper": `function(x)` that "wraps" the call of `func(x)` into caching logic: -![](decorator-makecaching-wrapper.png) +![](decorator-makecaching-wrapper.svg) -As we can see, the wrapper returns the result of `func(x)` "as is". From an outside code, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior. +From an outside code, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior. To summarize, there are several benefits of using a separate `cachingDecorator` instead of altering the code of `slow` itself: - The `cachingDecorator` is reusable. We can apply it to another function. -- The caching logic is separate, it did not increase the complexity of `slow` itself (if there were any). +- The caching logic is separate, it did not increase the complexity of `slow` itself (if there was any). - We can combine multiple decorators if needed (other decorators will follow). @@ -231,9 +229,7 @@ let worker = { worker.slow = cachingDecorator(worker.slow); ``` -We have two tasks to solve here. - -First is how to use both arguments `min` and `max` for the key in `cache` map. Previously, for a single argument `x` we could just `cache.set(x, result)` to save the result and `cache.get(x)` to retrieve it. But now we need to remember the result for a *combination of arguments* `(min,max)`. The native `Map` takes single value only as the key. +Previously, for a single argument `x` we could just `cache.set(x, result)` to save the result and `cache.get(x)` to retrieve it. But now we need to remember the result for a *combination of arguments* `(min,max)`. The native `Map` takes single value only as the key. There are many solutions possible: @@ -241,85 +237,11 @@ There are many solutions possible: 2. Use nested maps: `cache.set(min)` will be a `Map` that stores the pair `(max, result)`. So we can get `result` as `cache.get(min).get(max)`. 3. Join two values into one. In our particular case we can just use a string `"min,max"` as the `Map` key. For flexibility, we can allow to provide a *hashing function* for the decorator, that knows how to make one value from many. - For many practical applications, the 3rd variant is good enough, so we'll stick to it. -The second task to solve is how to pass many arguments to `func`. Currently, the wrapper `function(x)` assumes a single argument, and `func.call(this, x)` passes it. +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. -Here we can use another built-in method [func.apply](mdn:js/Function/apply). - -The syntax is: - -```js -func.apply(context, args) -``` - -It runs the `func` setting `this=context` and using an array-like object `args` as the list of arguments. - - -For instance, these two calls are almost the same: - -```js -func(1, 2, 3); -func.apply(context, [1, 2, 3]) -``` - -Both run `func` giving it arguments `1,2,3`. But `apply` also sets `this=context`. - -For instance, here `say` is called with `this=user` and `messageData` as a list of arguments: - -```js run -function say(time, phrase) { - alert(`[${time}] ${this.name}: ${phrase}`); -} - -let user = { name: "John" }; - -let messageData = ['10:00', 'Hello']; // become time and phrase - -*!* -// user becomes this, messageData is passed as a list of arguments (time, phrase) -say.apply(user, messageData); // [10:00] John: Hello (this=user) -*/!* -``` - -The only syntax difference between `call` and `apply` is that `call` expects a list of arguments, while `apply` takes an array-like object with them. - -We already know the spread operator `...` from the chapter that can pass an array (or any iterable) as a list of arguments. So if we use it with `call`, we can achieve almost the same as `apply`. - -These two calls are almost equivalent: - -```js -let args = [1, 2, 3]; - -*!* -func.call(context, ...args); // pass an array as list with spread operator -func.apply(context, args); // is same as using apply -*/!* -``` - -If we look more closely, there's a minor difference between such uses of `call` and `apply`. - -- The spread operator `...` allows to pass *iterable* `args` as the list to `call`. -- The `apply` accepts only *array-like* `args`. - -So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works. - -And if `args` is both iterable and array-like, like a real array, then we technically could use any of them, but `apply` will probably be faster, because it's a single operation. Most JavaScript engines internally optimize it better than a pair `call + spread`. - -One of the most important uses of `apply` is passing the call to another function, like this: - -```js -let wrapper = function() { - return anotherFunction.apply(this, arguments); -}; -``` - -That's called *call forwarding*. The `wrapper` passes everything it gets: the context `this` and arguments to `anotherFunction` and returns back its result. - -When an external code calls such `wrapper`, it is indistinguishable from the call of the original function. - -Now let's bake it all into the more powerful `cachingDecorator`: +Here's a more powerful `cachingDecorator`: ```js run let worker = { @@ -340,7 +262,7 @@ function cachingDecorator(func, hash) { } *!* - let result = func.apply(this, arguments); // (**) + let result = func.call(this, ...arguments); // (**) */!* cache.set(key, result); @@ -358,13 +280,52 @@ alert( worker.slow(3, 5) ); // works alert( "Again " + worker.slow(3, 5) ); // same (cached) ``` -Now the wrapper operates with any number of arguments. +Now it works with any number of arguments (though the hash function would also need to be adjusted to allow any number of arguments. An interesting way to handle this will be covered below). There are two changes: - In the line `(*)` it calls `hash` to create a single key from `arguments`. Here we use a simple "joining" function that turns arguments `(3, 5)` into the key `"3,5"`. More complex cases may require other hashing functions. -- Then `(**)` uses `func.apply` to pass both the context and all arguments the wrapper got (no matter how many) to the original function. +- Then `(**)` uses `func.call(this, ...arguments)` to pass both the context and all arguments the wrapper got (not just the first one) to the original function. +Instead of `func.call(this, ...arguments)` we could use `func.apply(this, arguments)`. + +The syntax of built-in method [func.apply](mdn:js/Function/apply) is: + +```js +func.apply(context, args) +``` + +It runs the `func` setting `this=context` and using an array-like object `args` as the list of arguments. + +The only syntax difference between `call` and `apply` is that `call` expects a list of arguments, while `apply` takes an array-like object with them. + +So these two calls are almost equivalent: + +```js +func.call(context, ...args); // pass an array as list with spread operator +func.apply(context, args); // is same as using apply +``` + +There's only a minor difference: + +- The spread operator `...` allows to pass *iterable* `args` as the list to `call`. +- The `apply` accepts only *array-like* `args`. + +So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works. + +And 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. + +Passing all arguments along with the context to another function is called *call forwarding*. + +That's the simplest form of it: + +```js +let wrapper = function() { + return func.apply(this, arguments); +}; +``` + +When an external code calls such `wrapper`, it is indistinguishable from the call of the original function `func`. ## Borrowing a method [#method-borrowing] @@ -450,10 +411,9 @@ The generic *call forwarding* is usually done with `apply`: ```js let wrapper = function() { return original.apply(this, arguments); -} +}; ``` -We also saw an example of *method borrowing* when we take a method from an object and `call` it in the context of another object. It is quite common to take array methods and apply them to arguments. The alternative is to use rest parameters object that is a real array. - +We also saw an example of *method borrowing* when we take a method from an object and `call` it in the context of another object. It is quite common to take array methods and apply them to `arguments`. The alternative is to use rest parameters object that is a real array. There are many decorators there in the wild. Check how well you got them by solving the tasks of this chapter. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png deleted file mode 100644 index e45e4867..00000000 Binary files a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.png and /dev/null differ diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg new file mode 100644 index 00000000..5fc7743f --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper.svg @@ -0,0 +1 @@ +wrapperaround the function \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png b/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png deleted file mode 100644 index eec94c5b..00000000 Binary files a/1-js/06-advanced-functions/09-call-apply-decorators/decorator-makecaching-wrapper@2x.png and /dev/null differ diff --git a/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md b/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md index 0cb673b1..403107ca 100644 --- a/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md +++ b/1-js/06-advanced-functions/10-bind/5-question-use-bind/solution.md @@ -38,6 +38,6 @@ An alternative solution could be: askPassword(() => user.loginOk(), () => user.loginFail()); ``` -Usually that also works, but may fail in more complex situations where `user` has a chance of being overwritten between the moments of asking and running `() => user.loginOk()`. - +Usually that also works and looks good. +It's a bit less reliable though in more complex situations where `user` variable might change *after* `askPassword` is called, but *before* the visitor answers and calls `() => user.loginOk()`. diff --git a/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md b/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md index eb19e664..fe6a9b4e 100644 --- a/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md +++ b/1-js/06-advanced-functions/10-bind/5-question-use-bind/task.md @@ -2,7 +2,7 @@ importance: 5 --- -# Ask losing this +# Fix a function that loses "this" The call to `askPassword()` in the code below should check the password and then call `user.loginOk/loginFail` depending on the answer. @@ -34,5 +34,3 @@ let user = { askPassword(user.loginOk, user.loginFail); */!* ``` - - diff --git a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md b/1-js/06-advanced-functions/10-bind/6-ask-partial/solution.md similarity index 100% rename from 1-js/06-advanced-functions/11-currying-partials/1-ask-currying/solution.md rename to 1-js/06-advanced-functions/10-bind/6-ask-partial/solution.md diff --git a/1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md b/1-js/06-advanced-functions/10-bind/6-ask-partial/task.md similarity index 100% rename from 1-js/06-advanced-functions/11-currying-partials/1-ask-currying/task.md rename to 1-js/06-advanced-functions/10-bind/6-ask-partial/task.md diff --git a/1-js/06-advanced-functions/10-bind/article.md b/1-js/06-advanced-functions/10-bind/article.md index fdb07c60..ce0c94a5 100644 --- a/1-js/06-advanced-functions/10-bind/article.md +++ b/1-js/06-advanced-functions/10-bind/article.md @@ -5,13 +5,13 @@ libs: # Function binding -When using `setTimeout` with object methods or passing object methods along, there's a known problem: "losing `this`". +When passing object methods as callbacks, for instance to `setTimeout`, there's a known problem: "losing `this`". -Suddenly, `this` just stops working right. The situation is typical for novice developers, but happens with experienced ones as well. +In this chapter we'll see the ways to fix it. ## Losing "this" -We already know that in JavaScript it's easy to lose `this`. Once a method is passed somewhere separately from the object -- `this` is lost. +We've already seen examples of losing `this`. Once a method is passed somewhere separately from the object -- `this` is lost. Here's how it may happen with `setTimeout`: @@ -37,7 +37,7 @@ let f = user.sayHi; setTimeout(f, 1000); // lost user context ``` -The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.JS, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases as we'll see, usually `this` just becomes `undefined`. +The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.js, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases, usually `this` just becomes `undefined`. The task is quite typical -- we want to pass an object method somewhere else (here -- to the scheduler) where it will be called. How to make sure that it will be called in the right context? @@ -100,7 +100,7 @@ The basic syntax is: ```js // more complex syntax will be little later let boundFunc = func.bind(context); -```` +``` The result of `func.bind(context)` is a special function-like "exotic object", that is callable as function and transparently passes the call to `func` setting `this=context`. @@ -196,8 +196,124 @@ 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. ```` +## Partial functions + +Until now we have only been talking about binding `this`. Let's take it a step further. + +We can bind not only `this`, but also arguments. That's rarely done, but sometimes can be handy. + +The full syntax of `bind`: + +```js +let bound = func.bind(context, [arg1], [arg2], ...); +``` + +It allows to bind context as `this` and starting arguments of the function. + +For instance, we have a multiplication function `mul(a, b)`: + +```js +function mul(a, b) { + return a * b; +} +``` + +Let's use `bind` to create a function `double` on its base: + +```js run +function mul(a, b) { + return a * b; +} + +*!* +let double = mul.bind(null, 2); +*/!* + +alert( double(3) ); // = mul(2, 3) = 6 +alert( double(4) ); // = mul(2, 4) = 8 +alert( double(5) ); // = mul(2, 5) = 10 +``` + +The call to `mul.bind(null, 2)` creates a new function `double` that passes calls to `mul`, fixing `null` as the context and `2` as the first argument. Further arguments are passed "as is". + +That's called [partial function application](https://en.wikipedia.org/wiki/Partial_application) -- we create a new function by fixing some parameters of the existing one. + +Please note that here we actually don't use `this` here. But `bind` requires it, so we must put in something like `null`. + +The function `triple` in the code below triples the value: + +```js run +function mul(a, b) { + return a * b; +} + +*!* +let triple = mul.bind(null, 3); +*/!* + +alert( triple(3) ); // = mul(3, 3) = 9 +alert( triple(4) ); // = mul(3, 4) = 12 +alert( triple(5) ); // = mul(3, 5) = 15 +``` + +Why do we usually make a partial function? + +The benefit is that we can create an independent function with a readable name (`double`, `triple`). We can use it and not provide first argument of every time as it's fixed with `bind`. + +In other cases, partial application is useful when we have a very generic function and want a less universal variant of it for convenience. + +For instance, we have a function `send(from, to, text)`. Then, inside a `user` object we may want to use a partial variant of it: `sendTo(to, text)` that sends from the current user. + +## Going partial without context + +What if we'd like to fix some arguments, but not the context `this`? For example, for an object method. + +The native `bind` does not allow that. We can't just omit the context and jump to arguments. + +Fortunately, a helper function `partial` for binding only arguments can be easily implemented. + +Like this: + +```js run +*!* +function partial(func, ...argsBound) { + return function(...args) { // (*) + return func.call(this, ...argsBound, ...args); + } +} +*/!* + +// Usage: +let user = { + firstName: "John", + say(time, phrase) { + alert(`[${time}] ${this.firstName}: ${phrase}!`); + } +}; + +// add a partial method with fixed time +user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); + +user.sayNow("Hello"); +// Something like: +// [10:00] John: Hello! +``` + +The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that calls `func` with: +- Same `this` as it gets (for `user.sayNow` call it's `user`) +- Then gives it `...argsBound` -- arguments from the `partial` call (`"10:00"`) +- Then gives it `...args` -- arguments given to the wrapper (`"Hello"`) + +So easy to do it with the spread operator, right? + +Also there's a ready [_.partial](https://lodash.com/docs#partial) implementation from lodash library. + ## Summary Method `func.bind(context, ...args)` returns a "bound variant" of function `func` that fixes the context `this` and first arguments if given. -Usually we apply `bind` to fix `this` in an object method, so that we can pass it somewhere. For example, to `setTimeout`. There are more reasons to `bind` in the modern development, we'll meet them later. +Usually we apply `bind` to fix `this` for an object method, so that we can pass it somewhere. For example, to `setTimeout`. + +When we fix some arguments of an existing function, the resulting (less universal) function is called *partially applied* or *partial*. + +Partials are convenient when we don't want to repeat the same argument over and over again. Like if we have a `send(from, to)` function, and `from` should always be the same for our task, we can get a partial and go on with it. diff --git a/1-js/06-advanced-functions/11-currying-partials/article.md b/1-js/06-advanced-functions/11-currying-partials/article.md deleted file mode 100644 index 310e4830..00000000 --- a/1-js/06-advanced-functions/11-currying-partials/article.md +++ /dev/null @@ -1,308 +0,0 @@ -libs: - - lodash - ---- - -# Currying and partials - -Until now we have only been talking about binding `this`. Let's take it a step further. - -We can bind not only `this`, but also arguments. That's rarely done, but sometimes can be handy. - -The full syntax of `bind`: - -```js -let bound = func.bind(context, arg1, arg2, ...); -``` - -It allows to bind context as `this` and starting arguments of the function. - -For instance, we have a multiplication function `mul(a, b)`: - -```js -function mul(a, b) { - return a * b; -} -``` - -Let's use `bind` to create a function `double` on its base: - -```js run -function mul(a, b) { - return a * b; -} - -*!* -let double = mul.bind(null, 2); -*/!* - -alert( double(3) ); // = mul(2, 3) = 6 -alert( double(4) ); // = mul(2, 4) = 8 -alert( double(5) ); // = mul(2, 5) = 10 -``` - -The call to `mul.bind(null, 2)` creates a new function `double` that passes calls to `mul`, fixing `null` as the context and `2` as the first argument. Further arguments are passed "as is". - -That's called [partial function application](https://en.wikipedia.org/wiki/Partial_application) -- we create a new function by fixing some parameters of the existing one. - -Please note that here we actually don't use `this` here. But `bind` requires it, so we must put in something like `null`. - -The function `triple` in the code below triples the value: - -```js run -function mul(a, b) { - return a * b; -} - -*!* -let triple = mul.bind(null, 3); -*/!* - -alert( triple(3) ); // = mul(3, 3) = 9 -alert( triple(4) ); // = mul(3, 4) = 12 -alert( triple(5) ); // = mul(3, 5) = 15 -``` - -Why do we usually make a partial function? - -Here our benefit is that we created an independent function with a readable name (`double`, `triple`). We can use it and don't write the first argument of every time, cause it's fixed with `bind`. - -In other cases, partial application is useful when we have a very generic function, and want a less universal variant of it for convenience. - -For instance, we have a function `send(from, to, text)`. Then, inside a `user` object we may want to use a partial variant of it: `sendTo(to, text)` that sends from the current user. - -## Going partial without context - -What if we'd like to fix some arguments, but not bind `this`? - -The native `bind` does not allow that. We can't just omit the context and jump to arguments. - -Fortunately, a `partial` function for binding only arguments can be easily implemented. - -Like this: - -```js run -*!* -function partial(func, ...argsBound) { - return function(...args) { // (*) - return func.call(this, ...argsBound, ...args); - } -} -*/!* - -// Usage: -let user = { - firstName: "John", - say(time, phrase) { - alert(`[${time}] ${this.firstName}: ${phrase}!`); - } -}; - -// add a partial method that says something now by fixing the first argument -user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); - -user.sayNow("Hello"); -// Something like: -// [10:00] John: Hello! -``` - -The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that calls `func` with: -- Same `this` as it gets (for `user.sayNow` call it's `user`) -- Then gives it `...argsBound` -- arguments from the `partial` call (`"10:00"`) -- Then gives it `...args` -- arguments given to the wrapper (`"Hello"`) - -So easy to do it with the spread operator, right? - -Also there's a ready [_.partial](https://lodash.com/docs#partial) implementation from lodash library. - -## Currying - -Sometimes people mix up partial function application mentioned above with another thing named "currying". That's another interesting technique of working with functions that we just have to mention here. - -[Currying](https://en.wikipedia.org/wiki/Currying) is translating a function from callable as `f(a, b, c)` into callable as `f(a)(b)(c)`. - -Literally, currying is a transformation of functions: from one way of calling into another. In JavaScript, we usually make a wrapper to keep the original function. - -Currying doesn't call a function. It just transforms it. We'll see use cases soon. - -Let's make `curry` function that performs currying for two-argument functions. In other words, `curry(f)` for two-argument `f(a, b)` translates it into `f(a)(b)` - -```js run -*!* -function curry(f) { // curry(f) does the currying transform - return function(a) { - return function(b) { - return f(a, b); - }; - }; -} -*/!* - -// usage -function sum(a, b) { - return a + b; -} - -let carriedSum = curry(sum); - -alert( carriedSum(1)(2) ); // 3 -``` - -As you can see, the implementation is a series of wrappers. - -- The result of `curry(func)` is a wrapper `function(a)`. -- When it is called like `sum(1)`, the argument is saved in the Lexical Environment, and a new wrapper is returned `function(b)`. -- Then `sum(1)(2)` finally calls `function(b)` providing `2`, and it passes the call to the original multi-argument `sum`. - -More advanced implementations of currying like [_.curry](https://lodash.com/docs#curry) from lodash library do something more sophisticated. They return a wrapper that allows a function to be called normally when all arguments are supplied *or* returns a partial otherwise. - -```js -function curry(f) { - return function(...args) { - // if args.length == f.length (as many arguments as f has), - // then pass the call to f - // otherwise return a partial function that fixes args as first arguments - }; -} -``` - -## Currying? What for? - -To understand the benefits we definitely need a worthy real-life example. Advanced currying allows the function to be both callable normally and get partials. - -For instance, we have the logging function `log(date, importance, message)` that formats and outputs the information. In real projects such functions also have many other useful features like: sending it over the network or filtering: - -```js -function log(date, importance, message) { - alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`); -} -``` - -Let's curry it! - -```js -log = _.curry(log); -``` - -After that `log` still works the normal way: - -```js -log(new Date(), "DEBUG", "some debug"); -``` - -...But also can be called in the curried form: - -```js -log(new Date())("DEBUG")("some debug"); // log(a)(b)(c) -``` - -Let's get a convenience function for today's logs: - -```js -// todayLog will be the partial of log with fixed first argument -let todayLog = log(new Date()); - -// use it -todayLog("INFO", "message"); // [HH:mm] INFO message -``` - -And now a convenience function for today's debug messages: - -```js -let todayDebug = todayLog("DEBUG"); - -todayDebug("message"); // [HH:mm] DEBUG message -``` - -So: -1. We didn't lose anything after currying: `log` is still callable normally. -2. We were able to generate partial functions that are convenient in many cases. - -## Advanced curry implementation - -In case you're interested, here's the "advanced" curry implementation that we could use above, it's pretty short: - -```js run -function curry(func) { - - return function curried(...args) { - if (args.length >= func.length) { - return func.apply(this, args); - } else { - return function(...args2) { - return curried.apply(this, args.concat(args2)); - } - } - }; - -} - -function sum(a, b, c) { - return a + b + c; -} - -let curriedSum = curry(sum); - -// still callable normally -alert( curriedSum(1, 2, 3) ); // 6 - -// get the partial with curried(1) and call it with 2 other arguments -alert( curriedSum(1)(2,3) ); // 6 - -// full curried form -alert( curriedSum(1)(2)(3) ); // 6 -``` - -The new `curry` may look complicated, but it's actually easy to understand. - -The result of `curry(func)` is the wrapper `curried` that looks like this: - -```js -// func is the function to transform -function curried(...args) { - if (args.length >= func.length) { // (1) - return func.apply(this, args); - } else { - return function pass(...args2) { // (2) - return curried.apply(this, args.concat(args2)); - } - } -}; -``` - -When we run it, there are two branches: - -1. Call now: if passed `args` count is the same as the original function has in its definition (`func.length`) or longer, then just pass the call to it. -2. Get a partial: otherwise, `func` is not called yet. Instead, another wrapper `pass` is returned, that will re-apply `curried` providing previous arguments together with the new ones. Then on a new call, again, we'll get either a new partial (if not enough arguments) or, finally, the result. - -For instance, let's see what happens in the case of `sum(a, b, c)`. Three arguments, so `sum.length = 3`. - -For the call `curried(1)(2)(3)`: - -1. The first call `curried(1)` remembers `1` in its Lexical Environment, and returns a wrapper `pass`. -2. The wrapper `pass` is called with `(2)`: it takes previous args (`1`), concatenates them with what it got `(2)` and calls `curried(1, 2)` with them together. - - As the argument count is still less than 3, `curry` returns `pass`. -3. The wrapper `pass` is called again with `(3)`, for the next call `pass(3)` takes previous args (`1`, `2`) and adds `3` to them, making the call `curried(1, 2, 3)` -- there are `3` arguments at last, they are given to the original function. - -If that's still not obvious, just trace the calls sequence in your mind or on the paper. - -```smart header="Fixed-length functions only" -The currying requires the function to have a known fixed number of arguments. -``` - -```smart header="A little more than currying" -By definition, currying should convert `sum(a, b, c)` into `sum(a)(b)(c)`. - -But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant. -``` - -## Summary - -- When we fix some arguments of an existing function, the resulting (less universal) function is called *a partial*. We can use `bind` to get a partial, but there are other ways also. - - Partials are convenient when we don't want to repeat the same argument over and over again. Like if we have a `send(from, to)` function, and `from` should always be the same for our task, we can get a partial and go on with it. - -- *Currying* is a transform that makes `f(a,b,c)` callable as `f(a)(b)(c)`. JavaScript implementations usually both keep the function callable normally and return the partial if arguments count is not enough. - - Currying is great when we want easy partials. As we've seen in the logging example: the universal function `log(date, importance, message)` after currying gives us partials when called with one argument like `log(date)` or two arguments `log(date, importance)`. diff --git a/1-js/06-advanced-functions/12-arrow-functions/article.md b/1-js/06-advanced-functions/12-arrow-functions/article.md index 1ade1a41..abc5dd80 100644 --- a/1-js/06-advanced-functions/12-arrow-functions/article.md +++ b/1-js/06-advanced-functions/12-arrow-functions/article.md @@ -2,7 +2,7 @@ Let's revisit arrow functions. -Arrow functions are not just a "shorthand" for writing small stuff. +Arrow functions are not just a "shorthand" for writing small stuff. They have some very specific and useful features. JavaScript is full of situations where we need to write a small function, that's executed somewhere else. @@ -14,7 +14,7 @@ For instance: It's in the very spirit of JavaScript to create a function and pass it somewhere. -And in such functions we usually don't want to leave the current context. +And in such functions we usually don't want to leave the current context. That's where arrow functions come in handy. ## Arrow functions have no "this" 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 c44e5650..8ac5fd0d 100644 --- a/1-js/07-object-properties/01-property-descriptors/article.md +++ b/1-js/07-object-properties/01-property-descriptors/article.md @@ -63,7 +63,7 @@ Object.defineProperty(obj, propertyName, descriptor) ``` `obj`, `propertyName` -: The object and property to work on. +: The object and its property to apply the descriptor. `descriptor` : Property descriptor to apply. @@ -116,31 +116,34 @@ Object.defineProperty(user, "name", { }); *!* -user.name = "Pete"; // Error: Cannot assign to read only property 'name'... +user.name = "Pete"; // Error: Cannot assign to read only property 'name' */!* ``` Now no one can change the name of our user, unless they apply their own `defineProperty` to override ours. -Here's the same operation, but for the case when a property doesn't exist: +```smart header="Errors appear only in strict mode" +In the non-strict mode, no errors occur when writing to read-only properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict. +``` + +Here's the same example, but the property is created from scratch: ```js run let user = { }; Object.defineProperty(user, "name", { *!* - value: "Pete", + value: "John", // for new properties need to explicitly list what's true enumerable: true, configurable: true */!* }); -alert(user.name); // Pete -user.name = "Alice"; // Error +alert(user.name); // John +user.name = "Pete"; // Error ``` - ## Non-enumerable Now let's add a custom `toString` to `user`. @@ -239,10 +242,6 @@ Object.defineProperty(user, "name", {writable: true}); // Error */!* ``` -```smart header="Errors appear only in use strict" -In the non-strict mode, no errors occur when writing to read-only properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict. -``` - ## Object.defineProperties There's a method [Object.defineProperties(obj, descriptors)](mdn:js/Object/defineProperties) that allows to define many properties at once. @@ -298,14 +297,13 @@ Property descriptors work at the level of individual properties. There are also methods that limit access to the *whole* object: [Object.preventExtensions(obj)](mdn:js/Object/preventExtensions) -: Forbids to add properties to the object. +: Forbids the addition of new properties to the object. [Object.seal(obj)](mdn:js/Object/seal) -: Forbids to add/remove properties, sets for all existing properties `configurable: false`. +: Forbids adding/removing of properties. Sets `configurable: false` for all existing properties. [Object.freeze(obj)](mdn:js/Object/freeze) -: Forbids to add/remove/change properties, sets for all existing properties `configurable: false, writable: false`. - +: 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) 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 43cd5ae6..78fab9a6 100644 --- a/1-js/07-object-properties/02-property-accessors/article.md +++ b/1-js/07-object-properties/02-property-accessors/article.md @@ -3,7 +3,7 @@ There are two kinds of properties. -The first kind is *data properties*. We already know how to work with them. Actually, all properties that we've been using till now were data properties. +The 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 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. @@ -34,7 +34,7 @@ let user = { }; ``` -Now we want to add a "fullName" property, that should be "John Smith". Of course, we don't want to copy-paste existing information, so we can implement it as an accessor: +Now we want to add a `fullName` property, that should be `"John Smith"`. Of course, we don't want to copy-paste existing information, so we can implement it as an accessor: ```js run let user = { @@ -55,7 +55,19 @@ alert(user.fullName); // John Smith From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't *call* `user.fullName` as a function, we *read* it normally: the getter runs behind the scenes. -As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error. +As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error: + +```js run +let user = { + get fullName() { + return `...`; + } +}; + +*!* +user.fullName = "Test"; // Error (property has only a getter) +*/!* +``` Let's fix it by adding a setter for `user.fullName`: @@ -82,15 +94,10 @@ alert(user.name); // Alice alert(user.surname); // Cooper ``` -Now we have a "virtual" property. It is readable and writable, but in fact does not exist. +As the result, we have a "virtual" property `fullName`. It is readable and writable, but in fact does not exist. -```smart header="Accessor properties are only accessible with get/set" -Once a property is defined with `get prop()` or `set prop()`, it's an accessor property, not a data properety any more. - -- If there's a getter -- we can read `object.prop`, othrewise we can't. -- If there's a setter -- we can set `object.prop=...`, othrewise we can't. - -And in either case we can't `delete` an accessor property. +```smart header="No support for `delete`" +An attempt to `delete` on accessor property causes an error. ``` @@ -100,7 +107,7 @@ Descriptors for accessor properties are different -- as compared with data prope For accessor properties, there is no `value` and `writable`, but instead there are `get` and `set` functions. -So an accessor descriptor may have: +That is, an accessor descriptor may have: - **`get`** -- a function without arguments, that works when a property is read, - **`set`** -- a function with one argument, that is called when the property is set, @@ -132,7 +139,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 or a data property, not both. +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. If we try to supply both `get` and `value` in the same descriptor, there will be an error: @@ -151,9 +158,9 @@ Object.defineProperty({}, 'prop', { ## Smarter getters/setters -Getters/setters can be used as wrappers over "real" property values to gain more control over them. +Getters/setters can be used as wrappers over "real" property values to gain more control over operations with them. -For instance, if we want to forbid too short names for `user`, we can store `name` in a special property `_name`. And filter assignments in the setter: +For instance, if we want to forbid too short names for `user`, we can have a setter `name` and keep the value in a separate property `_name`: ```js run let user = { @@ -176,14 +183,16 @@ alert(user.name); // Pete user.name = ""; // Name is too short... ``` -Technically, the external code may still access the name directly by using `user._name`. But there is a widely known agreement that properties starting with an underscore `"_"` are internal and should not be touched from outside the object. +So, the name is stored in `_name` property, and the access is done via getter and setter. + +Technically, external code is able to access the name directly by using `user._name`. But there is a widely known convention that properties starting with an underscore `"_"` are internal and should not be touched from outside the object. ## Using for compatibility -One of the great ideas behind getters and setters -- they allow to take control over a "normal" data property and tweak it at any moment. +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. -For instance, 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) { @@ -209,9 +218,11 @@ let john = new User("John", new Date(1992, 6, 1)); Now what to do with the old code that still uses `age` property? -We can try to find all such places and fix them, but that takes time and can be hard to do if that code is written by other people. And besides, `age` is a nice thing to have in `user`, right? In some places it's just what we want. +We can try to find all such places and fix them, but that takes time and can be hard to do if that code is used by many other people. And besides, `age` is a nice thing to have in `user`, right? -Adding a getter for `age` mitigates the problem: +Let's keep it. + +Adding a getter for `age` solves the problem: ```js run no-beautify function User(name, birthday) { 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 002b24b8..421b57e0 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 an object: +We have objects: ```js let head = { diff --git a/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md b/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md index c7d147b9..4d6ea265 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md +++ b/1-js/08-prototypes/01-prototype-inheritance/3-proto-and-this/solution.md @@ -3,4 +3,5 @@ That's because `this` is an object before the dot, so `rabbit.eat()` modifies `rabbit`. Property lookup and execution are two different things. -The method `rabbit.eat` is first found in the prototype, then executed with `this=rabbit` + +The method `rabbit.eat` is first found in the prototype, then executed with `this=rabbit`. diff --git a/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/solution.md b/1-js/08-prototypes/01-prototype-inheritance/4-hamster-proto/solution.md index fad4b886..bd412f12 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 @@ -10,7 +10,7 @@ Let's look carefully at what's going on in the call `speedy.eat("apple")`. So all hamsters share a single stomach! -Every time the `stomach` is taken from the prototype, then `stomach.push` modifies it "at place". +Both for `lazy.stomach.push(...)` and `speedy.stomach.push()`, the property `stomach` is found in the prototype (as it's not in the object itself), then the new data is pushed into it. Please note that such thing doesn't happen in case of a simple assignment `this.stomach=`: @@ -77,4 +77,4 @@ alert( speedy.stomach ); // apple alert( lazy.stomach ); // ``` -As a common solution, all properties that describe the state of a particular object, like `stomach` above, are usually written into that object. That prevents such problems. +As a common solution, all properties that describe the state of a particular object, like `stomach` above, should be written into that object. That prevents such problems. diff --git a/1-js/08-prototypes/01-prototype-inheritance/article.md b/1-js/08-prototypes/01-prototype-inheritance/article.md index 1641de23..5895a0b3 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/article.md +++ b/1-js/08-prototypes/01-prototype-inheritance/article.md @@ -10,9 +10,9 @@ For instance, we have a `user` object with its properties and methods, and want In JavaScript, objects have a special hidden property `[[Prototype]]` (as named in the specification), that is either `null` or references another object. That object is called "a prototype": -![prototype](object-prototype-empty.png) +![prototype](object-prototype-empty.svg) -That `[[Prototype]]` has a "magical" meaning. When we want to read a property from `object`, and it's missing, JavaScript automatically takes it from the prototype. In programming, such thing is called "prototypal inheritance". Many cool language features and programming techniques are based on it. +The 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. The property `[[Prototype]]` is internal and hidden, but there are many ways to set it. @@ -66,7 +66,7 @@ Here the line `(*)` sets `animal` to be a prototype of `rabbit`. Then, when `alert` tries to read property `rabbit.eats` `(**)`, it's not in `rabbit`, so JavaScript follows the `[[Prototype]]` reference and finds it in `animal` (look from the bottom up): -![](proto-animal-rabbit.png) +![](proto-animal-rabbit.svg) Here we can say that "`animal` is the prototype of `rabbit`" or "`rabbit` prototypically inherits from `animal`". @@ -97,11 +97,10 @@ rabbit.walk(); // Animal walk The method is automatically taken from the prototype, like this: -![](proto-animal-rabbit-walk.png) +![](proto-animal-rabbit-walk.svg) The prototype chain can be longer: - ```js run let animal = { eats: true, @@ -129,12 +128,12 @@ longEar.walk(); // Animal walk alert(longEar.jumps); // true (from rabbit) ``` -![](proto-animal-rabbit-chain.png) +![](proto-animal-rabbit-chain.svg) -There are actually only two limitations: +There are only two limitations: 1. The references can't go in circles. JavaScript will throw an error if we try to assign `__proto__` in a circle. -2. The value of `__proto__` can be either an object or `null`, other types (like primitives) are ignored. +2. The value of `__proto__` can be either an object or `null`. Other types are ignored. Also it may be obvious, but still: there can be only one `[[Prototype]]`. An object may not inherit from two others. @@ -169,9 +168,9 @@ rabbit.walk(); // Rabbit! Bounce-bounce! From now on, `rabbit.walk()` call finds the method immediately in the object and executes it, without using the prototype: -![](proto-animal-rabbit-walk-2.png) +![](proto-animal-rabbit-walk-2.svg) -That's for data properties only, not for accessors. If a property is a getter/setter, then it behaves like a function: getters/setters are looked up in the prototype. +Accessor properties are an exception, as assignment is handled by a setter function. So writing to such a property is actually the same as calling a function. For that reason `admin.fullName` works correctly in the code below: @@ -245,16 +244,86 @@ alert(animal.isSleeping); // undefined (no such property in the prototype) The resulting picture: -![](proto-animal-rabbit-walk-3.png) +![](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 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. + +For instance: + +```js run +let animal = { + eats: true +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +*!* +// Object.keys only return own keys +alert(Object.keys(rabbit)); // jumps +*/!* + +*!* +// for..in loops over both own and inherited keys +for(let prop in rabbit) alert(prop); // jumps, then eats +*/!* +``` + +If that's not what we want, and we'd like to exclude inherited properties, there's a built-in method [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. + +So we can filter out inherited properties (or do something else with them): + +```js run +let animal = { + eats: true +}; + +let rabbit = { + jumps: true, + __proto__: animal +}; + +for(let prop in rabbit) { + let isOwn = rabbit.hasOwnProperty(prop); + + if (isOwn) { + alert(`Our: ${prop}`); // Our: jumps + } else { + alert(`Inherited: ${prop}`); // Inherited: eats + } +} +``` + +Here we have the following inheritance chain: `rabbit` inherits from `animal`, that inherits from `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it: + +![](rabbit-animal-object.svg) + +Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? We did not define it. Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited. + +...But why `hasOwnProperty` does not appear in `for..in` loop, like `eats` and `jumps`, if it lists all 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. + +```smart header="Almost all other key/value-getting methods ignore inherited properties" +Almost all other key/value-getting methods, such as `Object.keys`, `Object.values` and so on ignore inherited properties. + +They only operate on the object itself. Properties from the prototype are *not* taken into account. +``` + ## Summary - In JavaScript, all objects have a hidden `[[Prototype]]` property that's either another object or `null`. - We can use `obj.__proto__` to access it (a historical getter/setter, there are other ways, to be covered soon). - The object referenced by `[[Prototype]]` is called a "prototype". -- If we want to read a property of `obj` or call a method, and it doesn't exist, then JavaScript tries to find it in the prototype. Write/delete operations work directly on the object, they don't use the prototype (unless the property is actually a setter). +- 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). - 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. diff --git a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.png b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.png deleted file mode 100644 index d0a905b3..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg new file mode 100644 index 00000000..fe4c2cac --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty.svg @@ -0,0 +1 @@ +prototype objectobject[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty@2x.png b/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty@2x.png deleted file mode 100644 index 91a2d084..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/object-prototype-empty@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.png deleted file mode 100644 index 2b07f76d..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg new file mode 100644 index 00000000..3e81f262 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain.svg @@ -0,0 +1 @@ +eats: true walk: functionanimaljumps: truerabbit[[Prototype]]earLength: 10longEar[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain@2x.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain@2x.png deleted file mode 100644 index b3976f96..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-chain@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.png deleted file mode 100644 index 3c122d5f..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg new file mode 100644 index 00000000..3cf5c4c7 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2.svg @@ -0,0 +1 @@ +eats: true walk: functionanimalwalk: functionrabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png deleted file mode 100644 index 35db68f1..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-2@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.png deleted file mode 100644 index 33e71d8f..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg new file mode 100644 index 00000000..acd42063 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3.svg @@ -0,0 +1 @@ +walk: function sleep: functionanimalrabbit[[Prototype]]name: "White Rabbit" isSleeping: true \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png deleted file mode 100644 index 29d6883c..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk-3@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.png deleted file mode 100644 index 3b26a582..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg new file mode 100644 index 00000000..ebdef958 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk.svg @@ -0,0 +1 @@ +eats: true walk: functionanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk@2x.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk@2x.png deleted file mode 100644 index 66aaa501..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit-walk@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.png deleted file mode 100644 index eae25e0d..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg new file mode 100644 index 00000000..735e1f2b --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit.svg @@ -0,0 +1 @@ +eats: trueanimaljumps: truerabbit[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit@2x.png b/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit@2x.png deleted file mode 100644 index 39194571..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.png b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.png deleted file mode 100644 index dae56a32..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg new file mode 100644 index 00000000..433bc613 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin.svg @@ -0,0 +1 @@ +name: "John" surname: "Smith" set fullName: functionisAdmin: true name: "Alice" surname: "Cooper"useradmin[[Prototype]] \ No newline at end of file diff --git a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin@2x.png b/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin@2x.png deleted file mode 100644 index 6443eee6..00000000 Binary files a/1-js/08-prototypes/01-prototype-inheritance/proto-user-admin@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg new file mode 100644 index 00000000..d32585b4 --- /dev/null +++ b/1-js/08-prototypes/01-prototype-inheritance/rabbit-animal-object.svg @@ -0,0 +1 @@ +toString: function hasOwnProperty: function ...Object.prototypeanimal[[Prototype]][[Prototype]][[Prototype]]nulleats: truerabbitjumps: true \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md index 771e3061..ebbdf3a7 100644 --- a/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md +++ b/1-js/08-prototypes/02-function-prototype/1-changing-prototype/solution.md @@ -7,7 +7,7 @@ Answers: 2. `false`. - Objects are assigned by reference. The object from `Rabbit.prototype` is not duplicated, it's still a single object is referenced both by `Rabbit.prototype` and by the `[[Prototype]]` of `rabbit`. + Objects are assigned by reference. The object from `Rabbit.prototype` is not duplicated, it's still a single object referenced both by `Rabbit.prototype` and by the `[[Prototype]]` of `rabbit`. So when we change its content through one reference, it is visible through the other one. diff --git a/1-js/08-prototypes/02-function-prototype/article.md b/1-js/08-prototypes/02-function-prototype/article.md index 6de6e03e..29b3773e 100644 --- a/1-js/08-prototypes/02-function-prototype/article.md +++ b/1-js/08-prototypes/02-function-prototype/article.md @@ -36,14 +36,14 @@ Setting `Rabbit.prototype = animal` literally states the following: "When a `new That's the resulting picture: -![](proto-constructor-animal-rabbit.png) +![](proto-constructor-animal-rabbit.svg) On the picture, `"prototype"` is a horizontal arrow, meaning a regular property, and `[[Prototype]]` is vertical, meaning the inheritance of `rabbit` from `animal`. ```smart header="`F.prototype` only used at `new F` time" -`F.prototype` 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. After that, there's no connection between `F.prototype` and the new object. Think of it as a "one-time gift". -After the creation, `F.prototype` may change, new objects created by `new F` will have another `[[Prototype]]`, but already existing objects keep the old one. +If, after the creation, `F.prototype` property changes (`F.prototype = `), then new objects created by `new F` will have another object as `[[Prototype]]`, but already existing objects keep the old one. ``` ## Default F.prototype, constructor property @@ -62,7 +62,7 @@ Rabbit.prototype = { constructor: Rabbit }; */ ``` -![](function-prototype-constructor.png) +![](function-prototype-constructor.svg) We can check it: @@ -86,7 +86,7 @@ let rabbit = new Rabbit(); // inherits from {constructor: Rabbit} alert(rabbit.constructor == Rabbit); // true (from prototype) ``` -![](rabbit-prototype-constructor.png) +![](rabbit-prototype-constructor.svg) We can use `constructor` property to create a new object using the same constructor as the existing one. @@ -160,9 +160,9 @@ In this chapter we briefly described the way of setting a `[[Prototype]]` for ob Everything is quite simple, just few notes to make things clear: -- The `F.prototype` property is not the same as `[[Prototype]]`. The only thing `F.prototype` does: it sets `[[Prototype]]` of new objects when `new F()` is called. -- The value of `F.prototype` should be either an object or null: other values won't work. -- The `"prototype"` property only has such a special effect when is set to a constructor function, and invoked with `new`. +- The `F.prototype` property (don't mess with `[[Prototype]]`) sets `[[Prototype]]` of new objects when `new F()` is called. +- The value of `F.prototype` should be either an object or `null`: other values won't work. +- The `"prototype"` property only has such a special effect when set on a constructor function, and invoked with `new`. On regular objects the `prototype` is nothing special: ```js diff --git a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.png b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.png deleted file mode 100644 index 92f80cea..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg new file mode 100644 index 00000000..35cdc61f --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor.svg @@ -0,0 +1 @@ +Rabbitprototypeconstructordefault "prototype" \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor@2x.png b/1-js/08-prototypes/02-function-prototype/function-prototype-constructor@2x.png deleted file mode 100644 index d8c83f6e..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/function-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/native-prototypes-array-tostring.png b/1-js/08-prototypes/02-function-prototype/native-prototypes-array-tostring.png deleted file mode 100644 index 02f0e747..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/native-prototypes-array-tostring.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/native-prototypes-array-tostring@2x.png b/1-js/08-prototypes/02-function-prototype/native-prototypes-array-tostring@2x.png deleted file mode 100644 index 7abe2c93..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/native-prototypes-array-tostring@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/native-prototypes-classes.png b/1-js/08-prototypes/02-function-prototype/native-prototypes-classes.png deleted file mode 100644 index 03737f2d..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/native-prototypes-classes.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/native-prototypes-classes@2x.png b/1-js/08-prototypes/02-function-prototype/native-prototypes-classes@2x.png deleted file mode 100644 index a2e9390e..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/native-prototypes-classes@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/object-prototype-1.png b/1-js/08-prototypes/02-function-prototype/object-prototype-1.png deleted file mode 100644 index b0ff1532..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/object-prototype-1.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/object-prototype-1@2x.png b/1-js/08-prototypes/02-function-prototype/object-prototype-1@2x.png deleted file mode 100644 index 5d4365cf..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/object-prototype-1@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/object-prototype.png b/1-js/08-prototypes/02-function-prototype/object-prototype.png deleted file mode 100644 index 581105aa..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/object-prototype.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/object-prototype@2x.png b/1-js/08-prototypes/02-function-prototype/object-prototype@2x.png deleted file mode 100644 index 4f0e5330..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/object-prototype@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.png b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.png deleted file mode 100644 index 2a745e8f..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg new file mode 100644 index 00000000..3489ecdd --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit.svg @@ -0,0 +1 @@ +eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit@2x.png b/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit@2x.png deleted file mode 100644 index 609ac141..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/proto-constructor-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-animal-object.png b/1-js/08-prototypes/02-function-prototype/rabbit-animal-object.png deleted file mode 100644 index e1758e9f..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/rabbit-animal-object.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-animal-object@2x.png b/1-js/08-prototypes/02-function-prototype/rabbit-animal-object@2x.png deleted file mode 100644 index 2d282a9d..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/rabbit-animal-object@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.png b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.png deleted file mode 100644 index fe71481c..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.png and /dev/null differ diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg new file mode 100644 index 00000000..3e11f275 --- /dev/null +++ b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor.svg @@ -0,0 +1 @@ +default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file diff --git a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor@2x.png b/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor@2x.png deleted file mode 100644 index 09f7631f..00000000 Binary files a/1-js/08-prototypes/02-function-prototype/rabbit-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/article.md b/1-js/08-prototypes/03-native-prototypes/article.md index a3fdfc6c..66be00ca 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. -We'll see how it is for plain objects first, and then for more complex ones. +First we'll see at the details, and then how to use it for adding new capabilities to built-in objects. ## Object.prototype @@ -15,17 +15,17 @@ alert( obj ); // "[object Object]" ? Where's the code that generates the string `"[object Object]"`? That's a built-in `toString` method, but where is it? The `obj` is empty! -...But the short notation `obj = {}` is the same as `obj = new Object()`, where `Object` -- is a built-in object constructor function. And that function has `Object.prototype` that references a huge object with `toString` and other functions. +...But the short notation `obj = {}` is the same as `obj = new Object()`, where `Object` is a built-in object constructor function, with its own `prototype` referencing a huge object with `toString` and other methods. -Like this (all that is built-in): +Here's what's going on: -![](object-prototype.png) +![](object-prototype.svg) -When `new Object()` is called (or a literal object `{...}` is created), the `[[Prototype]]` of it is set to `Object.prototype` by the rule that we've discussed in the previous chapter: +When `new Object()` is called (or a literal object `{...}` is created), the `[[Prototype]]` of it is set to `Object.prototype` according to the rule that we discussed in the previous chapter: -![](object-prototype-1.png) +![](object-prototype-1.svg) -Afterwards when `obj.toString()` is called -- the method is taken from `Object.prototype`. +So then when `obj.toString()` is called the method is taken from `Object.prototype`. We can check it like this: @@ -36,7 +36,7 @@ alert(obj.__proto__ === Object.prototype); // true // obj.toString === obj.__proto__.toString == Object.prototype.toString ``` -Please note that there is no additional `[[Prototype]]` in the chain above `Object.prototype`: +Please note that there is no more `[[Prototype]]` in the chain above `Object.prototype`: ```js run alert(Object.prototype.__proto__); // null @@ -46,13 +46,13 @@ alert(Object.prototype.__proto__); // null Other built-in objects such as `Array`, `Date`, `Function` and others also keep methods in prototypes. -For instance, when we create an array `[1, 2, 3]`, the default `new Array()` constructor is used internally. So the array data is written into the new object, and `Array.prototype` becomes its prototype and provides methods. That's very memory-efficient. +For instance, when we create an array `[1, 2, 3]`, the default `new Array()` constructor is used internally. So `Array.prototype` becomes its prototype and provides methods. That's very memory-efficient. -By specification, all built-in prototypes have `Object.prototype` on the top. Sometimes people say that "everything inherits from objects". +By specification, all of the built-in prototypes have `Object.prototype` on the top. That's why some people say that "everything inherits from objects". Here's the overall picture (for 3 built-ins to fit): -![](native-prototypes-classes.png) +![](native-prototypes-classes.svg) Let's check the prototypes manually: @@ -79,14 +79,14 @@ alert(arr); // 1,2,3 <-- the result of Array.prototype.toString As we've seen before, `Object.prototype` has `toString` as well, but `Array.prototype` is closer in the chain, so the array variant is used. -![](native-prototypes-array-tostring.png) +![](native-prototypes-array-tostring.svg) -In-browser tools like Chrome developer console also show inheritance (may need to use `console.dir` for built-in objects): +In-browser tools like Chrome developer console also show inheritance (`console.dir` may need to be used for built-in objects): ![](console_dir_array.png) -Other built-in objects also work the same way. Even functions. They are objects of a built-in `Function` constructor, and their methods: `call/apply` and others are taken from `Function.prototype`. Functions have their own `toString` too. +Other built-in objects also work the same way. Even functions -- they are objects of a built-in `Function` constructor, and their methods (`call`/`apply` and others) are taken from `Function.prototype`. Functions have their own `toString` too. ```js run function f() {} @@ -119,17 +119,17 @@ String.prototype.show = function() { "BOOM!".show(); // BOOM! ``` -During the process of development we may have ideas which new built-in methods we'd like to have. And there may be a slight temptation to add them to native prototypes. But that is generally a bad idea. +During the process of development, we may have ideas for new built-in methods we'd like to have, and we may be tempted to add them to native prototypes. But that is generally a bad idea. ```warn -Prototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them overwrites the other one. +Prototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them will be overwriting the method of the other. -So, generally modifying a native prototypeis considered a bad idea. +So, generally, modifying a native prototype is considered a bad idea. ``` -**In modern programming, there is only one case when modifying native prototypes is approved. That's polyfilling.** +**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 JavaScript specification, but not yet supported by current JavaScript engine. Then we may implement it manually and populate the built-in prototype with it. @@ -144,7 +144,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 + // but even an imperfect polyfill is often considered good enough for use return new Array(n + 1).join(this); }; } @@ -161,7 +161,7 @@ That's when we take a method from one object and copy it into another. Some methods of native prototypes are often borrowed. -For instance, if we're making an array-like object, we may want to copy some array methods to it. +For instance, if we're making an array-like object, we may want to copy some `Array` methods to it. E.g. @@ -181,7 +181,7 @@ 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. -Another possibility is to inherit by setting `obj.__proto__` to `Array.prototype`, then all `Array` methods are automatically available in `obj`. +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. @@ -192,5 +192,5 @@ Borrowing methods is flexible, it allows to mix functionality from different obj - All built-in objects follow the same pattern: - The methods are stored in the prototype (`Array.prototype`, `Object.prototype`, `Date.prototype` etc). - The object itself stores only the data (array items, object properties, the date). -- Primitives also store methods in prototypes of wrapper objects: `Number.prototype`, `String.prototype`, `Boolean.prototype`. There are no wrapper objects only for `undefined` and `null`. +- 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. diff --git a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.png b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.png deleted file mode 100644 index 92f80cea..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg new file mode 100644 index 00000000..35cdc61f --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor.svg @@ -0,0 +1 @@ +Rabbitprototypeconstructordefault "prototype" \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor@2x.png b/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor@2x.png deleted file mode 100644 index d8c83f6e..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/function-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.png b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.png deleted file mode 100644 index 02f0e747..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg new file mode 100644 index 00000000..770c908c --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring.svg @@ -0,0 +1 @@ +toString: function ...Array.prototypetoString: function ...Object.prototype[[Prototype]][[Prototype]][1, 2, 3] \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring@2x.png b/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring@2x.png deleted file mode 100644 index 7abe2c93..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/native-prototypes-array-tostring@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.png b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.png deleted file mode 100644 index 03737f2d..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg new file mode 100644 index 00000000..4989df56 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg @@ -0,0 +1 @@ +toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes@2x.png b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes@2x.png deleted file mode 100644 index a2e9390e..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.png b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.png deleted file mode 100644 index b0ff1532..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg new file mode 100644 index 00000000..38c33cae --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-1.svg @@ -0,0 +1 @@ +constructor: Object toString: function ...Object.prototypeObjectobj = new Object()[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-1@2x.png b/1-js/08-prototypes/03-native-prototypes/object-prototype-1@2x.png deleted file mode 100644 index 5d4365cf..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/object-prototype-1@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.png b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.png deleted file mode 100644 index 9115d5f3..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg new file mode 100644 index 00000000..858f8317 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype-null.svg @@ -0,0 +1 @@ +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype-null@2x.png b/1-js/08-prototypes/03-native-prototypes/object-prototype-null@2x.png deleted file mode 100644 index 3120a8d5..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/object-prototype-null@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype.png b/1-js/08-prototypes/03-native-prototypes/object-prototype.png deleted file mode 100644 index 581105aa..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/object-prototype.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype.svg b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg new file mode 100644 index 00000000..8d3d0bee --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/object-prototype.svg @@ -0,0 +1 @@ +constructor: Object toString: function ...Object.prototypeObjectprototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/object-prototype@2x.png b/1-js/08-prototypes/03-native-prototypes/object-prototype@2x.png deleted file mode 100644 index 4f0e5330..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/object-prototype@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.png b/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.png deleted file mode 100644 index 2a745e8f..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.png and /dev/null differ 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 new file mode 100644 index 00000000..3489ecdd --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit.svg @@ -0,0 +1 @@ +eats: truename: "White Rabbit"animalRabbitrabbit[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit@2x.png b/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit@2x.png deleted file mode 100644 index 609ac141..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/proto-constructor-animal-rabbit@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.png b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.png deleted file mode 100644 index fe71481c..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.png and /dev/null differ diff --git a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg new file mode 100644 index 00000000..3e11f275 --- /dev/null +++ b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor.svg @@ -0,0 +1 @@ +default "prototype"Rabbitrabbit[[Prototype]]prototypeconstructor \ No newline at end of file diff --git a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor@2x.png b/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor@2x.png deleted file mode 100644 index 09f7631f..00000000 Binary files a/1-js/08-prototypes/03-native-prototypes/rabbit-prototype-constructor@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/04-prototype-methods/article.md b/1-js/08-prototypes/04-prototype-methods/article.md index 3a925ab1..8a71dbf1 100644 --- a/1-js/08-prototypes/04-prototype-methods/article.md +++ b/1-js/08-prototypes/04-prototype-methods/article.md @@ -3,7 +3,7 @@ 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). +The `__proto__` is considered outdated and somewhat deprecated (in browser-only part of the JavaScript standard). The modern methods are: @@ -26,6 +26,7 @@ let rabbit = Object.create(animal); */!* alert(rabbit.eats); // true + *!* alert(Object.getPrototypeOf(rabbit) === animal); // get the prototype of rabbit */!* @@ -72,19 +73,19 @@ That's for historical reasons. - The `"prototype"` property of a constructor function works since very ancient times. - Later in the year 2012: `Object.create` appeared in the standard. It allowed to create objects with the given prototype, but did not allow to get/set it. So browsers implemented non-standard `__proto__` accessor that allowed to get/set a prototype at any time. -- Later in the year 2015: `Object.setPrototypeOf` and `Object.getPrototypeOf` were added to the standard. The `__proto__` was de-facto implemented everywhere, so it made its way to the Annex B of the standard, that is optional for non-browser environments. +- Later in the year 2015: `Object.setPrototypeOf` and `Object.getPrototypeOf` were added to the standard, to perform the same functionality as `__proto__`. As `__proto__` was de-facto implemented everywhere, it was kind-of deprecated and made its way to the Annex B of the standard, that is optional for non-browser environments. As of now we have all these ways at our disposal. -Why `__proto__` was replaced by the functions? That's an interesting question, requiring us to understand why `__proto__` is bad. Read on to get the answer. +Why was `__proto__` replaced by the functions `getPrototypeOf/setPrototypeOf`? That's an interesting question, requiring us to understand why `__proto__` is bad. Read on to get the answer. -```warn header="Don't reset `[[Prototype]]` unless the speed doesn't matter" +```warn header="Don't change `[[Prototype]]` on existing objects if speed matters" Technically, we can get/set `[[Prototype]]` at any time. But usually we only set it once at the object creation time, and then do not modify: `rabbit` inherits from `animal`, and that is not going to change. -And JavaScript engines are highly optimized to that. Changing a prototype "on-the-fly" with `Object.setPrototypeOf` or `obj.__proto__=` is a very slow operation, it breaks internal optimizations for object property access operations. So evade it unless you know what you're doing, or Javascript speed totally doesn't matter for you. +And JavaScript engines are highly optimized to that. Changing a prototype "on-the-fly" with `Object.setPrototypeOf` or `obj.__proto__=` is a very slow operation, it breaks internal optimizations for object property access operations. So evade it unless you know what you're doing, or JavaScript speed totally doesn't matter for you. ``` -## "Very plain" objects +## "Very plain" objects [#very-plain] As we know, objects can be used as associative arrays to store key/value pairs. @@ -107,11 +108,11 @@ That shouldn't surprise us. The `__proto__` property is special: it must be eith But we didn't *intend* to implement such behavior, right? We want to store key/value pairs, and the key named `"__proto__"` was not properly saved. So that's a bug! -Here the consequences are not terrible. But in other cases the prototype may indeed be changed, so the execution may go wrong in totally unexpected ways. +Here the consequences are not terrible. But in other cases, we may be assigning object values, then the prototype may indeed be changed. As the result, the execution will go wrong in totally unexpected ways. What's worst -- usually developers do not think about such possibility at all. That makes such bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on server-side. -Unexpected things also may happen when accessing `toString` property -- that's a function by default, and other built-in properties. +Unexpected things also may happen when assigning to `toString` -- that's a function by default, and other built-in methods. How to evade the problem? @@ -121,7 +122,7 @@ But `Object` also can serve us well here, because language creators gave a thoug The `__proto__` is not a property of an object, but an accessor property of `Object.prototype`: -![](object-prototype-2.png) +![](object-prototype-2.svg) So, if `obj.__proto__` is read or set, the corresponding getter/setter is called from its prototype, and it gets/sets `[[Prototype]]`. @@ -142,7 +143,7 @@ alert(obj[key]); // "some value" `Object.create(null)` creates an empty object without a prototype (`[[Prototype]]` is `null`): -![](object-prototype-null.png) +![](object-prototype-null.svg) So, there is no inherited getter/setter for `__proto__`. Now it is processed as a regular data property, so the example above works right. @@ -160,7 +161,7 @@ alert(obj); // Error (no toString) ...But that's usually fine for associative arrays. -Please note that most object-related methods are `Object.something(...)`, like `Object.keys(obj)` -- they are not in the prototype, so they will keep working on such objects: +Note that most object-related methods are `Object.something(...)`, like `Object.keys(obj)` -- they are not in the prototype, so they will keep working on such objects: ```js run @@ -179,7 +180,7 @@ Modern methods to setup and directly access the prototype are: - [Object.getPrototypeOf(obj)](mdn:js/Object.getPrototypeOf) -- returns the `[[Prototype]]` of `obj` (same as `__proto__` getter). - [Object.setPrototypeOf(obj, proto)](mdn:js/Object.setPrototypeOf) -- sets the `[[Prototype]]` of `obj` to `proto` (same as `__proto__` setter). -The built-in `__proto__` getter/setter is unsafe if we'd want to put user-generated keys in to an object. Just because a user may enter "__proto__" as the key, and there'll be an error with hopefully easy, but generally unpredictable consequences. +The built-in `__proto__` getter/setter is unsafe if we'd want to put user-generated keys in to an object. Just because a user may enter `"__proto__"` as the key, and there'll be an error, with hopefully light, but generally unpredictable consequences. So we can either use `Object.create(null)` to create a "very plain" object without `__proto__`, or stick to `Map` objects for that. @@ -189,15 +190,16 @@ Also, `Object.create` provides an easy way to shallow-copy an object with all de let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); ``` - -- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. -- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names. -- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names. -- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names. -- [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. - We also made it clear that `__proto__` is a getter/setter for `[[Prototype]]` and resides in `Object.prototype`, just as other methods. We can create an object without a prototype by `Object.create(null)`. Such objects are used as "pure dictionaries", they have no issues with `"__proto__"` as the key. +Other methods: + +- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. +- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic keys. +- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string keys. +- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own keys. +- [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) key named `key`. + All methods that return object properties (like `Object.keys` and others) -- return "own" properties. If we want inherited ones, then we can use `for..in`. diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-2.png b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.png deleted file mode 100644 index 8f26210d..00000000 Binary files a/1-js/08-prototypes/04-prototype-methods/object-prototype-2.png and /dev/null differ diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg new file mode 100644 index 00000000..86d09ae0 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-2.svg @@ -0,0 +1 @@ +... get __proto__: function set __proto__: functionObject.prototypeObjectobj[[Prototype]]prototype \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-2@2x.png b/1-js/08-prototypes/04-prototype-methods/object-prototype-2@2x.png deleted file mode 100644 index e42d3947..00000000 Binary files a/1-js/08-prototypes/04-prototype-methods/object-prototype-2@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.png b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.png deleted file mode 100644 index 9115d5f3..00000000 Binary files a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.png and /dev/null differ diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg new file mode 100644 index 00000000..858f8317 --- /dev/null +++ b/1-js/08-prototypes/04-prototype-methods/object-prototype-null.svg @@ -0,0 +1 @@ +obj[[Prototype]]null \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/object-prototype-null@2x.png b/1-js/08-prototypes/04-prototype-methods/object-prototype-null@2x.png deleted file mode 100644 index 3120a8d5..00000000 Binary files a/1-js/08-prototypes/04-prototype-methods/object-prototype-null@2x.png and /dev/null differ diff --git a/1-js/08-prototypes/05-getting-all-properties/article.md b/1-js/08-prototypes/05-getting-all-properties/article.md deleted file mode 100644 index 3339066d..00000000 --- a/1-js/08-prototypes/05-getting-all-properties/article.md +++ /dev/null @@ -1,83 +0,0 @@ - -# Getting all properties - -There are many ways to get keys/values from an object. - -Most of them operate on the object itself, excluding the prototype, let's recall them: - -- [Object.keys(obj)](mdn:js/Object/keys) / [Object.values(obj)](mdn:js/Object/values) / [Object.entries(obj)](mdn:js/Object/entries) -- returns an array of enumerable own string property names/values/key-value pairs. These methods only list *enumerable* properties, and those that have *strings as keys*. - -If we want symbolic properties: - -- [Object.getOwnPropertySymbols(obj)](mdn:js/Object/getOwnPropertySymbols) -- returns an array of all own symbolic property names. - -If we want non-enumerable properties: - -- [Object.getOwnPropertyNames(obj)](mdn:js/Object/getOwnPropertyNames) -- returns an array of all own string property names. - -If we want *all* properties: - -- [Reflect.ownKeys(obj)](mdn:js/Reflect/ownKeys) -- returns an array of all own property names. - -These methods are a bit different about which properties they return, but all of them operate on the object itself. Properties from the prototype are not listed. - -## for..in loop - -The `for..in` loop is different: it loops over inherited properties too. - -For instance: - -```js run -let animal = { - eats: true -}; - -let rabbit = { - jumps: true, - __proto__: animal -}; - -*!* -// only own keys -alert(Object.keys(rabbit)); // jumps -*/!* - -*!* -// inherited keys too -for(let prop in rabbit) alert(prop); // jumps, then eats -*/!* -``` - -If that's no 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`. - -So we can filter out inherited properties (or do something else with them): - -```js run -let animal = { - eats: true -}; - -let rabbit = { - jumps: true, - __proto__: animal -}; - -for(let prop in rabbit) { - let isOwn = rabbit.hasOwnProperty(prop); - alert(`${prop}: ${isOwn}`); // jumps: true, then eats: false -} -``` - -Here we have the following inheritance chain: `rabbit`, then `animal`, then `Object.prototype` (because `animal` is a literal object `{...}`, so it's by default), and then `null` above it: - -![](rabbit-animal-object.png) - -Note, there's one funny thing. Where is the method `rabbit.hasOwnProperty` coming from? Looking at the chain we can see that the method is provided by `Object.prototype.hasOwnProperty`. In other words, it's inherited. - -...But why `hasOwnProperty` does not appear in `for..in` loop, if it lists all inherited properties? The answer is simple: it's not enumerable. Just like all other properties of `Object.prototype`. That's why they are not listed. - -## Summary - -Most methods ignore inherited properties, with a notable exception of `for..in`. - -For the latter we can use [obj.hasOwnProperty(key)](mdn:js/Object/hasOwnProperty): it returns `true` if `obj` has its own (not inherited) property named `key`. diff --git a/1-js/08-prototypes/05-getting-all-properties/rabbit-animal-object.png b/1-js/08-prototypes/05-getting-all-properties/rabbit-animal-object.png deleted file mode 100644 index e1758e9f..00000000 Binary files a/1-js/08-prototypes/05-getting-all-properties/rabbit-animal-object.png and /dev/null differ diff --git a/1-js/08-prototypes/05-getting-all-properties/rabbit-animal-object@2x.png b/1-js/08-prototypes/05-getting-all-properties/rabbit-animal-object@2x.png deleted file mode 100644 index 2d282a9d..00000000 Binary files a/1-js/08-prototypes/05-getting-all-properties/rabbit-animal-object@2x.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/1-inheritance-error-assign/solution.md b/1-js/09-classes/01-class-patterns/1-inheritance-error-assign/solution.md deleted file mode 100644 index 55f945ca..00000000 --- a/1-js/09-classes/01-class-patterns/1-inheritance-error-assign/solution.md +++ /dev/null @@ -1,46 +0,0 @@ -Here's the line with the error: - -```js -Rabbit.prototype = Animal.prototype; -``` - -Here `Rabbit.prototype` and `Animal.prototype` become the same object. So methods of both classes become mixed in that object. - -As a result, `Rabbit.prototype.walk` overwrites `Animal.prototype.walk`, so all animals start to bounce: - -```js run -function Animal(name) { - this.name = name; -} - -Animal.prototype.walk = function() { - alert(this.name + ' walks'); -}; - -function Rabbit(name) { - this.name = name; -} - -*!* -Rabbit.prototype = Animal.prototype; -*/!* - -Rabbit.prototype.walk = function() { - alert(this.name + " bounces!"); -}; - -*!* -let animal = new Animal("pig"); -animal.walk(); // pig bounces! -*/!* -``` - -The correct variant would be: - -```js -Rabbit.prototype.__proto__ = Animal.prototype; -// or like this: -Rabbit.prototype = Object.create(Animal.prototype); -``` - -That makes prototypes separate, each of them stores methods of the corresponding class, but `Rabbit.prototype` inherits from `Animal.prototype`. diff --git a/1-js/09-classes/01-class-patterns/1-inheritance-error-assign/task.md b/1-js/09-classes/01-class-patterns/1-inheritance-error-assign/task.md deleted file mode 100644 index ee486c3d..00000000 --- a/1-js/09-classes/01-class-patterns/1-inheritance-error-assign/task.md +++ /dev/null @@ -1,29 +0,0 @@ -importance: 5 - ---- - -# An error in the inheritance - -Find an error in the prototypal inheritance below. - -What's wrong? What are consequences going to be? - -```js -function Animal(name) { - this.name = name; -} - -Animal.prototype.walk = function() { - alert(this.name + ' walks'); -}; - -function Rabbit(name) { - this.name = name; -} - -Rabbit.prototype = Animal.prototype; - -Rabbit.prototype.walk = function() { - alert(this.name + " bounces!"); -}; -``` diff --git a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.md b/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.md deleted file mode 100644 index 300b25d9..00000000 --- a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.md +++ /dev/null @@ -1 +0,0 @@ -Please note that properties that were internal in functional style (`template`, `timer`) and the internal method `render` are marked private with the underscore `_`. diff --git a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js b/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js deleted file mode 100644 index bdf7bb72..00000000 --- a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.view/clock.js +++ /dev/null @@ -1,32 +0,0 @@ -function Clock({ template }) { - this.template = template; -} - -Clock.prototype.render = function() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) mins = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this.template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this.timer); -}; - -Clock.prototype.start = function() { - this.render(); - this.timer = setInterval(() => this.render(), 1000); -}; diff --git a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.view/index.html b/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.view/index.html deleted file mode 100644 index fdee13d0..00000000 --- a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/solution.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/source.view/index.html b/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/source.view/index.html deleted file mode 100644 index fdee13d0..00000000 --- a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/source.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/task.md b/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/task.md deleted file mode 100644 index 71131816..00000000 --- a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/task.md +++ /dev/null @@ -1,9 +0,0 @@ -importance: 5 - ---- - -# Rewrite to prototypes - -The `Clock` class is written in functional style. Rewrite it using prototypes. - -P.S. The clock ticks in the console, open it to see. diff --git a/1-js/09-classes/01-class-patterns/article.md b/1-js/09-classes/01-class-patterns/article.md deleted file mode 100644 index 837941cb..00000000 --- a/1-js/09-classes/01-class-patterns/article.md +++ /dev/null @@ -1,240 +0,0 @@ - -# Class patterns - -```quote author="Wikipedia" -In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods). -``` - -There's a special syntax construct and a keyword `class` in JavaScript. But before studying it, we should consider that the term "class" comes from the theory of object-oriented programming. The definition is cited above, and it's language-independent. - -In JavaScript there are several well-known programming patterns to make classes even without using the `class` keyword. People talk about "classes" meaning no only those defined with `class`, but also with these patterns. - -The `class` construct will be described in the next chapter, but in JavaScript it's a "syntax sugar" and an extension of the prototypal class pattern described here. - - -## Functional class pattern - -The constructor function below can be considered a "class" according to the definition: - -```js run -function User(name) { - this.sayHi = function() { - alert(name); - }; -} - -let user = new User("John"); -user.sayHi(); // John -``` - -It follows all parts of the definition: - -1. It is a "program-code-template" for creating objects (callable with `new`). -2. It provides initial values for the state (`name` from parameters). -3. It provides methods (`sayHi`). - -This is called *functional class pattern*. - -In the functional class pattern, local variables and nested functions inside `User`, that are not assigned to `this`, are visible from inside, but not accessible by the outer code. - -So we can easily add internal functions and variables, like `calcAge()` here: - -```js run -function User(name, birthday) { -*!* - // only visible from other methods inside User - function calcAge() { - return new Date().getFullYear() - birthday.getFullYear(); - } -*/!* - - this.sayHi = function() { - alert(`${name}, age:${calcAge()}`); - }; -} - -let user = new User("John", new Date(2000, 0, 1)); -user.sayHi(); // John, age:17 -``` - -In this code variables `name`, `birthday` and the function `calcAge()` are internal, *private* to the object. They are only visible from inside of it. - -On the other hand, `sayHi` is the external, *public* method. The external code that creates `user` can access it. - -This way we can hide internal implementation details and helper methods from the outer code. Only what's assigned to `this` becomes visible outside. - -## Factory class pattern - -We can create a class without using `new` at all. - -Like this: - -```js run -function User(name, birthday) { - // only visible from other methods inside User - function calcAge() { - return new Date().getFullYear() - birthday.getFullYear(); - } - - return { - sayHi() { - alert(`${name}, age:${calcAge()}`); - } - }; -} - -*!* -let user = User("John", new Date(2000, 0, 1)); -*/!* -user.sayHi(); // John, age:17 -``` - -As we can see, the function `User` returns an object with public properties and methods. The only benefit of this method is that we can omit `new`: write `let user = User(...)` instead of `let user = new User(...)`. In other aspects it's almost the same as the functional pattern. - -## Prototype-based classes - -Prototype-based classes are the most important and generally the best. Functional and factory class patterns are rarely used in practice. - -Soon you'll see why. - -Here's the same class rewritten using prototypes: - -```js run -function User(name, birthday) { -*!* - this._name = name; - this._birthday = birthday; -*/!* -} - -*!* -User.prototype._calcAge = function() { -*/!* - return new Date().getFullYear() - this._birthday.getFullYear(); -}; - -User.prototype.sayHi = function() { - alert(`${this._name}, age:${this._calcAge()}`); -}; - -let user = new User("John", new Date(2000, 0, 1)); -user.sayHi(); // John, age:17 -``` - -The code structure: - -- The constructor `User` only initializes the current object state. -- Methods are added to `User.prototype`. - -As we can see, methods are lexically not inside `function User`, they do not share a common lexical environment. If we declare variables inside `function User`, then they won't be visible to methods. - -So, there is a widely known agreement that internal properties and methods are prepended with an underscore `"_"`. Like `_name` or `_calcAge()`. Technically, that's just an agreement, the outer code still can access them. But most developers recognize the meaning of `"_"` and try not to touch prefixed properties and methods in the external code. - -Here are the advantages over the functional pattern: - -- In the functional pattern, each object has its own copy of every method. We assign a separate copy of `this.sayHi = function() {...}` and other methods in the constructor. -- In the prototypal pattern, all methods are in `User.prototype` that is shared between all user objects. An object itself only stores the data. - -So the prototypal pattern is more memory-efficient. - -...But not only that. Prototypes allow us to setup the inheritance in a really efficient way. Built-in JavaScript objects all use prototypes. Also there's a special syntax construct: "class" that provides nice-looking syntax for them. And there's more, so let's go on with them. - -## Prototype-based inheritance for classes - -Let's say we have two prototype-based classes. - -`Rabbit`: - -```js -function Rabbit(name) { - this.name = name; -} - -Rabbit.prototype.jump = function() { - alert(`${this.name} jumps!`); -}; - -let rabbit = new Rabbit("My rabbit"); -``` - -![](rabbit-animal-independent-1.png) - -...And `Animal`: - -```js -function Animal(name) { - this.name = name; -} - -Animal.prototype.eat = function() { - alert(`${this.name} eats.`); -}; - -let animal = new Animal("My animal"); -``` - -![](rabbit-animal-independent-2.png) - -Right now they are fully independent. - -But we'd want `Rabbit` to extend `Animal`. In other words, rabbits should be based on animals, have access to methods of `Animal` and extend them with its own methods. - -What does it mean in the language of prototypes? - -Right now methods for `rabbit` objects are in `Rabbit.prototype`. We'd like `rabbit` to use `Animal.prototype` as a "fallback", if the method is not found in `Rabbit.prototype`. - -So the prototype chain should be `rabbit` -> `Rabbit.prototype` -> `Animal.prototype`. - -Like this: - -![](class-inheritance-rabbit-animal.png) - -The code to implement that: - -```js run -// Same Animal as before -function Animal(name) { - this.name = name; -} - -// All animals can eat, right? -Animal.prototype.eat = function() { - alert(`${this.name} eats.`); -}; - -// Same Rabbit as before -function Rabbit(name) { - this.name = name; -} - -Rabbit.prototype.jump = function() { - alert(`${this.name} jumps!`); -}; - -*!* -// setup the inheritance chain -Rabbit.prototype.__proto__ = Animal.prototype; // (*) -*/!* - -let rabbit = new Rabbit("White Rabbit"); -*!* -rabbit.eat(); // rabbits can eat too -*/!* -rabbit.jump(); -``` - -The line `(*)` sets up the prototype chain. So that `rabbit` first searches methods in `Rabbit.prototype`, then `Animal.prototype`. And then, just for completeness, let's mention that if the method is not found in `Animal.prototype`, then the search continues in `Object.prototype`, because `Animal.prototype` is a regular plain object, so it inherits from it. - -So here's the full picture: - -![](class-inheritance-rabbit-animal-2.png) - -## Summary - -The term "class" comes from the object-oriented programming. In JavaScript it usually means the functional class pattern or the prototypal pattern. The prototypal pattern is more powerful and memory-efficient, so it's recommended to stick to it. - -According to the prototypal pattern: -1. Methods are stored in `Class.prototype`. -2. Prototypes inherit from each other. - -In the next chapter we'll study `class` keyword and construct. It allows to write prototypal classes shorter and provides some additional benefits. diff --git a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal-2.png b/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal-2.png deleted file mode 100644 index 86b1a585..00000000 Binary files a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal-2.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal-2@2x.png b/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal-2@2x.png deleted file mode 100644 index f44bfb1d..00000000 Binary files a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal-2@2x.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal.png b/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal.png deleted file mode 100644 index 0da6479d..00000000 Binary files a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal@2x.png b/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal@2x.png deleted file mode 100644 index ebe8c032..00000000 Binary files a/1-js/09-classes/01-class-patterns/class-inheritance-rabbit-animal@2x.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-1.png b/1-js/09-classes/01-class-patterns/rabbit-animal-independent-1.png deleted file mode 100644 index f1d31218..00000000 Binary files a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-1.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-1@2x.png b/1-js/09-classes/01-class-patterns/rabbit-animal-independent-1@2x.png deleted file mode 100644 index 29c878ea..00000000 Binary files a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-1@2x.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-2.png b/1-js/09-classes/01-class-patterns/rabbit-animal-independent-2.png deleted file mode 100644 index bae44e57..00000000 Binary files a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-2.png and /dev/null differ diff --git a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-2@2x.png b/1-js/09-classes/01-class-patterns/rabbit-animal-independent-2@2x.png deleted file mode 100644 index 6197bd29..00000000 Binary files a/1-js/09-classes/01-class-patterns/rabbit-animal-independent-2@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/source.view/clock.js b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/solution.js similarity index 91% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/source.view/clock.js rename to 1-js/09-classes/01-class/1-rewrite-to-class/_js.view/solution.js index d701c0ca..0b31cf33 100644 --- a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/source.view/clock.js +++ b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/solution.js @@ -32,3 +32,7 @@ class Clock { this.timer = setInterval(() => this.render(), 1000); } } + + +let clock = new Clock({template: 'h:m:s'}); +clock.start(); diff --git a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/source.view/clock.js b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/source.js similarity index 90% rename from 1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/source.view/clock.js rename to 1-js/09-classes/01-class/1-rewrite-to-class/_js.view/source.js index c4bfaa0f..f1749c8b 100644 --- a/1-js/09-classes/01-class-patterns/2-rewrite-to-prototypes/source.view/clock.js +++ b/1-js/09-classes/01-class/1-rewrite-to-class/_js.view/source.js @@ -32,3 +32,6 @@ function Clock({ template }) { }; } + +let clock = new Clock({template: 'h:m:s'}); +clock.start(); diff --git a/1-js/09-classes/02-class/1-rewrite-to-class/solution.md b/1-js/09-classes/01-class/1-rewrite-to-class/solution.md similarity index 100% rename from 1-js/09-classes/02-class/1-rewrite-to-class/solution.md rename to 1-js/09-classes/01-class/1-rewrite-to-class/solution.md diff --git a/1-js/09-classes/02-class/1-rewrite-to-class/task.md b/1-js/09-classes/01-class/1-rewrite-to-class/task.md similarity index 53% rename from 1-js/09-classes/02-class/1-rewrite-to-class/task.md rename to 1-js/09-classes/01-class/1-rewrite-to-class/task.md index a29d347f..05365e41 100644 --- a/1-js/09-classes/02-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 -Rewrite the `Clock` class from prototypes to the modern "class" syntax. +The `Clock` class is written in functional style. Rewrite it the "class" syntax. P.S. The clock ticks in the console, open it to see. diff --git a/1-js/09-classes/01-class/article.md b/1-js/09-classes/01-class/article.md new file mode 100644 index 00000000..3cfcd4cb --- /dev/null +++ b/1-js/09-classes/01-class/article.md @@ -0,0 +1,351 @@ + +# Class basic syntax + +```quote author="Wikipedia" +In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods). +``` + +In practice, we often need to create many objects of the same kind, like users, or goods or whatever. + +As we already know from the chapter , `new function` can help with that. + +But in the modern JavaScript, there's a more advanced "class" construct, that introduces great new features which are useful for object-oriented programming. + +## The "class" syntax + +The basic syntax is: +```js +class MyClass { + // class methods + constructor() { ... } + method1() { ... } + method2() { ... } + method3() { ... } + ... +} +``` + +Then use `new MyClass()` to create a new object with all the listed methods. + +The `constructor()` method is called automatically by `new`, so we can initialize the object there. + +For example: + +```js run +class User { + + constructor(name) { + this.name = name; + } + + sayHi() { + alert(this.name); + } + +} + +// Usage: +let user = new User("John"); +user.sayHi(); +``` + +When `new User("John")` is called: +1. A new object is created. +2. The `constructor` runs with the given argument and assigns `this.name` to it. + +...Then we can call object methods, such as `user.sayHi()`. + + +```warn header="No comma between class methods" +A common pitfall for novice developers is to put a comma between class methods, which would result in a syntax error. + +The notation here is not to be confused with object literals. Within the class, no commas are required. +``` + +## What is a class? + +So, what exactly is a `class`? That's not an entirely new language-level entity, as one might think. + +Let's unveil any magic and see what a class really is. That'll help in understanding many complex aspects. + +In JavaScript, a class is a kind of a function. + +Here, take a look: + +```js run +class User { + constructor(name) { this.name = name; } + sayHi() { alert(this.name); } +} + +// proof: User is a function +*!* +alert(typeof User); // function +*/!* +``` + +What `class User {...}` construct really does is: + +1. Creates a function named `User`, that becomes the result of the class declaration. The function code is taken from the `constructor` method (assumed empty if we don't write such method). +2. Stores class methods, such as `sayHi`, in `User.prototype`. + +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. + +We can illustrate the result of `class User` declaration as: + +![](class-user.svg) + +Here's the code to introspect it: + +```js run +class User { + constructor(name) { this.name = name; } + sayHi() { alert(this.name); } +} + +// class is a function +alert(typeof User); // function + +// ...or, more precisely, the constructor method +alert(User === User.prototype.constructor); // true + +// The methods are in User.prototype, e.g: +alert(User.prototype.sayHi); // alert(this.name); + +// there are exactly two methods in the prototype +alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi +``` + +## Not just a syntax 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: + +```js run +// rewriting class User in pure functions + +// 1. Create constructor function +function User(name) { + this.name = name; +} +// any function prototype has constructor property by default, +// so we don't need to create it + +// 2. Add the method to prototype +User.prototype.sayHi = function() { + alert(this.name); +}; + +// Usage: +let user = new User("John"); +user.sayHi(); +``` + +The result of this definition is about the same. So, there are indeed reasons why `class` can be considered a syntax sugar to define a constructor together with its prototype methods. + +Although, 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. + + Unlike a regular function, a class constructor must be called with `new`: + + ```js run + class User { + constructor() {} + } + + alert(typeof User); // function + User(); // Error: Class constructor User cannot be invoked without 'new' + ``` + + Also, a string representation of a class constructor in most JavaScript engines starts with the "class..." + + ```js run + class User { + constructor() {} + } + + alert(User); // class User { ... } + ``` + +2. Class methods are non-enumerable. + A class definition sets `enumerable` flag to `false` for all methods in the `"prototype"`. + + That's good, because if we `for..in` over an object, we usually don't want its class methods. + +3. Classes always `use strict`. + All code inside the class construct is automatically in strict mode. + +Besides, `class` syntax brings many other features that we'll explore later. + +## Class Expression + +Just like functions, classes can be defined inside another expression, passed around, returned, assigned etc. + +Here's an example of a class expression: + +```js +let User = class { + sayHi() { + alert("Hello"); + } +}; +``` + +Similar to Named Function Expressions, class expressions may have a name. + +If a class expression has a name, it's visible inside the class only: + +```js run +// "Named Class Expression" +// (no such term in the spec, but that's similar to Named Function Expression) +let User = class *!*MyClass*/!* { + sayHi() { + alert(MyClass); // MyClass name is visible only inside the class + } +}; + +new User().sayHi(); // works, shows MyClass definition + +alert(MyClass); // error, MyClass name isn't visible outside of the class +``` + + +We can even make classes dynamically "on-demand", like this: + +```js run +function makeClass(phrase) { + // declare a class and return it + return class { + sayHi() { + alert(phrase); + }; + }; +} + +// Create a new class +let User = makeClass("Hello"); + +new User().sayHi(); // Hello +``` + + +## Getters/setters, other shorthands + +Just like literal objects, classes may include getters/setters, generators, computed properties etc. + +Here's an example for `user.name` implemented using `get/set`: + +```js run +class User { + + constructor(name) { + // invokes the setter + this.name = name; + } + +*!* + get name() { +*/!* + return this._name; + } + +*!* + set name(value) { +*/!* + if (value.length < 4) { + alert("Name is too short."); + return; + } + this._name = value; + } + +} + +let user = new User("John"); +alert(user.name); // John + +user = new User(""); // Name too short. +``` + +The class declaration creates getters and setters in `User.prototype`, like this: + +```js +Object.defineProperties(User.prototype, { + name: { + get() { + return this._name + }, + set(name) { + // ... + } + } +}); +``` + +Here's an example with a computed property in brackets `[...]`: + +```js run +class User { + +*!* + ['say' + 'Hi']() { +*/!* + alert("Hello"); + } + +} + +new User().sayHi(); +``` + +For a generator method, similarly, prepend it with `*`. + +## Class properties + +```warn header="Old browsers may need a polyfill" +Class-level properties are a recent addition to the language. +``` + +In the example above, `User` only had methods. Let's add a property: + +```js run +class User { +*!* + name = "Anonymous"; +*/!* + + sayHi() { + alert(`Hello, ${this.name}!`); + } +} + +new User().sayHi(); +``` + +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. + +## Summary + +The basic class syntax looks like this: + +```js +class MyClass { + prop = value; // property + + constructor(...) { // constructor + // ... + } + + method(...) {} // method + + get something(...) {} // getter method + set something(...) {} // setter method + + [Symbol.iterator]() {} // method with computed name (symbol here) + // ... +} +``` + +`MyClass` is technically a function (the one that we provide as `constructor`), while methods, getters and settors are written to `MyClass.prototype`. + +In the next chapters we'll learn more about classes, including inheritance and other features. diff --git a/1-js/09-classes/01-class/class-user.svg b/1-js/09-classes/01-class/class-user.svg new file mode 100644 index 00000000..5ac0146a --- /dev/null +++ b/1-js/09-classes/01-class/class-user.svg @@ -0,0 +1 @@ +sayHi: functionUserUser.prototypeprototypeconstructor: User \ No newline at end of file diff --git a/1-js/09-classes/03-class-inheritance/1-class-constructor-error/solution.md b/1-js/09-classes/02-class-inheritance/1-class-constructor-error/solution.md similarity index 100% rename from 1-js/09-classes/03-class-inheritance/1-class-constructor-error/solution.md rename to 1-js/09-classes/02-class-inheritance/1-class-constructor-error/solution.md diff --git a/1-js/09-classes/03-class-inheritance/1-class-constructor-error/task.md b/1-js/09-classes/02-class-inheritance/1-class-constructor-error/task.md similarity index 100% rename from 1-js/09-classes/03-class-inheritance/1-class-constructor-error/task.md rename to 1-js/09-classes/02-class-inheritance/1-class-constructor-error/task.md diff --git a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.md b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.md similarity index 100% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.md rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.md diff --git a/1-js/09-classes/02-class/1-rewrite-to-class/solution.view/clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/clock.js similarity index 100% rename from 1-js/09-classes/02-class/1-rewrite-to-class/solution.view/clock.js rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/clock.js diff --git a/1-js/09-classes/03-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 similarity index 100% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js diff --git a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.view/index.html b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/index.html similarity index 100% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.view/index.html rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/index.html diff --git a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.view/clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/clock.js similarity index 100% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/solution.view/clock.js rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/clock.js diff --git a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/source.view/index.html b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/index.html similarity index 100% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/source.view/index.html rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/source.view/index.html diff --git a/1-js/09-classes/03-class-inheritance/2-clock-class-extended/task.md b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/task.md similarity index 100% rename from 1-js/09-classes/03-class-inheritance/2-clock-class-extended/task.md rename to 1-js/09-classes/02-class-inheritance/2-clock-class-extended/task.md diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg b/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg new file mode 100644 index 00000000..0a1f4382 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/3-class-extend-object/rabbit-extends-object.svg @@ -0,0 +1 @@ +call: function bind: function ...Function.prototypeconstructorObjectRabbit[[Prototype]][[Prototype]]constructorcall: function bind: function ...Function.prototypeRabbit[[Prototype]]constructorclass Rabbitclass Rabbit extends Object \ No newline at end of file diff --git a/1-js/09-classes/03-class-inheritance/3-class-extend-object/solution.md b/1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md similarity index 98% rename from 1-js/09-classes/03-class-inheritance/3-class-extend-object/solution.md rename to 1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md index c1483aa3..fa26ec83 100644 --- a/1-js/09-classes/03-class-inheritance/3-class-extend-object/solution.md +++ b/1-js/09-classes/02-class-inheritance/3-class-extend-object/solution.md @@ -71,7 +71,7 @@ By the way, `Function.prototype` has "generic" function methods, like `call`, `b Here's the picture: -![](rabbit-extends-object.png) +![](rabbit-extends-object.svg) So, to put it short, there are two differences: diff --git a/1-js/09-classes/03-class-inheritance/3-class-extend-object/task.md b/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md similarity index 100% rename from 1-js/09-classes/03-class-inheritance/3-class-extend-object/task.md rename to 1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md diff --git a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg new file mode 100644 index 00000000..3412d982 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg @@ -0,0 +1 @@ +constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "White Rabbit"constructorconstructor \ No newline at end of file diff --git a/1-js/09-classes/03-class-inheritance/article.md b/1-js/09-classes/02-class-inheritance/article.md similarity index 58% rename from 1-js/09-classes/03-class-inheritance/article.md rename to 1-js/09-classes/02-class-inheritance/article.md index 1541a965..108cc11f 100644 --- a/1-js/09-classes/03-class-inheritance/article.md +++ b/1-js/09-classes/02-class-inheritance/article.md @@ -1,40 +1,82 @@ # Class inheritance -Classes can extend one another. There's a nice syntax, technically based on the prototypal inheritance. +Let's say we have two classes. -To inherit from another class, we should specify `"extends"` and the parent class before the brackets `{..}`. +`Animal`: + +```js +class Animal { + constructor(name) { + this.speed = 0; + this.name = name; + } + run(speed) { + this.speed += speed; + alert(`${this.name} runs with speed ${this.speed}.`); + } + stop() { + this.speed = 0; + alert(`${this.name} stands still.`); + } +} + +let animal = new Animal("My animal"); +``` + +![](rabbit-animal-independent-animal.svg) + + +...And `Rabbit`: + +```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} stopped.`); + alert(`${this.name} stands still.`); } - } +// Inherit from Animal by specifying "extends Animal" *!* -// Inherit from Animal class Rabbit extends Animal { +*/!* hide() { alert(`${this.name} hides!`); } } -*/!* let rabbit = new Rabbit("White Rabbit"); @@ -42,11 +84,15 @@ rabbit.run(5); // White Rabbit runs with speed 5. rabbit.hide(); // White Rabbit hides! ``` -The `extends` keyword actually adds a `[[Prototype]]` reference from `Rabbit.prototype` to `Animal.prototype`, just as you expect it to be, and as we've seen before. +Now the `Rabbit` code became a bit shorter, as it uses `Animal` constructor by default, and it also can `run`, as animals do. -![](animal-rabbit-extends.png) +Internally, `extends` keyword adds `[[Prototype]]` reference from `Rabbit.prototype` to `Animal.prototype`: -So now `rabbit` has access both to its own methods and to methods of `Animal`. +![](animal-rabbit-extends.svg) + +So, if a method is not found in `Rabbit.prototype`, JavaScript takes it from `Animal.prototype`. + +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. ````smart header="Any expression is allowed after `extends`" Class syntax allows to specify not just a class, but any expression after `extends`. @@ -85,7 +131,6 @@ class Rabbit extends 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. Classes provide `"super"` keyword for that. @@ -110,7 +155,7 @@ class Animal { stop() { this.speed = 0; - alert(`${this.name} stopped.`); + alert(`${this.name} stands still.`); } } @@ -131,7 +176,7 @@ class Rabbit extends Animal { let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. -rabbit.stop(); // White Rabbit stopped. 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. @@ -163,7 +208,7 @@ With constructors it gets a little bit tricky. Till now, `Rabbit` did not have its own `constructor`. -According to the [specification](https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation), if a class extends another class and has no `constructor`, then the following `constructor` is generated: +According to the [specification](https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation), if a class extends another class and has no `constructor`, then the following "empty" `constructor` is generated: ```js class Rabbit extends Animal { @@ -220,12 +265,12 @@ In JavaScript, there's a distinction between a "constructor function of an inher The difference is: -- When a normal constructor runs, it creates an empty object as `this` and continues with it. -- But when a derived constructor runs, it doesn't do it. It expects the parent constructor to do this job. +- When a normal constructor runs, 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 with `this` reference to it won't be created. And we'll get an error. +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. -For `Rabbit` to work, we need to call `super()` before using `this`, like here: +For `Rabbit` constructor to work, it needs to call `super()` before using `this`, like here: ```js run class Animal { @@ -261,17 +306,25 @@ alert(rabbit.earLength); // 10 ## Super: internals, [[HomeObject]] +```warn header="Advanced information" +If you're reading the tutorial for the first time - this section may be skipped. + +It's about the internal mechanisms behind inheritance and `super`. +``` + Let's get a little deeper under the hood of `super`. We'll see some interesting things by the way. -First to say, from all that we've learned till now, it's impossible for `super` to work. +First to say, from all that we've learned till now, it's impossible for `super` to work at all! -Yeah, indeed, let's ask ourselves, how it could technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, how to retrieve the `method`? Naturally, we need to take the `method` from the prototype of the current object. How, technically, we (or a JavaScript engine) can do it? +Yeah, indeed, let's ask ourselves, how it should technically work? When an object method runs, it gets the current object as `this`. If we call `super.method()` then, the engine needs to get the `method` from the prototype of the current object. But how? -Maybe we can get the method from `[[Prototype]]` of `this`, as `this.__proto__.method`? Unfortunately, that doesn't work. +The task may seem simple, but it isn't. The engine knows the current object `this`, so it could get the parent `method` as `this.__proto__.method`. Unfortunately, such a "naive" solution won't work. -Let's try to do it. Without classes, using plain objects for the sake of simplicity. +Let's demonstrate the problem. Without classes, using plain objects for the sake of simplicity. -Here, `rabbit.eat()` should call `animal.eat()` method of the parent object: +You may skip this part and go below to the `[[HomeObject]]` subsection if you don't want to know the details. That won't harm. Or read on if you're interested in understanding things in-depth. + +In the example below, `rabbit.__proto__ = animal`. Now let's try: in `rabbit.eat()` we'll call `animal.eat()`, using `this.__proto__`: ```js run let animal = { @@ -338,7 +391,7 @@ So, in both lines `(*)` and `(**)` the value of `this.__proto__` is exactly the Here's the picture of what happens: -![](this-super-loop.png) +![](this-super-loop.svg) 1. Inside `longEar.eat()`, the line `(**)` calls `rabbit.eat` providing it with `this=longEar`. ```js @@ -368,18 +421,16 @@ The problem can't be solved by using `this` alone. To provide the solution, JavaScript adds one more special internal property for functions: `[[HomeObject]]`. -**When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object.** +When a function is specified as a class or object method, its `[[HomeObject]]` property becomes that object. -This actually violates the idea of "unbound" functions, because methods remember their objects. And `[[HomeObject]]` can't be changed, so this bound is forever. So that's a very important change in the language. +Then `super` uses it to resolve the parent prototype and its methods. -But this change is safe. `[[HomeObject]]` is used only for calling parent methods in `super`, to resolve the prototype. So it doesn't break compatibility. - -Let's see how it works for `super` -- again, using plain objects: +Let's see how it works, first with plain objects: ```js run let animal = { name: "Animal", - eat() { // [[HomeObject]] == animal + eat() { // animal.eat.[[HomeObject]] == animal alert(`${this.name} eats.`); } }; @@ -387,7 +438,7 @@ let animal = { let rabbit = { __proto__: animal, name: "Rabbit", - eat() { // [[HomeObject]] == rabbit + eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } }; @@ -395,19 +446,79 @@ let rabbit = { let longEar = { __proto__: rabbit, name: "Long Ear", - eat() { // [[HomeObject]] == longEar + eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; *!* +// works correctly longEar.eat(); // Long Ear eats. */!* ``` -Every method remembers its object in the internal `[[HomeObject]]` property. Then `super` uses it to resolve the parent prototype. +It works as intended, due to `[[HomeObject]]` mechanics. A method, such as `longEar.eat`, knows its `[[HomeObject]]` and takes the parent method from its prototype. Without any use of `this`. -`[[HomeObject]]` is defined for methods defined both in classes and in plain objects. But for objects, methods must be specified exactly the given way: as `method()`, not as `"method: function()"`. +### Methods are not "free" + +As we've known before, generally functions are "free", not bound to objects in JavaScript. So they can be copied between objects and called with another `this`. + +The very existance of `[[HomeObject]]` violates that principle, because methods remember their objects. `[[HomeObject]]` can't be changed, so this bond is forever. + +The only place in the language where `[[HomeObject]]` is used -- is `super`. So, if a method does not use `super`, then we can still consider it free and copy between objects. But with `super` things may go wrong. + +Here's the demo of a wrong `super` result after copying: + +```js run +let animal = { + sayHi() { + console.log(`I'm an animal`); + } +}; + +// rabbit inherits from animal +let rabbit = { + __proto__: animal, + sayHi() { + super.sayHi(); + } +}; + +let plant = { + sayHi() { + console.log("I'm a plant"); + } +}; + +// tree inherits from plant +let tree = { + __proto__: plant, +*!* + sayHi: rabbit.sayHi // (*) +*/!* +}; + +*!* +tree.sayHi(); // I'm an animal (?!?) +*/!* +``` + +A call to `tree.sayHi()` shows "I'm an animal". Definitevely wrong. + +The reason is simple: +- In the line `(*)`, the method `tree.sayHi` was copied from `rabbit`. Maybe we just wanted to avoid code duplication? +- Its `[[HomeObject]]` is `rabbit`, as it was created in `rabbit`. There's no way to change `[[HomeObject]]`. +- The code of `tree.sayHi()` has `super.sayHi()` inside. It goes up from `rabbit` and takes the method from `animal`. + +Here's the diagram of what happens: + +![](super-homeobject-wrong.svg) + +### Methods, not function properties + +`[[HomeObject]]` is defined for methods both in classes and in plain objects. But for objects, methods must be specified exactly as `method()`, not as `"method: function()"`. + +The difference may be non-essential for us, but it's important for JavaScript. In the example below a non-method syntax is used for comparison. `[[HomeObject]]` property is not set and the inheritance doesn't work: @@ -429,3 +540,18 @@ let rabbit = { rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) */!* ``` + +## Summary + +1. To extend a class: `class Child extends Parent`: + - That means `Child.prototype.__proto__` will be `Parent.prototype`, so methods are inherited. +2. When overriding a constructor: + - We must call parent constructor as `super()` in `Child` constructor before using `this`. +3. When overriding another method: + - We can use `super.method()` in a `Child` method to call `Parent` method. +4. Internals: + - Methods remember their class/object in the internal `[[HomeObject]]` property. That's how `super` resolves parent methods. + - So it's not safe to copy a method with `super` from one object to another. + +Also: +- Arrow functions don't have own `this` or `super`, so they transparently fit into the surrounding context. diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg new file mode 100644 index 00000000..546aa334 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-array-object.svg @@ -0,0 +1 @@ +slice: function ...Array.prototypearrhasOwnProperty: function ...Object.prototype[1, 2, 3][[Prototype]][[Prototype]] \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg new file mode 100644 index 00000000..3bdda5a0 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal-2.svg @@ -0,0 +1 @@ +jump: functionRabbit.prototyperabbiteat: functionAnimal.prototypename: "White Rabbit"[[Prototype]][[Prototype]]Rabbit.prototype.__proto__ = Animal.prototype sets thistoString: function hasOwnProperty: function ...Object.prototype[[Prototype]][[Prototype]]null \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg new file mode 100644 index 00000000..91f82896 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/class-inheritance-rabbit-animal.svg @@ -0,0 +1 @@ +methods of RabbitRabbit.prototyperabbitmethods of AnimalAnimal.prototype[[Prototype]][[Prototype]]properties of rabbit \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg new file mode 100644 index 00000000..bf86db77 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-animal.svg @@ -0,0 +1 @@ + constructor: Animal run: function stop: functionAnimal.prototypeAnimalnew Animal[[Prototype]]prototypename: "My animal" \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg new file mode 100644 index 00000000..8a5e2503 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/rabbit-animal-independent-rabbit.svg @@ -0,0 +1 @@ + constructor: Rabbit hide: functionRabbit.prototypeRabbitnew Rabbit[[Prototype]]prototypename: "My rabbit" \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg new file mode 100644 index 00000000..c9c8fea9 --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg @@ -0,0 +1 @@ +sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/this-super-loop.svg b/1-js/09-classes/02-class-inheritance/this-super-loop.svg new file mode 100644 index 00000000..342574da --- /dev/null +++ b/1-js/09-classes/02-class-inheritance/this-super-loop.svg @@ -0,0 +1 @@ +rabbitlongEarrabbitlongEar \ No newline at end of file diff --git a/1-js/09-classes/02-class/1-rewrite-to-class/solution.view/index.html b/1-js/09-classes/02-class/1-rewrite-to-class/solution.view/index.html deleted file mode 100644 index fdee13d0..00000000 --- a/1-js/09-classes/02-class/1-rewrite-to-class/solution.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/09-classes/02-class/1-rewrite-to-class/source.view/clock.js b/1-js/09-classes/02-class/1-rewrite-to-class/source.view/clock.js deleted file mode 100644 index 537f7268..00000000 --- a/1-js/09-classes/02-class/1-rewrite-to-class/source.view/clock.js +++ /dev/null @@ -1,34 +0,0 @@ - - -function Clock({ template }) { - this.template = template; -} - -Clock.prototype.render = function() { - let date = new Date(); - - let hours = date.getHours(); - if (hours < 10) hours = '0' + hours; - - let mins = date.getMinutes(); - if (mins < 10) mins = '0' + mins; - - let secs = date.getSeconds(); - if (secs < 10) secs = '0' + secs; - - let output = this.template - .replace('h', hours) - .replace('m', mins) - .replace('s', secs); - - console.log(output); -}; - -Clock.prototype.stop = function() { - clearInterval(this.timer); -}; - -Clock.prototype.start = function() { - this.render(); - this.timer = setInterval(() => this.render(), 1000); -}; diff --git a/1-js/09-classes/02-class/1-rewrite-to-class/source.view/index.html b/1-js/09-classes/02-class/1-rewrite-to-class/source.view/index.html deleted file mode 100644 index fdee13d0..00000000 --- a/1-js/09-classes/02-class/1-rewrite-to-class/source.view/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Console clock - - - - - - - - - - diff --git a/1-js/09-classes/02-class/article.md b/1-js/09-classes/02-class/article.md deleted file mode 100644 index dd9284dd..00000000 --- a/1-js/09-classes/02-class/article.md +++ /dev/null @@ -1,272 +0,0 @@ - -# Classes - -The "class" construct allows one to define prototype-based classes with a clean, nice-looking syntax. It also introduces great new features which are useful for object-oriented programming. - -## The "class" syntax - -The `class` syntax is versatile, we'll start with a simple example first. - -Here's a prototype-based class `User`: - -```js run -function User(name) { - this.name = name; -} - -User.prototype.sayHi = function() { - alert(this.name); -} - -let user = new User("John"); -user.sayHi(); -``` - -...And here's the same using `class` syntax: - -```js run -class User { - - constructor(name) { - this.name = name; - } - - sayHi() { - alert(this.name); - } - -} - -let user = new User("John"); -user.sayHi(); -``` - -It's easy to see that these two examples are alike. Be sure to note that methods in a class do not have a comma between them. A common pitfall for novice developers is to put a comma between class methods, which would result in a syntax error. The notation here is not to be confused with object literals. Within the class syntactical sugar, no commas are required. - -## What is a class? - -So, what exactly is a `class`? We may think that it defines a new language-level entity, but that would be wrong. - -In Javascript, a class is a kind of a function. - -The definition `class User {...}` creates a function under the same name and puts the methods into `User.prototype`. So the structure is similar. - -This is demonstrated in the following code, which you can run yourself: - -```js run -class User { - constructor(name) { this.name = name; } - sayHi() { alert(this.name); } -} - -*!* -// proof: User is a function -alert(typeof User); // function -*/!* - -*!* -// proof: User is the "constructor" function -*/!* -alert(User === User.prototype.constructor); // true - -*!* -// proof: there are two methods in its "prototype" -*/!* -alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi -``` - -Abstractly, we can illustrate this process of `class User` creating a function as: - -![](class-user.png) - -`Class` is a special syntax to define a constructor together with its prototype methods. In addition to its basic operation, the `Class` syntax brings many other features with it which we'll explore later. - -## Class Expression - -Just like functions, classes can be defined inside another expression, passed around, returned etc. - -Here's a class-returning function - otherwise known as a "class factory": - -```js run -function makeClass(phrase) { -*!* - // declare a class and return it - return class { - sayHi() { - alert(phrase); - }; - }; -*/!* -} - -let User = makeClass("Hello"); - -new User().sayHi(); // Hello -``` - -That's quite normal if we recall that `class` is just a special form of a function-with-prototype definition. - -And, like Named Function Expressions, such classes also may have a name, that is visible inside that class only: - -```js run -// "Named Class Expression" (alas, no such term, but that's what's going on) -let User = class *!*MyClass*/!* { - sayHi() { - alert(MyClass); // MyClass is visible only inside the class - } -}; - -new User().sayHi(); // works, shows MyClass definition - -alert(MyClass); // error, MyClass not visible outside of the class -``` - -## Differences between classes and functions - -Classes have some differences compared to regular functions: - -Constructors require `new` -: Unlike a regular function, a class `constructor` can't be called without `new`: - -```js run -class User { - constructor() {} -} - -alert(typeof User); // function -User(); // Error: Class constructor User cannot be invoked without 'new' -``` - -Different string output -: If we output it like `alert(User)`, some engines show `"class User..."`, while others show `"function User..."`. - -Please don't be confused: the string representation may vary, but that's still a function, there is no separate "class" entity in JavaScript language. - -Class methods are non-enumerable -: A class definition sets `enumerable` flag to `false` for all methods in the `"prototype"`. That's good, because if we `for..in` over an object, we usually don't want its class methods. - -Classes have a default `constructor() {}` -: If there's no `constructor` in the `class` construct, then an empty function is generated, just as if we had written `constructor() {}`. - -Classes always `use strict` -: All code inside the class construct is automatically in strict mode. - - -## Getters/setters, other shorthands - -Classes also include getters/setters, generators, computed properties etc. - -Here's an example for `user.name` implemented using `get/set`: - -```js run -class User { - - constructor(name) { - // invokes the setter - this.name = name; - } - -*!* - get name() { -*/!* - return this._name; - } - -*!* - set name(value) { -*/!* - if (value.length < 4) { - alert("Name is too short."); - return; - } - this._name = value; - } - -} - -let user = new User("John"); -alert(user.name); // John - -user = new User(""); // Name too short. -``` - -Internally, getters and setters are created on `User.prototype`, like this: - -```js -Object.defineProperties(User.prototype, { - name: { - get() { - return this._name - }, - set(name) { - // ... - } - } -}); -``` - -Here's an example with computed properties: - -```js run -function f() { return "sayHi"; } - -class User { - [f()]() { - alert("Hello"); - } - -} - -new User().sayHi(); -``` - -For a generator method, similarly, prepend it with `*`. - -## Class properties - -```warn header="Old browsers may need a polyfill" -Class-level properties are a recent addition to the language. -``` - -In the example above, `User` only had methods. Let's add a property: - -```js run -class User { - name = "Anonymous"; - - sayHi() { - alert(`Hello, ${this.name}!`); - } -} - -new User().sayHi(); -``` - -The property is not placed into `User.prototype`. Instead, it is created by `new`, separately for every object. So, the property will never be shared between different objects of the same class. - - -## Summary - -The basic class syntax looks like this: - -```js -class MyClass { - prop = value; - - constructor(...) { - // ... - } - - method(...) {} - - get something(...) {} - set something(...) {} - - [Symbol.iterator]() {} - // ... -} -``` - -`MyClass` is technically a function, while methods 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/02-class/class-user.png b/1-js/09-classes/02-class/class-user.png deleted file mode 100644 index f090909a..00000000 Binary files a/1-js/09-classes/02-class/class-user.png and /dev/null differ diff --git a/1-js/09-classes/02-class/class-user@2x.png b/1-js/09-classes/02-class/class-user@2x.png deleted file mode 100644 index b953f91e..00000000 Binary files a/1-js/09-classes/02-class/class-user@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/3-class-extend-object/rabbit-extends-object.png b/1-js/09-classes/03-class-inheritance/3-class-extend-object/rabbit-extends-object.png deleted file mode 100644 index b0b1b6ad..00000000 Binary files a/1-js/09-classes/03-class-inheritance/3-class-extend-object/rabbit-extends-object.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png b/1-js/09-classes/03-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png deleted file mode 100644 index 76cd5877..00000000 Binary files a/1-js/09-classes/03-class-inheritance/3-class-extend-object/rabbit-extends-object@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/animal-rabbit-extends.png b/1-js/09-classes/03-class-inheritance/animal-rabbit-extends.png deleted file mode 100644 index ffdb6df7..00000000 Binary files a/1-js/09-classes/03-class-inheritance/animal-rabbit-extends.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/animal-rabbit-extends@2x.png b/1-js/09-classes/03-class-inheritance/animal-rabbit-extends@2x.png deleted file mode 100644 index e532c182..00000000 Binary files a/1-js/09-classes/03-class-inheritance/animal-rabbit-extends@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/class-inheritance-array-object.png b/1-js/09-classes/03-class-inheritance/class-inheritance-array-object.png deleted file mode 100644 index 4bb5bb95..00000000 Binary files a/1-js/09-classes/03-class-inheritance/class-inheritance-array-object.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/class-inheritance-array-object@2x.png b/1-js/09-classes/03-class-inheritance/class-inheritance-array-object@2x.png deleted file mode 100644 index 4741353f..00000000 Binary files a/1-js/09-classes/03-class-inheritance/class-inheritance-array-object@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-animal.png b/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-animal.png deleted file mode 100644 index 0da6479d..00000000 Binary files a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-animal.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-animal@2x.png b/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-animal@2x.png deleted file mode 100644 index ebe8c032..00000000 Binary files a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-animal@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-run-animal.png b/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-run-animal.png deleted file mode 100644 index 387975a9..00000000 Binary files a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-run-animal.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-run-animal@2x.png b/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-run-animal@2x.png deleted file mode 100644 index ca731359..00000000 Binary files a/1-js/09-classes/03-class-inheritance/class-inheritance-rabbit-run-animal@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/this-super-loop.png b/1-js/09-classes/03-class-inheritance/this-super-loop.png deleted file mode 100644 index 74d1d88e..00000000 Binary files a/1-js/09-classes/03-class-inheritance/this-super-loop.png and /dev/null differ diff --git a/1-js/09-classes/03-class-inheritance/this-super-loop@2x.png b/1-js/09-classes/03-class-inheritance/this-super-loop@2x.png deleted file mode 100644 index 8ce876f1..00000000 Binary files a/1-js/09-classes/03-class-inheritance/this-super-loop@2x.png and /dev/null differ diff --git a/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg new file mode 100644 index 00000000..fab401df --- /dev/null +++ b/1-js/09-classes/03-static-properties-methods/animal-rabbit-static.svg @@ -0,0 +1 @@ +constructor: Animal run: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitrabbit[[Prototype]][[Prototype]][[Prototype]]prototypeprototypecomparename: "White Rabbit" \ No newline at end of file diff --git a/1-js/09-classes/04-static-properties-methods/article.md b/1-js/09-classes/03-static-properties-methods/article.md similarity index 59% rename from 1-js/09-classes/04-static-properties-methods/article.md rename to 1-js/09-classes/03-static-properties-methods/article.md index 760641ea..583fabcf 100644 --- a/1-js/09-classes/04-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 methods to the class function, not to its `"prototype"`. Such methods are called *static*. +We can also assign a method to the class function itself, not to its `"prototype"`. Such methods are called *static*. -An example: +In a class, they are prepended by `static` keyword, like this: ```js run class User { @@ -17,21 +17,21 @@ class User { User.staticMethod(); // true ``` -That actually does the same as assigning it as a function property: +That actually does the same as assigning it as a property directly: ```js -function User() { } +class User() { } User.staticMethod = function() { alert(this === User); }; ``` -The value of `this` inside `User.staticMethod()` is the class constructor `User` itself (the "object before dot" rule). +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. -For instance, we have `Article` objects and need a function to compare them. The natural choice would be `Article.compare`, like this: +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: ```js run class Article { @@ -49,8 +49,8 @@ class Article { // usage let articles = [ - new Article("Mind", new Date(2019, 1, 1)), - new Article("Body", new Date(2019, 0, 1)), + new Article("HTML", new Date(2019, 1, 1)), + new Article("CSS", new Date(2019, 0, 1)), new Article("JavaScript", new Date(2019, 11, 1)) ]; @@ -58,16 +58,16 @@ let articles = [ articles.sort(Article.compare); */!* -alert( articles[0].title ); // Body +alert( articles[0].title ); // CSS ``` -Here `Article.compare` stands "over" the articles, as a means to compare them. It's not a method of an article, but rather of the whole class. +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. Another example would be a so-called "factory" method. Imagine, we need few ways to create an article: 1. Create by given parameters (`title`, `date` etc). 2. Create an empty article with today's date. -3. ... +3. ...or else somehow. The first way can be implemented by the constructor. And for the second one we can make a static method of the class. @@ -90,7 +90,7 @@ class Article { let article = Article.createTodays(); -alert( article.title ); // Todays digest +alert( article.title ); // Today's digest ``` Now every time we need to create a today's digest, we can call `Article.createTodays()`. Once again, that's not a method of an article, but a method of the whole class. @@ -107,7 +107,7 @@ Article.remove({id: 12345}); [recent browser=Chrome] -Static properties are also possible, just like regular class properties: +Static properties are also possible, they look like regular class properties, but prepended by `static`: ```js run class Article { @@ -123,11 +123,11 @@ That is the same as a direct assignment to `Article`: Article.publisher = "Ilya Kantor"; ``` -## Statics and inheritance +## Inheritance of static methods -Statics are inhereted, we can access `Parent.method` as `Child.method`. +Static methods are inherited. -For instance, `Animal.compare` in the code below is inhereted and accessible as `Rabbit.compare`: +For instance, `Animal.compare` in the code below is inherited and accessible as `Rabbit.compare`: ```js run class Animal { @@ -169,36 +169,39 @@ rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5. ``` -Now we can call `Rabbit.compare` assuming that the inherited `Animal.compare` will be called. +Now when we can call `Rabbit.compare`, the inherited `Animal.compare` will be called. -How does it work? Again, using prototypes. As you might have already guessed, extends also gives `Rabbit` the `[[Prototype]]` reference to `Animal`. +How does it work? Again, using prototypes. As you might have already guessed, `extends` gives `Rabbit` the `[[Prototype]]` reference to `Animal`. +![](animal-rabbit-static.svg) -![](animal-rabbit-static.png) +So, `Rabbit extends Animal` creates two `[[Prototype]]` references: -So, `Rabbit` function now inherits from `Animal` function. And `Animal` function normally has `[[Prototype]]` referencing `Function.prototype`, because it doesn't `extend` anything. +1. `Rabbit` function prototypally inherits from `Animal` function. +2. `Rabbit.prototype` prototypally inherits from `Animal.prototype`. -Here, let's check that: +As the result, inheritance works both for regular and static methods. + +Here, let's check that by code: ```js run class Animal {} class Rabbit extends Animal {} -// for static properties and methods +// for statics alert(Rabbit.__proto__ === Animal); // true -// and the next step is Function.prototype -alert(Animal.__proto__ === Function.prototype); // true - -// that's in addition to the "normal" prototype chain for object methods +// for regular methods alert(Rabbit.prototype.__proto__ === Animal.prototype); ``` -This way `Rabbit` has access to all static methods of `Animal`. - ## Summary -Static methods are used for the functionality that doesn't relate to a concrete class instance, doesn't require an instance to exist, but rather belongs to the class as a whole, like `Article.compare` -- a generic method to compare two articles. +Static methods are used for the functionality that belongs to the class "as a whole", doesn't relate to a concrete class instance. + +For example, a method for comparison `Article.compare(article1, article2)` or a factory method `Article.createTodays()`. + +They are labeled by the word `static` in class declaration. Static properties are used when we'd like to store class-level data, also not bound to an instance. @@ -214,13 +217,13 @@ class MyClass { } ``` -That's technically the same as assigning to the class itself: +Technically, static declaration is the same as assigning to the class itself: ```js MyClass.property = ... MyClass.method = ... ``` -Static properties are inherited. +Static properties and methods are inherited. -Technically, for `class B extends A` the prototype of the class `B` itself points to `A`: `B.[[Prototype]] = A`. So if a field is not found in `B`, the search continues in `A`. +For `class B extends A` the prototype of the class `B` itself points to `A`: `B.[[Prototype]] = A`. So if a field is not found in `B`, the search continues in `A`. diff --git a/1-js/09-classes/05-private-protected-properties-methods/article.md b/1-js/09-classes/04-private-protected-properties-methods/article.md similarity index 80% rename from 1-js/09-classes/05-private-protected-properties-methods/article.md rename to 1-js/09-classes/04-private-protected-properties-methods/article.md index 82466331..ef0d497a 100644 --- a/1-js/09-classes/05-private-protected-properties-methods/article.md +++ b/1-js/09-classes/04-private-protected-properties-methods/article.md @@ -48,16 +48,16 @@ So, all we need to use an object is to know its external interface. We may be co That was a general introduction. -In JavaScript, there are three types of properties and members: +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. - 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. 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 properly do the extension. +In many other languages there also exist "protected" fields: accessible only from inside the class and those extending it (like private, but plus access from inheriting classes). They are also useful for the internal interface. They are in a sense more widespread than private ones, because we usually want inheriting classes to gain access to them. -Protected fields are not implemented in Javascript on the language level, but in practice they are very convenient, so they are emulated. +Protected fields are not implemented in JavaScript on the language level, but in practice they are very convenient, so they are emulated. -In the next step we'll make a coffee machine in Javascript with all these types of properties. A coffee machine has a lot of details, we won't model them to stay simple (though we could). +Now we'll make a coffee machine in JavaScript with all these types of properties. A coffee machine has a lot of details, we won't model them to stay simple (though we could). ## Protecting "waterAmount" @@ -87,7 +87,7 @@ Let's change `waterAmount` property to protected to have more control over it. F **Protected properties are usually prefixed with an underscore `_`.** -That is not enforced on the language level, but there's a convention that such properties and methods should not be accessed from the outside. Most programmers follow it. +That is not enforced on the language level, but there's a well-known convention between programmers that such properties and methods should not be accessed from the outside. So our property will be called `_waterAmount`: @@ -164,16 +164,16 @@ class CoffeeMachine { } *!*getWaterAmount()*/!* { - return this.waterAmount; + return this._waterAmount; } } new CoffeeMachine().setWaterAmount(100); ``` -That looks a bit longer, but functions are more flexible. They can accept multiple arguments (even if we don't need them right now). So, for the future, just in case we need to refactor something, functions are a safer choise. +That looks a bit longer, but functions are more flexible. They can accept multiple arguments (even if we don't need them right now). -Surely, there's a tradeoff. On the other hand, get/set syntax is shorter, so ultimately there's no strict rule, it's up to you to decide. +On the other hand, get/set syntax is shorter, so ultimately there's no strict rule, it's up to you to decide. ```` ```smart header="Protected fields are inherited" @@ -186,51 +186,37 @@ So protected fields are naturally inheritable. Unlike private ones that we'll se [recent browser=none] -There's a finished Javascript proposal, almost in the standard, that provides language-level support for private properties and methods. +There's a finished JavaScript proposal, almost in the standard, that provides language-level support for private properties and methods. Privates should start with `#`. They are only accessible from inside the class. -For instance, here we add a private `#waterLimit` property and extract the water-checking logic into a separate method: +For instance, here's a private `#waterLimit` property and the water-checking private method `#checkWater`: -```js +```js run class CoffeeMachine { *!* #waterLimit = 200; */!* *!* - #checkWater(water) { + #checkWater(value) { if (value < 0) throw new Error("Negative water"); if (value > this.#waterLimit) throw new Error("Too much water"); } */!* - _waterAmount = 0; - - set waterAmount(value) { -*!* - this.#checkWater(value); -*/!* - this._waterAmount = value; - } - - get waterAmount() { - return this.waterAmount; - } - } let coffeeMachine = new CoffeeMachine(); *!* +// can't access privates from outside of the class coffeeMachine.#checkWater(); // Error coffeeMachine.#waterLimit = 1000; // Error */!* - -coffeeMachine.waterAmount = 100; // Works ``` -On the language level, `#` is a special sign that the field is private. We can't access it from outside or from inhereting classes. +On the language level, `#` is a special sign that the field is private. We can't access it from outside or from inheriting classes. Private fields do not conflict with public ones. We can have both private `#waterAmount` and public `waterAmount` fields at the same time. @@ -257,12 +243,12 @@ machine.waterAmount = 100; alert(machine.#waterAmount); // Error ``` -Unlike protected ones, private fields are enforced by the language itselfs. That's a good thing. +Unlike protected ones, private fields are enforced by the language itself. That's a good thing. But if we inherit from `CoffeeMachine`, then we'll have no direct access to `#waterAmount`. We'll need to rely on `waterAmount` getter/setter: ```js -class CoffeeMachine extends CoffeeMachine() { +class MegaCoffeeMachine extends CoffeeMachine() { method() { *!* alert( this.#waterAmount ); // Error: can only access from CoffeeMachine @@ -271,19 +257,19 @@ class CoffeeMachine 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 most of the time, 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 reason to access its internals. That's why protected fields are used more often, even though they are not supported by the language syntax. -````warn +````warn header="Private fields are not available as this[name]" Private fields are special. -Remember, usually we can access fields by this[name]: +As we know, usually we can access fields using `this[name]`: ```js class User { ... sayHi() { let fieldName = "name"; - alert(`Hello, ${this[fieldName]}`); + alert(`Hello, ${*!*this[fieldName]*/!*}`); } } ``` @@ -309,11 +295,11 @@ Protection for users, so that they don't shoot themselves in the feet Supportable : The situation in programming is more complex than with a real-life coffee machine, because we don't just buy it once. The code constantly undergoes development and improvement. - **If we strictly delimit the internal interface, then the developer of the class can freely change its internal properties and methods, even without informing the users..** + **If we strictly delimit the internal interface, then the developer of the class can freely change its internal properties and methods, even without informing the users.** - It's much easier to develop, if you know that certain methods can be renamed, their parameters can be changed, and even removed, because no external code depends on them. + If you're a developer of such class, it's great to know that private methods can be safely renamed, their parameters can be changed, and even removed, because no external code depends on them. - For users, when a new version comes out, it may be a total overhaul, but still simple to upgrade if the external interface is the same. + 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. @@ -322,9 +308,9 @@ Hiding complexity **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 public properties: +To hide 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 only can 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-private-protected-properties-methods/coffee-inside.jpg b/1-js/09-classes/04-private-protected-properties-methods/coffee-inside.jpg similarity index 100% rename from 1-js/09-classes/05-private-protected-properties-methods/coffee-inside.jpg rename to 1-js/09-classes/04-private-protected-properties-methods/coffee-inside.jpg diff --git a/1-js/09-classes/05-private-protected-properties-methods/coffee.jpg b/1-js/09-classes/04-private-protected-properties-methods/coffee.jpg similarity index 100% rename from 1-js/09-classes/05-private-protected-properties-methods/coffee.jpg rename to 1-js/09-classes/04-private-protected-properties-methods/coffee.jpg diff --git a/1-js/09-classes/04-static-properties-methods/animal-rabbit-static.png b/1-js/09-classes/04-static-properties-methods/animal-rabbit-static.png deleted file mode 100644 index f6331e95..00000000 Binary files a/1-js/09-classes/04-static-properties-methods/animal-rabbit-static.png and /dev/null differ diff --git a/1-js/09-classes/04-static-properties-methods/animal-rabbit-static@2x.png b/1-js/09-classes/04-static-properties-methods/animal-rabbit-static@2x.png deleted file mode 100644 index d515cb0f..00000000 Binary files a/1-js/09-classes/04-static-properties-methods/animal-rabbit-static@2x.png and /dev/null differ diff --git a/1-js/09-classes/05-extend-natives/article.md b/1-js/09-classes/05-extend-natives/article.md new file mode 100644 index 00000000..2dc4902b --- /dev/null +++ b/1-js/09-classes/05-extend-natives/article.md @@ -0,0 +1,89 @@ + +# Extending built-in classes + +Built-in classes like Array, Map and others are extendable also. + +For instance, here `PowerArray` inherits from the native `Array`: + +```js run +// add one more method to it (can do more) +class PowerArray extends Array { + isEmpty() { + return this.length === 0; + } +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +let filteredArr = arr.filter(item => item >= 10); +alert(filteredArr); // 10, 50 +alert(filteredArr.isEmpty()); // false +``` + +Please note a very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type `PowerArray`. Their internal implementation uses object `constructor` property for that. + +In the example above, +```js +arr.constructor === PowerArray +``` + +When `arr.filter()` is called, it internally creates the new array of results using exactly `arr.constructor`, not basic `Array`. That's actually very cool, because we can keep using `PowerArray` methods further on the result. + +Even more, we can customize that behavior. + +We can add a special static getter `Symbol.species` to the class. If exists, it should return the constructor that JavaScript will use internally to create new entities in `map`, `filter` and so on. + +If we'd like built-in methods like `map` or `filter` to return regular arrays, we can return `Array` in `Symbol.species`, like here: + +```js run +class PowerArray extends Array { + isEmpty() { + return this.length === 0; + } + +*!* + // built-in methods will use this as the constructor + static get [Symbol.species]() { + return Array; + } +*/!* +} + +let arr = new PowerArray(1, 2, 5, 10, 50); +alert(arr.isEmpty()); // false + +// filter creates new array using arr.constructor[Symbol.species] as constructor +let filteredArr = arr.filter(item => item >= 10); + +*!* +// filteredArr is not PowerArray, but Array +*/!* +alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function +``` + +As you can see, now `.filter` returns `Array`. So the extended functionality is not passed any further. + +```smart header="Other collections work similarly" +Other collections, such as `Map` and `Set`, work alike. They also use `Symbol.species`. +``` + +## No static inheritance in built-ins + +Built-in objects have their own static methods, for instance `Object.keys`, `Array.isArray` etc. + +As we already know, native classes extend each other. For instance, `Array` extends `Object`. + +Normally, when one class extends another, both static and non-static methods are inherited. That was thoroughly explained in the chapter [](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. + +Here's the picture structure for `Date` and `Object`: + +![](object-date-inheritance.svg) + +As you can see, there's no link between `Date` and `Object`. They are independent, only `Date.prototype` inherits from `Object.prototype`. + +That's an important difference of inheritance between built-in objects compared to what we get with `extends`. diff --git a/1-js/09-classes/05-extend-natives/object-date-inheritance.svg b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg new file mode 100644 index 00000000..a0165bcc --- /dev/null +++ b/1-js/09-classes/05-extend-natives/object-date-inheritance.svg @@ -0,0 +1 @@ +constructor: Object toString: function hasOwnProperty: function ...Object.prototypeconstructor: Date toString: function getDate: function ...Date.prototypeObjectDatenew Date()[[Prototype]][[Prototype]]prototypeprototypedefineProperty keys ...now parse ...1 Jan 2019 \ No newline at end of file diff --git a/1-js/09-classes/06-extend-natives/article.md b/1-js/09-classes/06-extend-natives/article.md deleted file mode 100644 index 24757abe..00000000 --- a/1-js/09-classes/06-extend-natives/article.md +++ /dev/null @@ -1,82 +0,0 @@ - -# Extending build-in classes - -Built-in classes like Array, Map and others are extendable also. - -For instance, here `PowerArray` inherits from the native `Array`: - -```js run -// add one more method to it (can do more) -class PowerArray extends Array { - isEmpty() { - return this.length === 0; - } -} - -let arr = new PowerArray(1, 2, 5, 10, 50); -alert(arr.isEmpty()); // false - -let filteredArr = arr.filter(item => item >= 10); -alert(filteredArr); // 10, 50 -alert(filteredArr.isEmpty()); // false -``` - -Please note a very interesting thing. Built-in methods like `filter`, `map` and others -- return new objects of exactly the inherited type. They rely on the `constructor` property to do so. - -In the example above, -```js -arr.constructor === PowerArray -``` - -So when `arr.filter()` is called, it internally creates the new array of results exactly as `new PowerArray`. -That's actually very cool, because we can keep using `PowerArray` methods further o the result. - -Even more, we can customize that behavior. - -There's a special static getter `Symbol.species`, if exists, it returns the constructor to use in such cases. - -If we'd like built-in methods like `map`, `filter` will return regular arrays, we can return `Array` in `Symbol.species`, like here: - -```js run -class PowerArray extends Array { - isEmpty() { - return this.length === 0; - } - -*!* - // built-in methods will use this as the constructor - static get [Symbol.species]() { - return Array; - } -*/!* -} - -let arr = new PowerArray(1, 2, 5, 10, 50); -alert(arr.isEmpty()); // false - -// filter creates new array using arr.constructor[Symbol.species] as constructor -let filteredArr = arr.filter(item => item >= 10); - -*!* -// filteredArr is not PowerArray, but Array -*/!* -alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function -``` - -As you can see, now `.filter` returns `Array`. So the extended functionality is not passed any further. - -## No static inheritance in built-ins - -Built-in objects have their own static methods, for instance `Object.keys`, `Array.isArray` etc. - -And we've already been talking about native classes extending each other: `Array.[[Prototype]] = Object`. - -But statics are an exception. Built-in classes don't inherit static properties from each other. - -In other words, the prototype of build-in constructor `Array` does not point to `Object`. This way `Array` and `Date` do not have `Array.keys` or `Date.keys`. And that feels natural. - -Here's the picture structure for `Date` and `Object`: - -![](object-date-inheritance.png) - -Note, there's no link between `Date` and `Object`. Both `Object` and `Date` exist independently. `Date.prototype` inherits from `Object.prototype`, but that's all. diff --git a/1-js/09-classes/06-extend-natives/object-date-inheritance.png b/1-js/09-classes/06-extend-natives/object-date-inheritance.png deleted file mode 100644 index b5f1932c..00000000 Binary files a/1-js/09-classes/06-extend-natives/object-date-inheritance.png and /dev/null differ diff --git a/1-js/09-classes/06-extend-natives/object-date-inheritance@2x.png b/1-js/09-classes/06-extend-natives/object-date-inheritance@2x.png deleted file mode 100644 index 38276d45..00000000 Binary files a/1-js/09-classes/06-extend-natives/object-date-inheritance@2x.png and /dev/null differ diff --git a/1-js/09-classes/07-instanceof/1-strange-instanceof/solution.md b/1-js/09-classes/06-instanceof/1-strange-instanceof/solution.md similarity index 100% rename from 1-js/09-classes/07-instanceof/1-strange-instanceof/solution.md rename to 1-js/09-classes/06-instanceof/1-strange-instanceof/solution.md diff --git a/1-js/09-classes/07-instanceof/1-strange-instanceof/task.md b/1-js/09-classes/06-instanceof/1-strange-instanceof/task.md similarity index 100% rename from 1-js/09-classes/07-instanceof/1-strange-instanceof/task.md rename to 1-js/09-classes/06-instanceof/1-strange-instanceof/task.md diff --git a/1-js/09-classes/07-instanceof/article.md b/1-js/09-classes/06-instanceof/article.md similarity index 76% rename from 1-js/09-classes/07-instanceof/article.md rename to 1-js/09-classes/06-instanceof/article.md index 702c9e6b..0b02c99b 100644 --- a/1-js/09-classes/07-instanceof/article.md +++ b/1-js/09-classes/06-instanceof/article.md @@ -11,7 +11,7 @@ The syntax is: obj instanceof Class ``` -It returns `true` if `obj` belongs to the `Class` (or a class inheriting from it). +It returns `true` if `obj` belongs to the `Class` or a class inheriting from it. For instance: @@ -46,14 +46,17 @@ alert( arr instanceof Object ); // true Please note that `arr` also belongs to the `Object` class. That's because `Array` prototypally inherits from `Object`. -The `instanceof` operator examines the prototype chain for the check, and is also fine-tunable using the static method `Symbol.hasInstance`. +Normally, `instanceof` operator examines the prototype chain for the check. We can also set a custom logic in the static method `Symbol.hasInstance`. The algorithm of `obj instanceof Class` works roughly as follows: -1. If there's a static method `Symbol.hasInstance`, then use it. Like this: +1. If there's a static method `Symbol.hasInstance`, then just call it: `Class[Symbol.hasInstance](obj)`. It should return either `true` or `false`, and we're done. That's how we can customize the behavior of `instanceof`. + + For example: ```js run - // assume anything that canEat is an animal + // setup instanceOf check that assumes that + // anything with canEat property is an animal class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; @@ -61,22 +64,25 @@ The algorithm of `obj instanceof Class` works roughly as follows: } let obj = { canEat: true }; + alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) is called ``` -2. Most classes do not have `Symbol.hasInstance`. In that case, check if `Class.prototype` equals to one of prototypes in the `obj` prototype chain. +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. - In other words, compare: + In other words, compare one after another: ```js - obj.__proto__ === Class.prototype - obj.__proto__.__proto__ === Class.prototype - obj.__proto__.__proto__.__proto__ === Class.prototype + obj.__proto__ === Class.prototype? + obj.__proto__.__proto__ === Class.prototype? + obj.__proto__.__proto__.__proto__ === Class.prototype? ... + // if any answer is true, return true + // otherwise, if we reached the end of the chain, return false ``` - In the example above `Rabbit.prototype === rabbit.__proto__`, so that gives the answer immediately. + In the example above `rabbit.__proto__ === Rabbit.prototype`, so that gives the answer immediately. - In the case of an inheritance, `rabbit` is an instance of the parent class as well: + In the case of an inheritance, the match will be at the second step: ```js run class Animal {} @@ -86,19 +92,22 @@ The algorithm of `obj instanceof Class` works roughly as follows: *!* alert(rabbit instanceof Animal); // true */!* + // rabbit.__proto__ === Rabbit.prototype + *!* // rabbit.__proto__.__proto__ === Animal.prototype (match!) + */!* ``` Here's the illustration of what `rabbit instanceof Animal` compares with `Animal.prototype`: -![](instanceof.png) +![](instanceof.svg) By the way, there's also a method [objA.isPrototypeOf(objB)](mdn:js/object/isPrototypeOf), that returns `true` if `objA` is somewhere in the chain of prototypes for `objB`. So the test of `obj instanceof Class` can be rephrased as `Class.prototype.isPrototypeOf(obj)`. That's funny, but the `Class` constructor itself does not participate in the check! Only the chain of prototypes and `Class.prototype` matters. -That can lead to interesting consequences when `prototype` is changed. +That can lead to interesting consequences when `prototype` property is changed after the object is created. Like here: @@ -115,9 +124,7 @@ alert( rabbit instanceof Rabbit ); // false */!* ``` -That's one of the reasons to avoid changing `prototype`. Just to keep safe. - -## Bonus: Object toString for the type +## Bonus: Object.prototype.toString for the type We already know that plain objects are converted to string as `[object Object]`: @@ -150,7 +157,7 @@ let objectToString = Object.prototype.toString; // what type is this? let arr = []; -alert( objectToString.call(arr) ); // [object Array] +alert( objectToString.call(arr) ); // [object *!*Array*/!*] ``` Here we used [call](mdn:js/function/call) as described in the chapter [](info:call-apply-decorators) to execute the function `objectToString` in the context `this=arr`. @@ -182,7 +189,7 @@ alert( {}.toString.call(user) ); // [object User] For most environment-specific objects, there is such a property. Here are few browser specific examples: ```js run -// toStringTag for the envinronment-specific object and class: +// toStringTag for the environment-specific object and class: alert( window[Symbol.toStringTag]); // window alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest @@ -194,11 +201,11 @@ As you can see, the result is exactly `Symbol.toStringTag` (if exists), wrapped At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized. -It can be used instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check. +We can use `{}.toString.call` instead of `instanceof` for built-in objects when we want to get the type as a string rather than just to check. ## Summary -Let's recap the type-checking methods that we know: +Let's summarize the type-checking methods that we know: | | works for | returns | |---------------|-------------|---------------| diff --git a/1-js/09-classes/06-instanceof/instanceof.svg b/1-js/09-classes/06-instanceof/instanceof.svg new file mode 100644 index 00000000..920be68b --- /dev/null +++ b/1-js/09-classes/06-instanceof/instanceof.svg @@ -0,0 +1 @@ +Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file diff --git a/1-js/09-classes/07-instanceof/instanceof.png b/1-js/09-classes/07-instanceof/instanceof.png deleted file mode 100644 index eb43cb3d..00000000 Binary files a/1-js/09-classes/07-instanceof/instanceof.png and /dev/null differ diff --git a/1-js/09-classes/07-instanceof/instanceof@2x.png b/1-js/09-classes/07-instanceof/instanceof@2x.png deleted file mode 100644 index f6e06575..00000000 Binary files a/1-js/09-classes/07-instanceof/instanceof@2x.png and /dev/null differ diff --git a/1-js/09-classes/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md new file mode 100644 index 00000000..d5f1ab83 --- /dev/null +++ b/1-js/09-classes/07-mixins/article.md @@ -0,0 +1,208 @@ +# Mixins + +In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]` for an object. And a class may extend only one other class. + +But sometimes that feels limiting. For instance, we have a class `StreetSweeper` and a class `Bicycle`, and want to make their mix: a `StreetSweepingBicycle`. + +Or we have a class `User` and a class `EventEmitter` that implements event generation, and we'd like to add the functionality of `EventEmitter` to `User`, so that our users can emit events. + +There's a concept that can help here, called "mixins". + +As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class containing methods that can be used by other classes without a need to inherit from it. + +In other words, a *mixin* provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes. + +## A mixin example + +The simplest way to implement a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class. + +For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`: + +```js run +*!* +// mixin +*/!* +let sayHiMixin = { + sayHi() { + alert(`Hello ${this.name}`); + }, + sayBye() { + alert(`Bye ${this.name}`); + } +}; + +*!* +// usage: +*/!* +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hello Dude! +``` + +There's no inheritance, but a simple method copying. So `User` may inherit from another class and also include the mixin to "mix-in" the additional methods, like this: + +```js +class User extends Person { + // ... +} + +Object.assign(User.prototype, sayHiMixin); +``` + +Mixins can make use of inheritance inside themselves. + +For instance, here `sayHiMixin` inherits from `sayMixin`: + +```js run +let sayMixin = { + say(phrase) { + alert(phrase); + } +}; + +let sayHiMixin = { + __proto__: sayMixin, // (or we could use Object.create to set the prototype here) + + sayHi() { + *!* + // call parent method + */!* + super.say(`Hello ${this.name}`); // (*) + }, + sayBye() { + super.say(`Bye ${this.name}`); // (*) + } +}; + +class User { + constructor(name) { + this.name = name; + } +} + +// copy the methods +Object.assign(User.prototype, sayHiMixin); + +// now User can say hi +new User("Dude").sayHi(); // Hello Dude! +``` + +Please note that the call to the parent method `super.say()` from `sayHiMixin` (at lines labelled with `(*)`) looks for the method in the prototype of that mixin, not the class. + +Here's the diagram (see the right part): + +![](mixin-inheritance.svg) + +That's because methods `sayHi` and `sayBye` were initially created in `sayHiMixin`. So even though they got copied, their `[[HomeObject]]` internal property references `sayHiMixin`, as shown on the picture above. + +As `super` looks for parent methods in `[[HomeObject]].[[Prototype]]`, that means it searches `sayHiMixin.[[Prototype]]`, not `User.[[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. + +- 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. + +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. + +Or, a `menu` can generate the event `"select"` when a menu item is selected, and other objects may assign handlers to react on that event. And so on. + +Here's the code: + +```js run +let eventMixin = { + /** + * Subscribe to event, usage: + * menu.on('select', function(item) { ... } + */ + on(eventName, handler) { + if (!this._eventHandlers) this._eventHandlers = {}; + if (!this._eventHandlers[eventName]) { + this._eventHandlers[eventName] = []; + } + this._eventHandlers[eventName].push(handler); + }, + + /** + * Cancel the subscription, usage: + * menu.off('select', handler) + */ + off(eventName, handler) { + let handlers = this._eventHandlers && this._eventHandlers[eventName]; + if (!handlers) return; + for (let i = 0; i < handlers.length; i++) { + if (handlers[i] === handler) { + handlers.splice(i--, 1); + } + } + }, + + /** + * Generate an event with the given name and data + * this.trigger('select', data1, data2); + */ + trigger(eventName, ...args) { + if (!this._eventHandlers || !this._eventHandlers[eventName]) { + return; // no handlers for that event name + } + + // call the handlers + this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); + } +}; +``` + + +- `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name happens. Technically, there's `_eventHandlers` property, that stores an array of handlers for each event name. So it just adds it to the list. +- `.off(eventName, handler)` -- removes the function from the handlers list. +- `.trigger(eventName, ...args)` -- generates the event: all handlers from `_eventHandlers[eventName]` are called, with a list of arguments `...args`. + +Usage: + +```js run +// Make a class +class Menu { + choose(value) { + this.trigger("select", value); + } +} +// Add the mixin with event-related methods +Object.assign(Menu.prototype, eventMixin); + +let menu = new Menu(); + +// add a handler, to be called on selection: +*!* +menu.on("select", value => alert(`Value selected: ${value}`)); +*/!* + +// triggers the event => the handler above runs and shows: +// Value selected: 123 +menu.choose("123"); +``` + +Now if we'd like any code to react on menu selection, we can listen to it with `menu.on(...)`. + +And `eventMixin` mixin makes it easy to add such behavior to as many classes as we'd like, without interfering with the inheritance chain. + +## Summary + +*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes. + +Some other languages like 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. + +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. diff --git a/1-js/09-classes/08-mixins/head.html b/1-js/09-classes/07-mixins/head.html similarity index 100% rename from 1-js/09-classes/08-mixins/head.html rename to 1-js/09-classes/07-mixins/head.html diff --git a/1-js/09-classes/07-mixins/mixin-inheritance.svg b/1-js/09-classes/07-mixins/mixin-inheritance.svg new file mode 100644 index 00000000..2e7ba8b3 --- /dev/null +++ b/1-js/09-classes/07-mixins/mixin-inheritance.svg @@ -0,0 +1 @@ +sayHi: function sayBye: functionsayHiMixinsay: functionsayMixin[[Prototype]]constructor: User sayHi: function sayBye: functionUser.prototype[[Prototype]]name: ...user[[HomeObject] \ No newline at end of file diff --git a/1-js/09-classes/08-mixins/article.md b/1-js/09-classes/08-mixins/article.md deleted file mode 100644 index bb51395e..00000000 --- a/1-js/09-classes/08-mixins/article.md +++ /dev/null @@ -1,205 +0,0 @@ -# Mixins - -In JavaScript we can only inherit from a single object. There can be only one `[[Prototype]]` for an object. And a class may extend only one other class. - -But sometimes that feels limiting. For instance, I have a class `StreetSweeper` and a class `Bicycle`, and want to make a `StreetSweepingBicycle`. - -Or, talking about programming, we have a class `Renderer` that implements templating and a class `EventEmitter` that implements event handling, and want to merge these functionalities together with a class `Page`, to make a page that can use templates and emit events. - -There's a concept that can help here, called "mixins". - -As defined in Wikipedia, a [mixin](https://en.wikipedia.org/wiki/Mixin) is a class that contains methods for use by other classes without having to be the parent class of those other classes. - -In other words, a *mixin* provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes. - -## A mixin example - -The simplest way to make a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class. - -For instance here the mixin `sayHiMixin` is used to add some "speech" for `User`: - -```js run -*!* -// mixin -*/!* -let sayHiMixin = { - sayHi() { - alert(`Hello ${this.name}`); - }, - sayBye() { - alert(`Bye ${this.name}`); - } -}; - -*!* -// usage: -*/!* -class User { - constructor(name) { - this.name = name; - } -} - -// copy the methods -Object.assign(User.prototype, sayHiMixin); - -// now User can say hi -new User("Dude").sayHi(); // Hello Dude! -``` - -There's no inheritance, but a simple method copying. So `User` may extend some other class and also include the mixin to "mix-in" the additional methods, like this: - -```js -class User extends Person { - // ... -} - -Object.assign(User.prototype, sayHiMixin); -``` - -Mixins can make use of inheritance inside themselves. - -For instance, here `sayHiMixin` inherits from `sayMixin`: - -```js run -let sayMixin = { - say(phrase) { - alert(phrase); - } -}; - -let sayHiMixin = { - __proto__: sayMixin, // (or we could use Object.create to set the prototype here) - - sayHi() { - *!* - // call parent method - */!* - super.say(`Hello ${this.name}`); - }, - sayBye() { - super.say(`Bye ${this.name}`); - } -}; - -class User { - constructor(name) { - this.name = name; - } -} - -// copy the methods -Object.assign(User.prototype, sayHiMixin); - -// now User can say hi -new User("Dude").sayHi(); // Hello Dude! -``` - -Please note that the call to the parent method `super.say()` from `sayHiMixin` looks for the method in the prototype of that mixin, not the class. - -![](mixin-inheritance.png) - -That's because methods from `sayHiMixin` have `[[HomeObject]]` set to it. So `super` actually means `sayHiMixin.__proto__`, not `User.__proto__`. - -## EventMixin - -Now let's make a mixin for real life. - -The important feature of many objects is working with events. - -That is: an object should have a method to "generate an event" when something important happens to it, and other objects should be able to "listen" to such events. - -An event must have a name and, optionally, bundle some additional data. - -For instance, an object `user` can generate an event `"login"` when the visitor logs in. And another object `calendar` may want to receive such events to load the calendar for the logged-in person. - -Or, a `menu` can generate the event `"select"` when a menu item is selected, and other objects may want to get that information and react on that event. - -Events is a way to "share information" with anyone who wants it. They can be useful in any class, so let's make a mixin for them: - -```js run -let eventMixin = { - /** - * Subscribe to event, usage: - * menu.on('select', function(item) { ... } - */ - on(eventName, handler) { - if (!this._eventHandlers) this._eventHandlers = {}; - if (!this._eventHandlers[eventName]) { - this._eventHandlers[eventName] = []; - } - this._eventHandlers[eventName].push(handler); - }, - - /** - * Cancel the subscription, usage: - * menu.off('select', handler) - */ - off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; - if (!handlers) return; - for (let i = 0; i < handlers.length; i++) { - if (handlers[i] === handler) { - handlers.splice(i--, 1); - } - } - }, - - /** - * Generate the event and attach the data to it - * this.trigger('select', data1, data2); - */ - trigger(eventName, ...args) { - if (!this._eventHandlers || !this._eventHandlers[eventName]) { - return; // no handlers for that event name - } - - // call the handlers - this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); - } -}; -``` - -There are 3 methods here: - -1. `.on(eventName, handler)` -- assigns function `handler` to run when the event with that name happens. The handlers are stored in the `_eventHandlers` property. -2. `.off(eventName, handler)` -- removes the function from the handlers list. -3. `.trigger(eventName, ...args)` -- generates the event: all assigned handlers are called and `args` are passed as arguments to them. - - -Usage: - -```js run -// Make a class -class Menu { - choose(value) { - this.trigger("select", value); - } -} -// Add the mixin -Object.assign(Menu.prototype, eventMixin); - -let menu = new Menu(); - -// call the handler on selection: -*!* -menu.on("select", value => alert(`Value selected: ${value}`)); -*/!* - -// triggers the event => shows Value selected: 123 -menu.choose("123"); // value selected -``` - -Now if we have the code interested to react on user selection, we can bind it with `menu.on(...)`. - -And the `eventMixin` can add such behavior to as many classes as we'd like, without interfering with the inheritance chain. - -## Summary - -*Mixin* -- is a generic object-oriented programming term: a class that contains methods for other classes. - -Some other languages like e.g. python allow to create mixins using multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying them into the prototype. - -We can use mixins as a way to augment a class by multiple behaviors, like event-handling as we have seen above. - -Mixins may become a point of conflict if they occasionally overwrite native class methods. So generally one should think well about the naming for a mixin, to minimize such possibility. diff --git a/1-js/09-classes/08-mixins/mixin-inheritance.png b/1-js/09-classes/08-mixins/mixin-inheritance.png deleted file mode 100644 index 68f9ac27..00000000 Binary files a/1-js/09-classes/08-mixins/mixin-inheritance.png and /dev/null differ diff --git a/1-js/09-classes/08-mixins/mixin-inheritance@2x.png b/1-js/09-classes/08-mixins/mixin-inheritance@2x.png deleted file mode 100644 index cd3c3004..00000000 Binary files a/1-js/09-classes/08-mixins/mixin-inheritance@2x.png and /dev/null differ 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 05ba72e0..303431d6 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 @@ -44,4 +44,4 @@ function f() { f(); // cleanup! ``` -It's `finally` that guarantees the cleanup here. If we just put the code at the end of `f`, it wouldn't run. +It's `finally` that guarantees the cleanup here. If we just put the code at the end of `f`, it wouldn't run in these situations. diff --git a/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md b/1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md index e8468734..c573cc23 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 @@ -33,6 +33,6 @@ Compare the two code fragments. */!* ``` -We definitely need the cleanup after the work has started, doesn't matter if there was an error or not. +We definitely need the cleanup after the work, doesn't matter if there was an error or not. Is there an advantage here in using `finally` or both code fragments are equal? If there is such an advantage, then give an example when it matters. diff --git a/1-js/10-error-handling/1-try-catch/article.md b/1-js/10-error-handling/1-try-catch/article.md index dacc2376..fa8ba5e9 100644 --- a/1-js/10-error-handling/1-try-catch/article.md +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -15,7 +15,7 @@ try { // code... -} catch (err)] { +} catch (err) { // error handling @@ -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 then jumps over `catch`. +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. -![](try-catch-flow.png) +![](try-catch-flow.svg) So, an error inside the `try {…}` block does not kill the script: we have a chance to handle it in `catch`. -Let's see more examples. +Let's see examples. - An errorless example: shows `alert` `(1)` and `(2)`: @@ -50,8 +50,6 @@ Let's see more examples. alert('Catch is ignored, because there are no errors'); // (3) } - - alert("...Then the execution continues"); ``` - An example with an error: shows `(1)` and `(3)`: @@ -68,11 +66,9 @@ Let's see more examples. } catch(err) { - alert(`Error has occured!`); // *!*(3) <--*/!* + alert(`Error has occurred!`); // *!*(3) <--*/!* } - - alert("...Then the execution continues"); ``` @@ -108,7 +104,7 @@ try { } ``` -That's because `try..catch` actually wraps the `setTimeout` call that schedules the function. But the function itself is executed later, when the engine has already left the `try..catch` construct. +That's because the function itself is executed later, when the engine has already left the `try..catch` construct. To catch an exception inside a scheduled function, `try..catch` must be inside that function: ```js run @@ -134,10 +130,10 @@ try { } ``` -For all built-in errors, the error object inside `catch` block has two main properties: +For all built-in errors, the error object has two main properties: `name` -: Error name. For an undefined variable that's `"ReferenceError"`. +: Error name. For instance, for an undefined variable that's `"ReferenceError"`. `message` : Textual message about error details. @@ -157,7 +153,7 @@ try { } catch(err) { alert(err.name); // ReferenceError alert(err.message); // lalala is not defined - alert(err.stack); // ReferenceError: lalala is not defined at ... + alert(err.stack); // ReferenceError: lalala is not defined at (...call stack) // Can also show an error as a whole // The error is converted to string as "name: message" @@ -174,8 +170,8 @@ If we don't need error details, `catch` may omit it: ```js try { // ... -} catch { - // error object omitted +} catch { // <-- without (err) + // ... } ``` @@ -187,7 +183,7 @@ As we already know, JavaScript supports the [JSON.parse(str)](mdn:js/JSON/parse) Usually it's used to decode data received over the network, from the server or another source. -We receive it and call `JSON.parse`, like this: +We receive it and call `JSON.parse` like this: ```js run let json = '{"name":"John", "age": 30}'; // data from the server @@ -302,13 +298,13 @@ try { *!* alert(e.name); // SyntaxError */!* - alert(e.message); // Unexpected token o in JSON at position 0 + alert(e.message); // Unexpected token o in JSON at position 2 } ``` As we can see, that's a `SyntaxError`. -And in our case, the absence of `name` could be treated as a syntax error also, assuming that users must have a `name`. +And in our case, the absence of `name` is an error, as users must have a `name`. So let's throw it: @@ -338,7 +334,7 @@ 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 variable is undefined 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 that "incorrect data" thing. Like this: @@ -355,7 +351,7 @@ try { } ``` -Of course, everything's possible! Programmers do make mistakes. Even in open-source utilities used by millions for decades -- suddenly a crazy bug may be discovered that leads to terrible hacks (like it happened with the `ssh` tool). +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. @@ -489,7 +485,7 @@ The code has two ways of execution: 1. If you answer "Yes" to "Make an error?", then `try -> catch -> finally`. 2. If you say "No", then `try -> finally`. -The `finally` clause is often used when we start doing something before `try..catch` and want to finalize it in any case of outcome. +The `finally` clause is often used when we start doing something and want to finalize it in any case of outcome. For instance, we want to measure the time that a Fibonacci numbers function `fib(n)` takes. Naturally, we can start measuring before it runs and finish afterwards. But what if there's an error during the function call? In particular, the implementation of `fib(n)` in the code below returns an error for negative or non-integer numbers. @@ -521,20 +517,20 @@ try { } */!* -alert(result || "error occured"); +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. -In other words, there may be two ways to exit a function: either a `return` or `throw`. The `finally` clause handles them both. +In other words, the function may finish with `return` or `throw`, that doesn't matter. The `finally` clause executes in both cases. ```smart header="Variables are local inside `try..catch..finally`" Please note that `result` and `diff` variables in the code above are declared *before* `try..catch`. -Otherwise, if `let` were made inside the `{...}` block, it would only be visible inside of it. +Otherwise, if we declared `let` in `try` block, it would only be visible inside of it. ``` ````smart header="`finally` and `return`" @@ -565,7 +561,7 @@ alert( func() ); // first works alert from finally, and then this one ````smart header="`try..finally`" -The `try..finally` construct, without `catch` clause, is also useful. We apply it when we don't want to handle errors right here, but want to be sure that processes that we started are finalized. +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() { @@ -577,7 +573,7 @@ function func() { } } ``` -In the code above, an error inside `try` always falls out, because there's no `catch`. But `finally` works before the execution flow jumps outside. +In the code above, an error inside `try` always falls out, because there's no `catch`. But `finally` works before the execution flow leaves the function. ```` ## Global catch @@ -590,7 +586,7 @@ Let's imagine we've got a fatal error outside of `try..catch`, and the script di Is there a way to react on such occurrences? We may want to log the error, show something to the user (normally they don't see error messages) etc. -There is none in the specification, but environments usually provide it, because it's really useful. For instance, Node.JS has [process.on('uncaughtException')](https://nodejs.org/api/process.html#process_event_uncaughtexception) for that. And in the browser we can assign a function to special [window.onerror](mdn:api/GlobalEventHandlers/onerror) property. It will run in case of an uncaught error. +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. The syntax: @@ -637,13 +633,13 @@ There are also web-services that provide error-logging for such cases, like 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/1-try-catch/try-catch-flow@2x.png b/1-js/10-error-handling/1-try-catch/try-catch-flow@2x.png deleted file mode 100644 index 7515aa8c..00000000 Binary files a/1-js/10-error-handling/1-try-catch/try-catch-flow@2x.png and /dev/null differ 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 5079c746..2414ce7e 100644 --- a/1-js/10-error-handling/2-custom-errors/article.md +++ b/1-js/10-error-handling/2-custom-errors/article.md @@ -6,7 +6,7 @@ Our errors should support basic error properties like `message`, `name` and, pre JavaScript allows to use `throw` with any argument, so technically our custom error classes don't need to inherit from `Error`. But if we inherit, then it becomes possible to use `obj instanceof Error` to identify error objects. So it's better to inherit from it. -As we build our application, our own errors naturally form a hierarchy, for instance `HttpTimeoutError` may inherit from `HttpError`, and so on. +As the application grows, our own errors naturally form a hierarchy, for instance `HttpTimeoutError` may inherit from `HttpError`, and so on. ## Extending Error @@ -17,17 +17,13 @@ Here's an example of how a valid `json` may look: let json = `{ "name": "John", "age": 30 }`; ``` -Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it throws `SyntaxError`. - -But even if `json` is syntactically correct, that doesn't mean that it's a valid user, right? It may miss the necessary data. For instance, it may not have `name` and `age` properties that are essential for our users. +Internally, we'll use `JSON.parse`. If it receives malformed `json`, then it throws `SyntaxError`. But even if `json` is syntactically correct, that doesn't mean that it's a valid user, right? It may miss the necessary data. For instance, it may not have `name` and `age` properties that are essential for our users. Our function `readUser(json)` will not only read JSON, but check ("validate") the data. If there are no required fields, or the format is wrong, then that's an error. And that's not a `SyntaxError`, because the data is syntactically correct, but another kind of error. We'll call it `ValidationError` and create a class for it. An error of that kind should also carry the information about the offending field. Our `ValidationError` class should inherit from the built-in `Error` class. -That class is built-in, but we should have its approximate code before our eyes, to understand what we're extending. - -So here you are: +That class is built-in, here's it approximate code, for us to understand what we're extending: ```js // The "pseudocode" for the built-in Error class defined by JavaScript itself @@ -35,12 +31,12 @@ class Error { constructor(message) { this.message = message; this.name = "Error"; // (different names for different built-in error classes) - this.stack = ; // non-standard, but most environments support it + this.stack = ; // non-standard, but most environments support it } } ``` -Now let's go on and inherit `ValidationError` from it: +Now let's inherit `ValidationError` from it and try it in action: ```js run untrusted *!* @@ -65,10 +61,9 @@ try { } ``` -Please take a look at the constructor: +Please note: in the line `(1)` we call the parent constructor. JavaScript requires us to call `super` in the child constructor, so that's obligatory. The parent constructor sets the `message` property. -1. In the line `(1)` we call the parent constructor. JavaScript requires us to call `super` in the child constructor, so that's obligatory. The parent constructor sets the `message` property. -2. The parent constructor also sets the `name` property to `"Error"`, so in the line `(2)` we reset it to the right value. +The parent constructor also sets the `name` property to `"Error"`, so in the line `(2)` we reset it to the right value. Let's try to use it in `readUser(json)`: @@ -126,7 +121,7 @@ We could also look at `err.name`, like this: The `instanceof` version is much better, because in the future we are going to extend `ValidationError`, make subtypes of it, like `PropertyRequiredError`. And `instanceof` check will continue to work for new inheriting classes. So that's future-proof. -Also it's important that if `catch` meets an unknown error, then it rethrows it in the line `(**)`. The `catch` only knows how to handle validation and syntax errors, other kinds (due to a typo in the code or such) should fall through. +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. ## Further inheritance @@ -185,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 = ` when creating each custom error. But there's a way out. We can make our own "basic error" class that removes this burden from our shoulders by using `this.constructor.name` for `this.name` in the constructor. And then inherit from it. +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. Let's call it `MyError`. @@ -218,9 +213,9 @@ Now custom errors are much shorter, especially `ValidationError`, as we got rid ## Wrapping exceptions -The purpose of the function `readUser` in the code above is "to read the user data", right? There may occur different kinds of errors in the process. Right now we have `SyntaxError` and `ValidationError`, but in the future `readUser` function may grow: the new code will probably generate other kinds of errors. +The 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 to check for different error types and rethrow the unknown ones. But if `readUser` function generates several kinds of errors -- then we should ask ourselves: do we really want to check for all error types one-by-one in every code that calls `readUser`? +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`? 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. @@ -303,5 +298,5 @@ The approach is called "wrapping exceptions", because we take "low level excepti ## Summary - We can inherit from `Error` and other built-in error classes normally, just need to take care of `name` property and don't forget to call `super`. -- Most of the time, we should use `instanceof` to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from the 3rd-party library and there's no easy way to get the class. Then `name` property can be used for such checks. -- Wrapping exceptions is a widespread technique when a function handles low-level exceptions and makes a higher-level object to report about the errors. Low-level exceptions sometimes become properties of that object like `err.cause` in the examples above, but that's not strictly required. +- 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. +- 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 a5a91793..c2f67c6c 100644 --- a/1-js/11-async/01-callbacks/article.md +++ b/1-js/11-async/01-callbacks/article.md @@ -25,15 +25,16 @@ loadScript('/my/script.js'); The function is called "asynchronously," because the action (script loading) finishes not now, but later. -The call initiates the script loading, then the execution continues. While the script is loading, the code below may finish executing, and if the loading takes time, other scripts may run meanwhile too. +If there's a code below `loadScript(…)`, it doesn't wait until the loading finishes. ```js loadScript('/my/script.js'); -// the code below loadScript doesn't wait for the script loading to finish +// the code below loadScript +// doesn't wait for the script loading to finish // ... ``` -Now let's say we want to use the new script when it loads. It probably declares new functions, so we'd like to run them. +We'd like to use the new script as soon as it loads. It declares new functions, and we want to run them. But if we do that immediately after the `loadScript(…)` call, that wouldn't work: @@ -45,7 +46,7 @@ newFunction(); // no such function! */!* ``` -Naturally, the browser probably didn't have time to load the script. So the immediate call to the new function fails. As of now, the `loadScript` function doesn't provide a way to track the load completion. The script loads and eventually runs, that's all. But we'd like to know when it happens, to use new functions and variables from that script. +Naturally, the browser probably didn't have time to load the script. As of now, the `loadScript` function doesn't provide a way to track the load completion. The script loads and eventually runs, that's all. But we'd like to know when it happens, to use new functions and variables from that script. Let's add a `callback` function as a second argument to `loadScript` that should execute when the script loads: @@ -98,7 +99,7 @@ Here we did it in `loadScript`, but of course, it's a general approach. ## Callback in callback -How to load two scripts sequentially: the first one, and then the second one after it? +How can we load two scripts sequentially: the first one, and then the second one after it? The natural solution would be to put the second `loadScript` call inside the callback, like this: @@ -140,7 +141,7 @@ So, every new action is inside a callback. That's fine for few actions, but not ## Handling errors -In examples above we didn't consider errors. What if the script loading fails? Our callback should be able to react on that. +In the above examples we didn't consider errors. What if the script loading fails? Our callback should be able to react on that. Here's an improved version of `loadScript` that tracks loading errors: @@ -222,7 +223,31 @@ As calls become more nested, the code becomes deeper and increasingly more diffi That's sometimes called "callback hell" or "pyramid of doom." -![](callback-hell.png) + + +![](callback-hell.svg) The "pyramid" of nested calls grows to the right with every asynchronous action. Soon it spirals out of control. @@ -262,7 +287,7 @@ function step3(error, script) { See? It does the same, and there's no deep nesting now because we made every action a separate top-level function. -It works, but the code looks like a torn apart spreadsheet. It's difficult to read, and you probably noticed that. One needs to eye-jump between pieces while reading it. That's inconvenient, especially if the reader is not familiar with the code and doesn't know where to eye-jump. +It works, but the code looks like a torn apart spreadsheet. It's difficult to read, and you probably noticed that one needs to eye-jump between pieces while reading it. That's inconvenient, especially if the reader is not familiar with the code and doesn't know where to eye-jump. Also, the functions named `step*` are all of single use, they are created only to avoid the "pyramid of doom." No one is going to reuse them outside of the action chain. So there's a bit of a namespace cluttering here. diff --git a/1-js/11-async/01-callbacks/callback-hell.png b/1-js/11-async/01-callbacks/callback-hell.png deleted file mode 100644 index f42224a2..00000000 Binary files a/1-js/11-async/01-callbacks/callback-hell.png and /dev/null differ diff --git a/1-js/11-async/01-callbacks/callback-hell.svg b/1-js/11-async/01-callbacks/callback-hell.svg new file mode 100644 index 00000000..907f62c2 --- /dev/null +++ b/1-js/11-async/01-callbacks/callback-hell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/1-js/11-async/01-callbacks/callback-hell@2x.png b/1-js/11-async/01-callbacks/callback-hell@2x.png deleted file mode 100644 index 7c9a3ab5..00000000 Binary files a/1-js/11-async/01-callbacks/callback-hell@2x.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/article.md b/1-js/11-async/02-promise-basics/article.md index bf70ee13..5c4bcb2a 100644 --- a/1-js/11-async/02-promise-basics/article.md +++ b/1-js/11-async/02-promise-basics/article.md @@ -2,13 +2,13 @@ Imagine that you're a top singer, and fans ask day and night for your upcoming single. -To get some relief, you promise to send it to them when it's published. You give your fans a list to which they can subscribe for updates. They can fill in their email addresses, so that when the song becomes available, all subscribed parties instantly receive it. And even if something goes very wrong, say, if plans to publish the song are cancelled, they will still be notified. +To get some relief, you promise to send it to them when it's published. You give your fans a list. They can fill in their email addresses, so that when the song becomes available, all subscribed parties instantly receive it. And even if something goes very wrong, say, a fire in the studio, so that you can't publish the song, they will still be notified. -Everyone is happy, because the people don't crowd you any more, and fans, because they won't miss the single. +Everyone is happy: you, because the people don't crowd you anymore, and fans, because they won't miss the single. This is a real-life analogy for things we often have in programming: -1. A "producing code" that does something and takes time. For instance, the code loads a remote script. That's a "singer". +1. A "producing code" that does something and takes time. For instance, a code that loads the data over a network. That's a "singer". 2. A "consuming code" that wants the result of the "producing code" once it's ready. Many functions may need that result. These are the "fans". 3. A *promise* is a special JavaScript object that links the "producing code" and the "consuming code" together. In terms of our analogy: this is the "subscription list". The "producing code" takes whatever time it needs to produce the promised result, and the "promise" makes that result available to all of the subscribed code when it's ready. @@ -22,45 +22,47 @@ let promise = new Promise(function(resolve, reject) { }); ``` -The function passed to `new Promise` is called the *executor*. When the promise is created, this executor function runs automatically. It contains the producing code, that should eventually produce a result. In terms of the analogy above: the executor is the "singer". +The function passed to `new Promise` is called the *executor*. When `new Promise` is created, it runs automatically. It contains the producing code, that should eventually produce a result. In terms of the analogy above: the executor is the "singer". -The resulting `promise` object has internal properties: +Its arguments `resolve` and `reject` are callbacks provided by JavaScript itself. Our code is only inside the executor. -- `state` — initially "pending", then changes to either "fulfilled" or "rejected", -- `result` — an arbitrary value of your choosing, initially `undefined`. +When the executor obtains the result, be it soon or late - doesn't matter, it should call one of these callbacks: -When the executor finishes the job, it should call one of the functions that it gets as arguments: +- `resolve(value)` — if the job finished successfully, with result `value`. +- `reject(error)` — if an error occurred, `error` is the error object. -- `resolve(value)` — to indicate that the job finished successfully: - - sets `state` to `"fulfilled"`, - - sets `result` to `value`. -- `reject(error)` — to indicate that an error occurred: - - sets `state` to `"rejected"`, - - sets `result` to `error`. +So to summarize: the executor runs automatically, it should do a job and then call either `resolve` or `reject`. -![](promise-resolve-reject.png) +The `promise` object returned by `new Promise` constructor has internal properties: -Later we'll see how these changes become known to "fans". +- `state` — initially `"pending"`, then changes to either `"fulfilled"` when `resolve` is called or `"rejected"` when `reject` is called. +- `result` — initially `undefined`, then changes to `value` when `resolve(value)` called or `error` when `reject(error)` is called. -Here's an example of a Promise constructor and a simple executor function with its "producing code" (the `setTimeout`): +So the executor eventually moves `promise` to one of these states: + +![](promise-resolve-reject.svg) + +Later we'll see how "fans" can subscribe to these changes. + +Here's an example of a promise constructor and a simple executor function with "producing code" that takes time (via `setTimeout`): ```js run let promise = new Promise(function(resolve, reject) { // the function is executed automatically when the promise is constructed - // after 1 second signal that the job is done with the result "done!" - setTimeout(() => *!*resolve("done!")*/!*, 1000); + // after 1 second signal that the job is done with the result "done" + setTimeout(() => *!*resolve("done")*/!*, 1000); }); ``` We can see two things by running the code above: -1. The executor is called automatically and immediately (by the `new Promise`). -2. The executor receives two arguments: `resolve` and `reject` — these functions are pre-defined by the JavaScript engine. So we don't need to create them. Instead, we should write the executor to call them when ready. +1. The executor is called automatically and immediately (by `new Promise`). +2. The executor receives two arguments: `resolve` and `reject` — these functions are pre-defined by the JavaScript engine. So we don't need to create them. We only should call one of them when ready. -After one second of "processing" the executor calls `resolve("done")` to produce the result: + After one second of "processing" the executor calls `resolve("done")` to produce the result. This changes the state of the `promise` object: -![](promise-resolve-1.png) + ![](promise-resolve-1.svg) That was an example of a successful job completion, a "fulfilled promise". @@ -73,20 +75,24 @@ let promise = new Promise(function(resolve, reject) { }); ``` -![](promise-reject-1.png) +The call to `reject(...)` moves the promise object to `"rejected"` state: -To summarize, the executor should do a job (something that takes time usually) and then call `resolve` or `reject` to change the state of the corresponding Promise object. +![](promise-reject-1.svg) -The Promise that is either resolved or rejected is called "settled", as opposed to a "pending" Promise. +To summarize, the executor should do a job (something that takes time usually) and then call `resolve` or `reject` to change the state of the corresponding promise object. + +A promise that is either resolved or rejected is called "settled", as opposed to a initially "pending" promise. ````smart header="There can be only a single result or an error" -The executor should call only one `resolve` or one `reject`. The promise's state change is final. +The executor should call only one `resolve` or one `reject`. Any state change is final. All further calls of `resolve` and `reject` are ignored: ```js let promise = new Promise(function(resolve, reject) { +*!* resolve("done"); +*/!* reject(new Error("…")); // ignored setTimeout(() => resolve("…")); // ignored @@ -99,7 +105,7 @@ Also, `resolve`/`reject` expect only one argument (or none) and will ignore addi ```` ```smart header="Reject with `Error` objects" -In case something goes wrong, we can call `reject` with any type of argument (just like `resolve`). But it is recommended to use `Error` objects (or objects that inherit from `Error`). The reasoning for that will soon become apparent. +In case something goes wrong, the executor should call `reject`. That can be done with any type of argument (just like `resolve`). But it is recommended to use `Error` objects (or objects that inherit from `Error`). The reasoning for that will soon become apparent. ``` ````smart header="Immediately calling `resolve`/`reject`" @@ -112,13 +118,13 @@ let promise = new Promise(function(resolve, reject) { }); ``` -For instance, this might happen when we start to do a job but then see that everything has already been completed. +For instance, this might happen when we start to do a job but then see that everything has already been completed and cached. -That's fine. We immediately have a resolved Promise, nothing wrong with that. +That's fine. We immediately have a resolved promise. ```` ```smart header="The `state` and `result` are internal" -The properties `state` and `result` of the Promise object are internal. We can't directly access them from our "consuming code". We can use the methods `.then`/`.catch`/`.finally` for that. They are described below. +The properties `state` and `result` of the Promise object are internal. We can't directly access them. We can use the methods `.then`/`.catch`/`.finally` for that. They are described below. ``` ## Consumers: then, catch, finally @@ -138,17 +144,11 @@ promise.then( ); ``` -The first argument of `.then` is a function that: +The first argument of `.then` is a function that runs when the promise is resolved, and receives the result. -1. runs when the Promise is resolved, and -2. receives the result. +The second argument of `.then` is a function that runs when the promise is rejected, and receives the error. -The second argument of `.then` is a function that: - -1. runs when the Promise is rejected, and -2. receives the error. - -For instance, here's a reaction to a successfuly resolved promise: +For instance, here's a reaction to a successfully resolved promise: ```js run let promise = new Promise(function(resolve, reject) { @@ -214,11 +214,11 @@ The call `.catch(f)` is a complete analog of `.then(null, f)`, it's just a short ### finally -Just like there's a finally clause in a regular `try {...} catch {...}`, there's `finally` in promises. +Just like there's a `finally` clause in a regular `try {...} catch {...}`, there's `finally` in promises. -The call `.finally(f)` is similar to `.then(f, f)` in the sense that it always runs when the promise is settled: be it resolve or reject. +The call `.finally(f)` is similar to `.then(f, f)` in the sense that `f` always runs when the promise is settled: be it resolve or reject. -It is a good handler to perform cleanup, e.g. to stop our loading indicators in `finally`, as they are not needed any more, no matter what the outcome is. +`finally` is a good handler for performing cleanup, e.g. stopping our loading indicators, as they are not needed anymore, no matter what the outcome is. Like this: @@ -233,10 +233,10 @@ new Promise((resolve, reject) => { .then(result => show result, err => show error) ``` -It's not exactly an alias though. There are several important differences: +It's not exactly an alias of `then(f,f)` though. There are several important differences: 1. A `finally` handler has no arguments. In `finally` we don't know whether the promise is successful or not. That's all right, as our task is usually to perform "general" finalizing procedures. -2. Finally passes through results and errors to the next handler. +2. A `finally` handler passes through results and errors to the next handler. For instance, here the result is passed through `finally` to `then`: ```js run @@ -257,14 +257,14 @@ It's not exactly an alias though. There are several important differences: .catch(err => alert(err)); // <-- .catch handles the error object ``` - That's very convenient, because finally is not meant to process promise results. So it passes them through. + That's very convenient, because `finally` is not meant to process a promise result. So it passes it through. We'll talk more about promise chaining and result-passing between handlers in the next chapter. -3. The last, but not the least, `.finally(f)` is a more convenient syntax than `.then(f, f)`: no need to duplicate the function. +3. Last, but not least, `.finally(f)` is a more convenient syntax than `.then(f, f)`: no need to duplicate the function `f`. ````smart header="On settled promises handlers runs immediately" -If a promise is pending, `.then/catch/finally` handlers wait for the result. Otherwise, if a promise has already settled, they execute immediately: +If a promise is pending, `.then/catch/finally` handlers wait for it. Otherwise, if a promise has already settled, they execute immediately: ```js run // an immediately resolved promise @@ -272,13 +272,11 @@ let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done! (shows up right now) ``` - -The good thing is: `.then` handler is guaranteed to run whether the promise takes time or settles it immediately. ```` Next, let's see more practical examples of how promises can help us to write asynchronous code. -## Example: loadScript +## Example: loadScript [#loadscript] We've got the `loadScript` function for loading a script from the previous chapter. @@ -290,7 +288,7 @@ function loadScript(src, callback) { script.src = src; script.onload = () => callback(null, script); - script.onerror = () => callback(new Error(`Script load error ` + src)); + script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); } @@ -307,7 +305,7 @@ function loadScript(src) { script.src = src; script.onload = () => resolve(script); - script.onerror = () => reject(new Error("Script load error: " + src)); + script.onerror = () => reject(new Error(`Script load error for ${src}`)); document.head.append(script); }); @@ -324,7 +322,7 @@ promise.then( error => alert(`Error: ${error.message}`) ); -promise.then(script => alert('One more handler to do something else!')); +promise.then(script => alert('Another handler...')); ``` We can immediately see a few benefits over the callback-based pattern: @@ -335,4 +333,4 @@ We can immediately see a few benefits over the callback-based pattern: | Promises allow us to do things in the natural order. First, we run `loadScript(script)`, and `.then` we write what to do with the result. | We must have a `callback` function at our disposal when calling `loadScript(script, callback)`. In other words, we must know what to do with the result *before* `loadScript` is called. | | We can call `.then` on a Promise as many times as we want. Each time, we're adding a new "fan", a new subscribing function, to the "subscription list". More about this in the next chapter: [](info:promise-chaining). | There can be only one callback. | -So Promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters. +So promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters. diff --git a/1-js/11-async/02-promise-basics/promise-init.png b/1-js/11-async/02-promise-basics/promise-init.png deleted file mode 100644 index c54ce05f..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-init.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-init@2x.png b/1-js/11-async/02-promise-basics/promise-init@2x.png deleted file mode 100644 index 82f0d713..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-init@2x.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-reject-1.png b/1-js/11-async/02-promise-basics/promise-reject-1.png deleted file mode 100644 index 30692f4e..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-reject-1.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-reject-1.svg b/1-js/11-async/02-promise-basics/promise-reject-1.svg new file mode 100644 index 00000000..5a94d805 --- /dev/null +++ b/1-js/11-async/02-promise-basics/promise-reject-1.svg @@ -0,0 +1 @@ +new Promise(executor)state: "pending" result: undefinedreject(error)state: "rejected" result: error \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/promise-reject-1@2x.png b/1-js/11-async/02-promise-basics/promise-reject-1@2x.png deleted file mode 100644 index fd5fb5cf..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-reject-1@2x.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-resolve-1.png b/1-js/11-async/02-promise-basics/promise-resolve-1.png deleted file mode 100644 index 87cb6b2d..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-resolve-1.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-resolve-1.svg b/1-js/11-async/02-promise-basics/promise-resolve-1.svg new file mode 100644 index 00000000..f70eec8c --- /dev/null +++ b/1-js/11-async/02-promise-basics/promise-resolve-1.svg @@ -0,0 +1 @@ +new Promise(executor)state: "pending" result: undefinedresolve("done")state: "fulfilled" result: "done" \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/promise-resolve-1@2x.png b/1-js/11-async/02-promise-basics/promise-resolve-1@2x.png deleted file mode 100644 index 3f6bb90f..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-resolve-1@2x.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-resolve-reject.png b/1-js/11-async/02-promise-basics/promise-resolve-reject.png deleted file mode 100644 index f6c0abd3..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-resolve-reject.png and /dev/null differ diff --git a/1-js/11-async/02-promise-basics/promise-resolve-reject.svg b/1-js/11-async/02-promise-basics/promise-resolve-reject.svg new file mode 100644 index 00000000..657da3ff --- /dev/null +++ b/1-js/11-async/02-promise-basics/promise-resolve-reject.svg @@ -0,0 +1 @@ +new Promise(executor)state: "pending" result: undefinedresolve(value)reject(error)state: "fulfilled" result: valuestate: "rejected" result: error \ No newline at end of file diff --git a/1-js/11-async/02-promise-basics/promise-resolve-reject@2x.png b/1-js/11-async/02-promise-basics/promise-resolve-reject@2x.png deleted file mode 100644 index 6a695361..00000000 Binary files a/1-js/11-async/02-promise-basics/promise-resolve-reject@2x.png and /dev/null differ diff --git a/1-js/11-async/03-promise-chaining/article.md b/1-js/11-async/03-promise-chaining/article.md index 54be060a..e35c619e 100644 --- a/1-js/11-async/03-promise-chaining/article.md +++ b/1-js/11-async/03-promise-chaining/article.md @@ -1,10 +1,7 @@ # Promises chaining -Let's return to the problem mentioned in the chapter . - -- We have a sequence of asynchronous tasks to be done one after another. For instance, loading scripts. -- How to code it well? +Let's return to the problem mentioned in the chapter : we have a sequence of asynchronous tasks to be done one after another. For instance, loading scripts. How can we code it well? Promises provide a couple of recipes to do that. @@ -45,30 +42,12 @@ Here the flow is: As the result is passed along the chain of handlers, we can see a sequence of `alert` calls: `1` -> `2` -> `4`. -![](promise-then-chain.png) +![](promise-then-chain.svg) The whole thing works, because a call to `promise.then` returns a promise, so that we can call the next `.then` on it. When a handler returns a value, it becomes the result of that promise, so the next `.then` is called with it. -To make these words more clear, here's the start of the chain: - -```js run -new Promise(function(resolve, reject) { - - setTimeout(() => resolve(1), 1000); - -}).then(function(result) { - - alert(result); - return result * 2; // <-- (1) - -}) // <-- (2) -// .then… -``` - -The value returned by `.then` is a promise, that's why we are able to add another `.then` at `(2)`. When the value is returned in `(1)`, that promise becomes resolved, so the next handler runs with the value. - **A classic newbie error: technically we can also add many `.then` to a single promise. This is not chaining.** For example: @@ -97,7 +76,7 @@ What we did here is just several handlers to one promise. They don't pass the re Here's the picture (compare it with the chaining above): -![](promise-then-many.png) +![](promise-then-many.svg) All `.then` on the same promise get the same result -- the result of that promise. So in the code above all `alert` show the same: `1`. @@ -105,9 +84,9 @@ In practice we rarely need multiple handlers for one promise. Chaining is used m ## Returning promises -Normally, a value returned by a `.then` handler is immediately passed to the next handler. But there's an exception. +A handler, used in `.then(handler)` may create and return a promise. -If the returned value is a promise, then the further execution is suspended until it settles. After that, the result of that promise is given to the next `.then` handler. +In that case further handlers wait till it settles, and then get its result. For instance: @@ -141,15 +120,15 @@ new Promise(function(resolve, reject) { }); ``` -Here the first `.then` shows `1` returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result (the argument of `resolve`, here it's `result*2`) is passed on to handler of the second `.then` in the line `(**)`. It shows `2` and does the same thing. +Here the first `.then` shows `1` and returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result (the argument of `resolve`, here it's `result * 2`) is passed on to handler of the second `.then`. That handler is in the line `(**)`, it shows `2` and does the same thing. -So the output is again 1 -> 2 -> 4, but now with 1 second delay between `alert` calls. +So the output is the same as in the previous example: 1 -> 2 -> 4, but now with 1 second delay between `alert` calls. Returning promises allows us to build chains of asynchronous actions. ## Example: loadScript -Let's use this feature with `loadScript` to load scripts one by one, in sequence: +Let's use this feature with the promisified `loadScript`, defined in the [previous chapter](info:promise-basics#loadscript), to load scripts one by one, in sequence: ```js run loadScript("/article/promise-chaining/one.js") @@ -187,7 +166,7 @@ Here each `loadScript` call returns a promise, and the next `.then` runs when it We can add more asynchronous actions to the chain. Please note that code is still "flat", it grows down, not to the right. There are no signs of "pyramid of doom". -Please note that technically we can add `.then` directly to each `loadScript`, like this: +Technically, we could add `.then` directly to each `loadScript`, like this: ```js run loadScript("/article/promise-chaining/one.js").then(script1 => { @@ -210,9 +189,7 @@ Sometimes it's ok to write `.then` directly, because the nested function has acc ````smart header="Thenables" -To be precise, `.then` may return an arbitrary "thenable" object, and it will be treated the same way as a promise. - -A "thenable" object is any object with a method `.then`. +To be precise, a handler may return not exactly a promise, but a so-called "thenable" object - an arbitrary object that has method `.then`, and it will be treated the same way as a promise. The idea is that 3rd-party libraries may implement "promise-compatible" objects of their own. They can have extended set of methods, but also be compatible with native promises, because they implement `.then`. @@ -232,7 +209,9 @@ class Thenable { new Promise(resolve => resolve(1)) .then(result => { +*!* return new Thenable(result); // (*) +*/!* }) .then(alert); // shows 2 after 1000ms ``` @@ -247,7 +226,7 @@ This feature allows to integrate custom objects with promise chains without havi In frontend programming promises are often used for network requests. So let's see an extended example of that. -We'll use the [fetch](mdn:api/WindowOrWorkerGlobalScope/fetch) method to load the information about the user from the remote server. The method is quite complex, it has many optional parameters, but the basic usage is quite simple: +We'll use the [fetch](info:fetch) method to load the information about the user from the remote server. It has a lot of optional parameters covered in [separate chapters](info:fetch), but the basic syntax is quite simple: ```js let promise = fetch(url); @@ -264,7 +243,7 @@ fetch('/article/promise-chaining/user.json') // .then below runs when the remote server responds .then(function(response) { // response.text() returns a new promise that resolves with the full response text - // when we finish downloading it + // when it loads return response.text(); }) .then(function(text) { @@ -281,19 +260,19 @@ We'll also use arrow functions for brevity: // same as above, but response.json() parses the remote content as JSON fetch('/article/promise-chaining/user.json') .then(response => response.json()) - .then(user => alert(user.name)); // iliakan + .then(user => alert(user.name)); // iliakan, got user name ``` Now let's do something with the loaded user. -For instance, we can make one more request to github, load the user profile and show the avatar: +For instance, we can make one more request to GitHub, load the user profile and show the avatar: ```js run // Make a request for user.json fetch('/article/promise-chaining/user.json') // Load it as json .then(response => response.json()) - // Make a request to github + // Make a request to GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // Load the response as json .then(response => response.json()) @@ -308,7 +287,7 @@ fetch('/article/promise-chaining/user.json') }); ``` -The code works, see comments about the details, but it should be quite self-descriptive. Although, there's a potential problem in it, a typical error of those who begin to use promises. +The code works, see comments about the details. Although, there's a potential problem in it, a typical error of those who begin to use promises. Look at the line `(*)`: how can we do something *after* the avatar has finished showing and gets removed? For instance, we'd like to show a form for editing that user or something else. As of now, there's no way. @@ -322,7 +301,7 @@ fetch('/article/promise-chaining/user.json') .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) *!* - .then(githubUser => new Promise(function(resolve, reject) { + .then(githubUser => new Promise(function(resolve, reject) { // (*) */!* let img = document.createElement('img'); img.src = githubUser.avatar_url; @@ -332,7 +311,7 @@ fetch('/article/promise-chaining/user.json') setTimeout(() => { img.remove(); *!* - resolve(githubUser); + resolve(githubUser); // (**) */!* }, 3000); })) @@ -340,9 +319,11 @@ fetch('/article/promise-chaining/user.json') .then(githubUser => alert(`Finished showing ${githubUser.name}`)); ``` -Now right after `setTimeout` runs `img.remove()`, it calls `resolve(githubUser)`, thus passing the control to the next `.then` in the chain and passing forward the user data. +That is, `.then` handler in the line `(*)` now returns `new Promise`, that becomes settled only after the call of `resolve(githubUser)` in `setTimeout` `(**)`. -As a rule, an asynchronous action should always return a promise. +The next `.then` in chain will wait for that. + +As a good rule, an asynchronous action should always return a promise. That makes it possible to plan actions after it. Even if we don't plan to extend the chain now, we may need it later. @@ -383,8 +364,8 @@ loadJson('/article/promise-chaining/user.json') ## Summary -If `.then` (or `catch/finally`, doesn't matter) handler returns a promise, the rest of the chain waits until it settles. When it does, its result (or error) is passed further. +If a `.then` (or `catch/finally`, doesn't matter) handler returns a promise, the rest of the chain waits until it settles. When it does, its result (or error) is passed further. Here's a full picture: -![](promise-handler-variants.png) +![](promise-handler-variants.svg) diff --git a/1-js/11-async/03-promise-chaining/head.html b/1-js/11-async/03-promise-chaining/head.html index 31c6b427..0a0075fb 100644 --- a/1-js/11-async/03-promise-chaining/head.html +++ b/1-js/11-async/03-promise-chaining/head.html @@ -10,25 +10,6 @@ function loadScript(src) { document.head.append(script); }); } - -class HttpError extends Error { - constructor(response) { - super(`${response.status} for ${response.url}`); - this.name = 'HttpError'; - this.response = response; - } -} - -function loadJson(url) { - return fetch(url) - .then(response => { - if (response.status == 200) { - return response.json(); - } else { - throw new HttpError(response); - } - }) -} return valuereturn promisethrow errorstate: "fulfilled" result: valuestate: "rejected" result: error...with the result of the new promise...state: "pending" result: undefinedthe call of .then(handler) always returns a promise:if handler ends with…that promise settles with: \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-handler-variants@2x.png b/1-js/11-async/03-promise-chaining/promise-handler-variants@2x.png deleted file mode 100644 index 98d0fa46..00000000 Binary files a/1-js/11-async/03-promise-chaining/promise-handler-variants@2x.png and /dev/null differ diff --git a/1-js/11-async/03-promise-chaining/promise-then-chain.png b/1-js/11-async/03-promise-chaining/promise-then-chain.png deleted file mode 100644 index 52939e5f..00000000 Binary files a/1-js/11-async/03-promise-chaining/promise-then-chain.png and /dev/null differ diff --git a/1-js/11-async/03-promise-chaining/promise-then-chain.svg b/1-js/11-async/03-promise-chaining/promise-then-chain.svg new file mode 100644 index 00000000..631ad64a --- /dev/null +++ b/1-js/11-async/03-promise-chaining/promise-then-chain.svg @@ -0,0 +1 @@ +.thennew Promiseresolve(1)return 2.thenreturn 4.then \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-then-chain@2x.png b/1-js/11-async/03-promise-chaining/promise-then-chain@2x.png deleted file mode 100644 index 731f8c93..00000000 Binary files a/1-js/11-async/03-promise-chaining/promise-then-chain@2x.png and /dev/null differ diff --git a/1-js/11-async/03-promise-chaining/promise-then-many.png b/1-js/11-async/03-promise-chaining/promise-then-many.png deleted file mode 100644 index c37f6fe0..00000000 Binary files a/1-js/11-async/03-promise-chaining/promise-then-many.png and /dev/null differ diff --git a/1-js/11-async/03-promise-chaining/promise-then-many.svg b/1-js/11-async/03-promise-chaining/promise-then-many.svg new file mode 100644 index 00000000..a50359e5 --- /dev/null +++ b/1-js/11-async/03-promise-chaining/promise-then-many.svg @@ -0,0 +1 @@ +.thennew Promiseresolve(1).then.then \ No newline at end of file diff --git a/1-js/11-async/03-promise-chaining/promise-then-many@2x.png b/1-js/11-async/03-promise-chaining/promise-then-many@2x.png deleted file mode 100644 index 6fc13c2c..00000000 Binary files a/1-js/11-async/03-promise-chaining/promise-then-many@2x.png and /dev/null differ diff --git a/1-js/11-async/04-promise-error-handling/article.md b/1-js/11-async/04-promise-error-handling/article.md index 6a0c64c0..624c7e81 100644 --- a/1-js/11-async/04-promise-error-handling/article.md +++ b/1-js/11-async/04-promise-error-handling/article.md @@ -1,11 +1,9 @@ # Error handling with promises -Asynchronous actions may sometimes fail: in case of an error the corresponding promise becomes rejected. For instance, `fetch` fails if the remote server is not available. We can use `.catch` to handle errors (rejections). +Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That's very convenient in practice. -Promise chaining is great at that aspect. When a promise rejects, the control jumps to the closest rejection handler down the chain. That's very convenient in practice. - -For instance, in the code below the URL is wrong (no such server) and `.catch` handles the error: +For instance, in the code below the URL to `fetch` is wrong (no such site) and `.catch` handles the error: ```js run *!* @@ -15,17 +13,9 @@ fetch('https://no-such-server.blabla') // rejects .catch(err => alert(err)) // TypeError: failed to fetch (the text may vary) ``` -Or, maybe, everything is all right with the server, but the response is not a valid JSON: +As you can see, the `.catch` doesn't have to be immediate. It may appear after one or maybe several `.then`. -```js run -fetch('/') // fetch works fine now, the server responds successfully -*!* - .then(response => response.json()) // rejects: the page is HTML, not a valid json -*/!* - .catch(err => alert(err)) // SyntaxError: Unexpected token < in JSON at position 0 -``` - -The easiest way to catch all errors is to append `.catch` to the end of chain: +Or, maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors is to append `.catch` to the end of chain: ```js run fetch('/article/promise-chaining/user.json') @@ -48,11 +38,11 @@ fetch('/article/promise-chaining/user.json') */!* ``` -Normally, `.catch` doesn't trigger at all, because there are no errors. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it. +Normally, such `.catch` doesn't trigger at all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch it. ## Implicit try..catch -The code of a promise executor and promise handlers has an "invisible `try..catch`" around it. If an error happens, it gets caught and treated as a rejection. +The code of a promise executor and promise handlers has an "invisible `try..catch`" around it. If an exception happens, it gets caught and treated as a rejection. For instance, this code: @@ -74,9 +64,9 @@ new Promise((resolve, reject) => { }).catch(alert); // Error: Whoops! ``` -The "invisible `try..catch`" around the executor automatically catches the error and treats it as a rejection. +The "invisible `try..catch`" around the executor automatically catches the error and turns it into rejected promise. -That's so not only in the executor, but in handlers as well. If we `throw` inside `.then` handler, that means a rejected promise, so the control jumps to the nearest error handler. +This happens not only in the executor function, but in its handlers as well. If we `throw` inside a `.then` handler, that means a rejected promise, so the control jumps to the nearest error handler. Here's an example: @@ -90,7 +80,7 @@ new Promise((resolve, reject) => { }).catch(alert); // Error: Whoops! ``` -That's so not only for `throw`, but for any errors, including programming errors as well: +This happens for all errors, not just those caused by the `throw` statement. For example, a programming error: ```js run new Promise((resolve, reject) => { @@ -102,11 +92,11 @@ new Promise((resolve, reject) => { }).catch(alert); // ReferenceError: blabla is not defined ``` -As a side effect, the final `.catch` not only catches explicit rejections, but also occasional errors in the handlers above. +The final `.catch` not only catches explicit rejections, but also occasional errors in the handlers above. ## Rethrowing -As we already noticed, `.catch` behaves like `try..catch`. We may have as many `.then` as we want, and then use a single `.catch` at the end to handle errors in all of them. +As we already noticed, `.catch` at the end of the chain is similar to `try..catch`. We may have as many `.then` handlers as we want, and then use a single `.catch` at the end to handle errors in all of them. In a regular `try..catch` we can analyze the error and maybe rethrow it if can't handle. The same thing is possible for promises. @@ -150,7 +140,7 @@ new Promise((resolve, reject) => { } }).then(function() { - /* never runs here */ + /* doesn't run here */ }).catch(error => { // (**) alert(`The unknown error has occurred: ${error}`); @@ -159,116 +149,28 @@ new Promise((resolve, reject) => { }); ``` -Then the execution jumps from the first `.catch` `(*)` to the next one `(**)` down the chain. - -In the section below we'll see a practical example of rethrowing. - -## Fetch error handling example - -Let's improve error handling for the user-loading example. - -The promise returned by [fetch](mdn:api/WindowOrWorkerGlobalScope/fetch) rejects when it's impossible to make a request. For instance, a remote server is not available, or the URL is malformed. But if the remote server responds with error 404, or even error 500, then it's considered a valid response. - -What if the server returns a non-JSON page with error 500 in the line `(*)`? What if there's no such user, and github returns a page with error 404 at `(**)`? - -```js run -fetch('no-such-user.json') // (*) - .then(response => response.json()) - .then(user => fetch(`https://api.github.com/users/${user.name}`)) // (**) - .then(response => response.json()) - .catch(alert); // SyntaxError: Unexpected token < in JSON at position 0 - // ... -``` - - -As of now, the code tries to load the response as JSON no matter what and dies with a syntax error. You can see that by running the example above, as the file `no-such-user.json` doesn't exist. - -That's not good, because the error just falls through the chain, without details: what failed and where. - -So let's add one more step: we should check the `response.status` property that has HTTP status, and if it's not 200, then throw an error. - -```js run -class HttpError extends Error { // (1) - constructor(response) { - super(`${response.status} for ${response.url}`); - this.name = 'HttpError'; - this.response = response; - } -} - -function loadJson(url) { // (2) - return fetch(url) - .then(response => { - if (response.status == 200) { - return response.json(); - } else { - throw new HttpError(response); - } - }) -} - -loadJson('no-such-user.json') // (3) - .catch(alert); // HttpError: 404 for .../no-such-user.json -``` - -1. We make a custom class for HTTP Errors to distinguish them from other types of errors. Besides, the new class has a constructor that accepts `response` object and saves it in the error. So error-handling code will be able to access it. -2. Then we put together the requesting and error-handling code into a function that fetches the `url` *and* treats any non-200 status as an error. That's convenient, because we often need such logic. -3. Now `alert` shows better message. - -The great thing about having our own class for errors is that we can easily check for it in error-handling code. - -For instance, we can make a request, and then if we get 404 -- ask the user to modify the information. - -The code below loads a user with the given name from github. If there's no such user, then it asks for the correct name: - -```js run -function demoGithubUser() { - let name = prompt("Enter a name?", "iliakan"); - - return loadJson(`https://api.github.com/users/${name}`) - .then(user => { - alert(`Full name: ${user.name}.`); - return user; - }) - .catch(err => { -*!* - if (err instanceof HttpError && err.response.status == 404) { -*/!* - alert("No such user, please reenter."); - return demoGithubUser(); - } else { - throw err; // (*) - } - }); -} - -demoGithubUser(); -``` - -Please note: `.catch` here catches all errors, but it "knows how to handle" only `HttpError 404`. In that particular case it means that there's no such user, and `.catch` just retries in that case. - -For other errors, it has no idea what could go wrong. Maybe a programming error or something. So it just rethrows it in the line `(*)`. +The execution jumps from the first `.catch` `(*)` to the next one `(**)` down the chain. ## Unhandled rejections -What happens when an error is not handled? For instance, after the rethrow `(*)` in the example above. - -Or we could just forget to append an error handler to the end of the chain, like here: +What happens when an error is not handled? For instance, we forgot to append `.catch` to the end of the chain, like here: ```js untrusted run refresh new Promise(function() { noSuchFunction(); // Error here (no such function) }) .then(() => { - // zero or many promise handlers + // successful promise handlers, one or more }); // without .catch at the end! ``` -In case of an error, the promise state becomes "rejected", and the execution should jump to the closest rejection handler. But there is no such handler in the examples above. So the error gets "stuck". +In case of an error, the promise becomes rejected, and the execution should jump to the closest rejection handler. But there is none. So the error gets "stuck". There's no code to handle it. -In practice, just like with a regular unhandled errors, it means that something terribly gone wrong, the script probably died. +In practice, just like with regular unhandled errors in code, it means that something has terribly gone wrong. -Most JavaScript engines track such situations and generate a global error in that case. We can see it in the console. +What happens when a regular error occurs and is not caught by `try..catch`? The script dies with a message in console. Similar thing happens with unhandled promise rejections. + +JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above. In the browser we can catch such errors using the event `unhandledrejection`: @@ -292,52 +194,11 @@ If an error occurs, and there's no `.catch`, the `unhandledrejection` handler tr Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server. -In non-browser environments like Node.JS there are other similar ways to track unhandled errors. - +In non-browser environments like Node.js there are other ways to track unhandled errors. ## Summary -- `.catch` handles promise rejections of all kinds: be it a `reject()` call, or an error thrown in a handler. -- We should place `.catch` exactly in places where we want to handle errors and know how to handle them. The handler should analyze errors (custom error classes help) and rethrow unknown ones. -- It's normal not to use `.catch` if we don't know how to handle errors (all errors are unrecoverable). -- In any case we should have the `unhandledrejection` event handler (for browsers, and analogs for other environments), to track unhandled errors and inform the user (and probably our server) about the them. So that our app never "just dies". - -And finally, if we have load-indication, then `.finally` is a great handler to stop it when the fetch is complete: - -```js run -function demoGithubUser() { - let name = prompt("Enter a name?", "iliakan"); - -*!* - document.body.style.opacity = 0.3; // (1) start the indication -*/!* - - return loadJson(`https://api.github.com/users/${name}`) -*!* - .finally(() => { // (2) stop the indication - document.body.style.opacity = ''; - return new Promise(resolve => setTimeout(resolve, 0)); // (*) - }) -*/!* - .then(user => { - alert(`Full name: ${user.name}.`); - return user; - }) - .catch(err => { - if (err instanceof HttpError && err.response.status == 404) { - alert("No such user, please reenter."); - return demoGithubUser(); - } else { - throw err; - } - }); -} - -demoGithubUser(); -``` - -Here on the line `(1)` we indicate loading by dimming the document. The method doesn't matter, could use any type of indication instead. - -When the promise is settled, be it a successful fetch or an error, `finally` triggers at the line `(2)` and stops the indication. - -There's a little browser trick `(*)` with returning a zero-timeout promise from `finally`. That's because some browsers (like Chrome) need "a bit time" outside promise handlers to paint document changes. So it ensures that the indication is visually stopped before going further on the chain. +- `.catch` handles errors in promises of all kinds: be it a `reject()` call, or an error thrown in a handler. +- We should place `.catch` exactly in places where we want to handle errors and know how to handle them. The handler should analyze errors (custom error classes help) and rethrow unknown ones (maybe they are programming mistakes). +- It's ok not to use `.catch` at all, if there's no way to recover from an error. +- In any case we should have the `unhandledrejection` event handler (for browsers, and analogs for other environments), to track unhandled errors and inform the user (and probably our server) about the them, so that our app never "just dies". diff --git a/1-js/11-async/04-promise-error-handling/head.html b/1-js/11-async/04-promise-error-handling/head.html index 31c6b427..a0b74196 100644 --- a/1-js/11-async/04-promise-error-handling/head.html +++ b/1-js/11-async/04-promise-error-handling/head.html @@ -1,16 +1,4 @@ - - - diff --git a/1-js/11-async/05-promise-api/01-promise-errors-as-results/source.view/index.html b/1-js/11-async/05-promise-api/01-promise-errors-as-results/source.view/index.html deleted file mode 100644 index c760ad8c..00000000 --- a/1-js/11-async/05-promise-api/01-promise-errors-as-results/source.view/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/01-promise-errors-as-results/task.md b/1-js/11-async/05-promise-api/01-promise-errors-as-results/task.md deleted file mode 100644 index e4e77aeb..00000000 --- a/1-js/11-async/05-promise-api/01-promise-errors-as-results/task.md +++ /dev/null @@ -1,48 +0,0 @@ -# Fault-tolerant Promise.all - -We'd like to fetch multiple URLs in parallel. - -Here's the code to do that: - -```js run -let urls = [ - 'https://api.github.com/users/iliakan', - 'https://api.github.com/users/remy', - 'https://api.github.com/users/jeresig' -]; - -Promise.all(urls.map(url => fetch(url))) - // for each response show its status - .then(responses => { // (*) - for(let response of responses) { - alert(`${response.url}: ${response.status}`); - } - }); -``` - -The problem is that if any of requests fails, then `Promise.all` rejects with the error, and we lose results of all the other requests. - -That's not good. - -Modify the code so that the array `responses` in the line `(*)` would include the response objects for successful fetches and error objects for failed ones. - -For instance, if one of URLs is bad, then it should be like: - -```js -let urls = [ - 'https://api.github.com/users/iliakan', - 'https://api.github.com/users/remy', - 'http://no-such-url' -]; - -Promise.all(...) // your code to fetch URLs... - // ...and pass fetch errors as members of the resulting array... - .then(responses => { - // 3 urls => 3 array members - alert(responses[0].status); // 200 - alert(responses[1].status); // 200 - alert(responses[2]); // TypeError: failed to fetch (text may vary) - }); -``` - -P.S. In this task you don't have to load the full response using `response.text()` or `response.json()`. Just handle fetch errors the right way. diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.view/index.html b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.view/index.html deleted file mode 100644 index 744efd2b..00000000 --- a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/solution.view/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/source.view/index.html b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/source.view/index.html deleted file mode 100644 index adb86d41..00000000 --- a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/source.view/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - diff --git a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/task.md b/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/task.md deleted file mode 100644 index 50734a87..00000000 --- a/1-js/11-async/05-promise-api/02-promise-errors-as-results-2/task.md +++ /dev/null @@ -1,34 +0,0 @@ -# Fault-tolerant fetch with JSON - -Improve the solution of the previous task . Now we need not just to call `fetch`, but to load the JSON objects from given URLs. - -Here's the example code to do that: - -```js run -let urls = [ - 'https://api.github.com/users/iliakan', - 'https://api.github.com/users/remy', - 'https://api.github.com/users/jeresig' -]; - -// make fetch requests -Promise.all(urls.map(url => fetch(url))) - // map each response to response.json() - .then(responses => Promise.all( - responses.map(r => r.json()) - )) - // show name of each user - .then(users => { // (*) - for(let user of users) { - alert(user.name); - } - }); -``` - -The problem is that if any of requests fails, then `Promise.all` rejects with the error, and we lose results of all the other requests. So the code above is not fault-tolerant, just like the one in the previous task. - -Modify the code so that the array in the line `(*)` would include parsed JSON for successful requests and error for errored ones. - -Please note that the error may occur both in `fetch` (if the network request fails) and in `response.json()` (if the response is invalid JSON). In both cases the error should become a member of the results object. - -The sandbox has both of these cases. diff --git a/1-js/11-async/05-promise-api/article.md b/1-js/11-async/05-promise-api/article.md index 17c2cca7..5db47b66 100644 --- a/1-js/11-async/05-promise-api/article.md +++ b/1-js/11-async/05-promise-api/article.md @@ -1,71 +1,12 @@ # Promise API -There are 4 static methods in the `Promise` class. We'll quickly cover their use cases here. - -## Promise.resolve - -The syntax: - -```js -let promise = Promise.resolve(value); -``` - -Returns a resolved promise with the given `value`. - -Same as: - -```js -let promise = new Promise(resolve => resolve(value)); -``` - -The method is used when we already have a value, but would like to have it "wrapped" into a promise. - -For instance, the `loadCached` function below fetches the `url` and remembers the result, so that future calls on the same URL return it immediately: - -```js -function loadCached(url) { - let cache = loadCached.cache || (loadCached.cache = new Map()); - - if (cache.has(url)) { -*!* - return Promise.resolve(cache.get(url)); // (*) -*/!* - } - - return fetch(url) - .then(response => response.text()) - .then(text => { - cache.set(url,text); - return text; - }); -} -``` - -We can use `loadCached(url).then(…)`, because the function is guaranteed to return a promise. That's the purpose `Promise.resolve` in the line `(*)`: it makes sure the interface unified. We can always use `.then` after `loadCached`. - -## Promise.reject - -The syntax: - -```js -let promise = Promise.reject(error); -``` - -Create a rejected promise with the `error`. - -Same as: - -```js -let promise = new Promise((resolve, reject) => reject(error)); -``` - -We cover it here for completeness, rarely used in real code. +There are 5 static methods in the `Promise` class. We'll quickly cover their use cases here. ## Promise.all Let's say we want to run many promises to execute in parallel, and wait till all of them are ready. -For instance, download several urls in parallel and process the content when all done. +For instance, download several URLs in parallel and process the content when all are done. That's what `Promise.all` is for. @@ -75,9 +16,9 @@ The syntax is: let promise = Promise.all([...promises...]); ``` -It takes an array of promises (technically can be any iterable, but usually an array) and returns a new promise. +`Promise.all` takes an array of promises (technically can be any iterable, but usually an array) and returns a new promise. -The new promise resolves when all listed promises are settled and has an array of their results. +The new promise resolves when all listed promises are settled and the array of their results becomes its result. For instance, the `Promise.all` below settles after 3 seconds, and then its result is an array `[1, 2, 3]`: @@ -89,7 +30,7 @@ Promise.all([ ]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member ``` -Please note that the relative order is the same. Even though the first promise takes the longest time to resolve, it is still first in the array of results. +Please note that the order of resulting array members is the same as source promises. Even though the first promise takes the longest time to resolve, it's still first in the array of results. A common trick is to map an array of job data into an array of promises, and then wrap that into `Promise.all`. @@ -102,7 +43,7 @@ let urls = [ 'https://api.github.com/users/jeresig' ]; -// map every url to the promise fetch(github url) +// map every url to the promise of the fetch let requests = urls.map(url => fetch(url)); // Promise.all waits until all jobs are resolved @@ -112,7 +53,7 @@ Promise.all(requests) )); ``` -A more real-life example with fetching user information for an array of github users by their names (or we could fetch an array of goods by their ids, the logic is same): +A bigger example with fetching user information for an array of GitHub users by their names (we could fetch an array of goods by their ids, the logic is same): ```js run let names = ['iliakan', 'remy', 'jeresig']; @@ -121,7 +62,7 @@ let requests = names.map(name => fetch(`https://api.github.com/users/${name}`)); Promise.all(requests) .then(responses => { - // all responses are ready, we can show HTTP status codes + // all responses are resolved successfully for(let response of responses) { alert(`${response.url}: ${response.status}`); // shows 200 for every url } @@ -134,7 +75,7 @@ Promise.all(requests) .then(users => users.forEach(user => alert(user.name))); ``` -If any of the promises is rejected, `Promise.all` immediately rejects with that error. +**If any of the promises is rejected, the promise returned by `Promise.all` immediately rejects with that error.** For instance: @@ -150,12 +91,16 @@ Promise.all([ Here the second promise rejects in two seconds. That leads to immediate rejection of `Promise.all`, so `.catch` executes: the rejection error becomes the outcome of the whole `Promise.all`. -The important detail is that promises provide no way to "cancel" or "abort" their execution. So other promises continue to execute, and the eventually settle, but all their results are ignored. +```warn header="In case of an error, other promises are ignored" +If one promise rejects, `Promise.all` immediately rejects, completely forgetting about the other ones in the list. Their results are ignored. -There are ways to avoid this: we can either write additional code to `clearTimeout` (or otherwise cancel) the promises in case of an error, or we can make errors show up as members in the resulting array (see the task below this chapter about it). +For example, if there are multiple `fetch` calls, like in the example above, and one fails, other ones will still continue to execute, but `Promise.all` don't watch them any more. They will probably settle, but the result will be ignored. -````smart header="`Promise.all(...)` allows non-promise items in `iterable`" -Normally, `Promise.all(...)` accepts an iterable (in most cases an array) of promises. But if any of those objects is not a promise, it's wrapped in `Promise.resolve`. +`Promise.all` does nothing to cancel them, as there's no concept of "cancellation" in promises. In [another chapter](info:fetch-abort) we'll cover `AbortController` that can help with that, but it's not a part of the Promise API. +``` + +````smart header="`Promise.all(iterable)` allows non-promise \"regular\" values in `iterable`" +Normally, `Promise.all(...)` accepts an iterable (in most cases an array) of promises. But if any of those objects is not a promise, it's passed to the resulting array "as is". For instance, here the results are `[1, 2, 3]`: @@ -164,18 +109,95 @@ Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), - 2, // treated as Promise.resolve(2) - 3 // treated as Promise.resolve(3) + 2, + 3 ]).then(alert); // 1, 2, 3 ``` -So we are able to pass non-promise values to `Promise.all` where convenient. - +So we are able to pass ready values to `Promise.all` where convenient. ```` +## Promise.allSettled + +[recent browser="new"] + +`Promise.all` rejects as a whole if any promise rejects. That's good for "all or nothing" cases, when we need *all* results to go on: + +```js +Promise.all([ + fetch('/template.html'), + fetch('/style.css'), + fetch('/data.json') +]).then(render); // render method needs results of all fetches +``` + +`Promise.allSettled` waits for all promises to settle. The resulting array has: + +- `{status:"fulfilled", value:result}` for successful responses, +- `{status:"rejected", reason:error}` for errors. + +For example, we'd like to fetch the information about multiple users. Even if one request fails, we're still interested in the others. + +Let's use `Promise.allSettled`: + +```js run +let urls = [ + 'https://api.github.com/users/iliakan', + 'https://api.github.com/users/remy', + 'https://no-such-url' +]; + +Promise.allSettled(urls.map(url => fetch(url))) + .then(results => { // (*) + results.forEach((result, num) => { + if (result.status == "fulfilled") { + alert(`${urls[num]}: ${result.value.status}`); + } + if (result.status == "rejected") { + alert(`${urls[num]}: ${result.reason}`); + } + }); + }); +``` + +The `results` in the line `(*)` above will be: +```js +[ + {status: 'fulfilled', value: ...response...}, + {status: 'fulfilled', value: ...response...}, + {status: 'rejected', reason: ...error object...} +] +``` + +So, for each promise we get its status and `value/error`. + +### Polyfill + +If the browser doesn't support `Promise.allSettled`, it's easy to polyfill: + +```js +if(!Promise.allSettled) { + Promise.allSettled = function(promises) { + return Promise.all(promises.map(p => Promise.resolve(p).then(value => ({ + state: 'fulfilled', + value + }), reason => ({ + state: 'rejected', + reason + })))); + }; +} +``` + +In this code, `promises.map` takes input values, turns into promises (just in case a non-promise was passed) with `p => Promise.resolve(p)`, and then adds `.then` handler to every one. + +That handler turns a successful result `v` into `{state:'fulfilled', value:v}`, and an error `r` into `{state:'rejected', reason:r}`. That's exactly the format of `Promise.allSettled`. + +Then we can use `Promise.allSettled` to get the results or *all* given promises, even if some of them reject. + ## Promise.race -Similar to `Promise.all` takes an iterable of promises, but instead of waiting for all of them to finish -- waits for the first result (or error), and goes on with it. +Similar to `Promise.all`, but waits only for the first settled promise, and gets its result (or error). The syntax is: @@ -193,15 +215,70 @@ Promise.race([ ]).then(alert); // 1 ``` -So, the first result/error becomes the result of the whole `Promise.race`. After the first settled promise "wins the race", all further results/errors are ignored. +The first promise here was fastest, so it became the result. After the first settled promise "wins the race", all further results/errors are ignored. + + +## Promise.resolve/reject + +Methods `Promise.resolve` and `Promise.reject` are rarely needed in modern code, because `async/await` syntax (we'll cover it in [a bit later](info:async-await)) makes them somewhat obsolete. + +We cover them here for completeness, and for those who can't use `async/await` for some reason. + +- `Promise.resolve(value)` creates a resolved promise with the result `value`. + +Same as: + +```js +let promise = new Promise(resolve => resolve(value)); +``` + +The method is used for compatibility, when a function is expected to return a promise. + +For example, `loadCached` function below fetches URL and remembers (caches) its content. For future calls with the same URL it immediately gets the previous content from cache, but uses `Promise.resolve` to make a promise of it, so that the returned value is always a promise: + +```js +let cache = new Map(); + +function loadCached(url) { + if (cache.has(url)) { +*!* + return Promise.resolve(cache.get(url)); // (*) +*/!* + } + + return fetch(url) + .then(response => response.text()) + .then(text => { + cache.set(url,text); + return text; + }); +} +``` + +We can write `loadCached(url).then(…)`, because the function is guaranteed to return a promise. We can always use `.then` after `loadCached`. That's the purpose of `Promise.resolve` in the line `(*)`. + +### Promise.reject + +- `Promise.reject(error)` creates a rejected promise with `error`. + +Same as: + +```js +let promise = new Promise((resolve, reject) => reject(error)); +``` + +In practice, this method is almost never used. ## Summary -There are 4 static methods of `Promise` class: +There are 5 static methods of `Promise` class: -1. `Promise.resolve(value)` -- makes a resolved promise with the given value, -2. `Promise.reject(error)` -- makes a rejected promise with the given error, -3. `Promise.all(promises)` -- waits for all promises to resolve and returns an array of their results. If any of the given promises rejects, then it becomes the error of `Promise.all`, and all other results are ignored. -4. `Promise.race(promises)` -- waits for the first promise to settle, and its result/error becomes the outcome. +1. `Promise.all(promises)` -- waits for all promises to resolve and returns an array of their results. If any of the given promises rejects, then it becomes the error of `Promise.all`, and all other results are ignored. +2. `Promise.allSettled(promises)` (recently added method) -- waits for all promises to settle and returns their results as array of objects with: + - `state`: `"fulfilled"` or `"rejected"` + - `value` (if fulfilled) or `reason` (if rejected). +3. `Promise.race(promises)` -- waits for the first promise to settle, and its result/error becomes the outcome. +4. `Promise.resolve(value)` -- makes a resolved promise with the given value. +5. `Promise.reject(error)` -- makes a rejected promise with the given error. -Of these four, `Promise.all` is the most common in practice. +Of these five, `Promise.all` is probably the most common in practice. diff --git a/1-js/11-async/06-promisify/article.md b/1-js/11-async/06-promisify/article.md index 404b8af0..6d91c049 100644 --- a/1-js/11-async/06-promisify/article.md +++ b/1-js/11-async/06-promisify/article.md @@ -2,8 +2,6 @@ Promisification -- is a long word for a simple transform. It's conversion of a function that accepts a callback into a function returning a promise. -In other words, we create a wrapper-function that does the same, internally calling the original one, but returns a promise. - Such transforms are often needed in real-life, as many functions and libraries are callback-based. But promises are more convenient. So it makes sense to promisify those. For instance, we have `loadScript(src, callback)` from the chapter . @@ -23,7 +21,7 @@ function loadScript(src, callback) { // loadScript('path/script.js', (err, script) => {...}) ``` -Let's promisify it. The new `loadScriptPromise(src)` function will do the same, but accept only `src` (no callback) and return a promise. +Let's promisify it. The new `loadScriptPromise(src)` function will do the same, but accept only `src` (no `callback`) and return a promise. ```js let loadScriptPromise = function(src) { @@ -39,13 +37,13 @@ let loadScriptPromise = function(src) { // loadScriptPromise('path/script.js').then(...) ``` -Now `loadScriptPromise` fits well in our promise-based code. +Now `loadScriptPromise` fits well in promise-based code. As we can see, it delegates all the work to the original `loadScript`, providing its own callback that translates to promise `resolve/reject`. -As we may need to promisify many functions, it makes sense to use a helper. +In practice we'll probably need to promisify many functions, it makes sense to use a helper. -That's actually very simple -- `promisify(f)` below takes a to-promisify function `f` and returns a wrapper function. +We'll call it `promisify(f)`: it accepts a to-promisify function `f` and returns a wrapper function. That wrapper does the same as in the code above: returns a promise and passes the call to the original `f`, tracking the result in a custom callback: @@ -61,7 +59,7 @@ function promisify(f) { } } - args.push(callback); // append our custom callback to the end of arguments + args.push(callback); // append our custom callback to the end of f arguments f.call(this, ...args); // call the original function }); @@ -73,11 +71,11 @@ let loadScriptPromise = promisify(loadScript); loadScriptPromise(...).then(...); ``` -Here we assume that the original function expects a callback with two arguments `(err, result)`. That's what we meet most often. Then our custom callbacks is exactly in the right format, and `promisify` works great for such case. +Here we assume that the original function expects a callback with two arguments `(err, result)`. That's what we encounter most often. Then our custom callback is in exactly the right format, and `promisify` works great for such a case. -But what if the original `f` expects a callback with more arguments `callback(err, res1, res2)`? +But what if the original `f` expects a callback with more arguments `callback(err, res1, res2, ...)`? -Here's a modification of `promisify` that returns an array of multiple callback results: +Here's a more advanced version of `promisify`: if called as `promisify(f, true)`, the promise result will be an array of callback results `[res1, res2, ...]`: ```js // promisify(f, true) to get array of results @@ -102,10 +100,10 @@ function promisify(f, manyArgs = false) { // usage: f = promisify(f, true); -f(...).then(err => ..., arrayOfResults => ...) +f(...).then(arrayOfResults => ..., err => ...) ``` -In some cases, `err` may be absent at all: `callback(result)`, or there's something exotic in the callback format, then we can promisify such functions manually. +For more exotic callback formats, like those without `err` at all: `callback(result)`, we can promisify such functions without using the helper, manually. There are also modules with a bit more flexible promisification functions, e.g. [es6-promisify](https://github.com/digitaldesignlabs/es6-promisify). In Node.js, there's a built-in `util.promisify` function for that. @@ -114,5 +112,5 @@ Promisification is a great approach, especially when you use `async/await` (see Remember, a promise may have only one result, but a callback may technically be called many times. -So promisification is only meant for functions that call the callback once. Furhter calls will be ignored. +So promisification is only meant for functions that call the callback once. Further calls will be ignored. ``` diff --git a/1-js/11-async/07-microtask-queue/article.md b/1-js/11-async/07-microtask-queue/article.md index 3f6011c4..f444e21c 100644 --- a/1-js/11-async/07-microtask-queue/article.md +++ b/1-js/11-async/07-microtask-queue/article.md @@ -1,27 +1,27 @@ -# Microtasks and event loop +# Microtasks Promise handlers `.then`/`.catch`/`.finally` are always asynchronous. -Even when a Promise is immediately resolved, the code on the lines *below* your `.then`/`.catch`/`.finally` will still execute first. +Even when a Promise is immediately resolved, the code on the lines *below* `.then`/`.catch`/`.finally` will still execute before these handlers . -Here's the code that demonstrates it: +Here's the demo: ```js run let promise = Promise.resolve(); -promise.then(() => alert("promise done")); +promise.then(() => alert("promise done!")); alert("code finished"); // this alert shows first ``` -If you run it, you see `code finished` first, and then `promise done`. +If you run it, you see `code finished` first, and then `promise done!`. That's strange, because the promise is definitely done from the beginning. -Why `.then` triggered after? What's going on? +Why did the `.then` trigger afterwards? What's going on? -# Microtasks +## Microtasks queue Asynchronous tasks need proper management. For that, the standard specifies an internal queue `PromiseJobs`, more often referred to as "microtask queue" (v8 term). @@ -30,19 +30,17 @@ As said in the [specification](https://tc39.github.io/ecma262/#sec-jobs-and-job- - The queue is first-in-first-out: tasks enqueued first are run first. - Execution of a task is initiated only when nothing else is running. -Or, to say that simply, when a promise is ready, its `.then/catch/finally` handlers are put into the queue. They are not executed yet. Javascript engine takes a task from the queue and executes it, when it becomes free from the current code. +Or, to say that simply, when a promise is ready, its `.then/catch/finally` handlers are put into the queue. They are not executed yet. JavaScript engine takes a task from the queue and executes it, when it becomes free from the current code. That's why "code finished" in the example above shows first. -![](promiseQueue.png) +![](promiseQueue.svg) Promise handlers always go through that internal queue. -If there's a chain with multiple `.then/catch/finally`, then every one of them is executed asynchronously. +If there's a chain with multiple `.then/catch/finally`, then every one of them is executed asynchronously. That is, it first gets queued, and executed when the current code is complete and previously queued handlers are finished. -That is, it first gets queued, and executed when the current code is complete and previously queued handlers are finished. - -What if the order matters for us? How to make `code finished` work after `promise done`? +**What if the order matters for us? How can we make `code finished` work after `promise done`?** Easy, just put it into the queue with `.then`: @@ -54,100 +52,15 @@ Promise.resolve() Now the order is as intended. -## Event loop - -Browser Javascript, as well as Node.js, is based on an *event loop*. - -"Event loop" is a process when the engine sleeps and waits for events, then reacts on those and sleeps again. - -Examples of events: -- `mousemove`, a user moved their mouse. -- `setTimeout` handler is to be called. -- an external ` ``` -Here we can see it in the browser, but the same is true for any module. - ### Module-level scope Each module has its own top-level scope. In other words, top-level variables and functions from a module are not seen in other scripts. @@ -80,13 +83,13 @@ In the example below, two scripts are imported, and `hello.js` tries to use `use Modules are expected to `export` what they want to be accessible from outside and `import` what they need. -So we should import `user.js` directly into `hello.js` instead of `index.html`. +So we should import `user.js` into `hello.js` and get the required functionality from it instead of relying on global variables. That's the correct variant: [codetabs src="scopes-working" height="140" current="hello.js"] -In the browser, independant top-level scope also exists for each ` ``` -If we really need to make a "global" in-browser variable, we can explicitly assign it to `window` and access as `window.user`. But that's an exception requiring a good reason. +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. ### A module code is evaluated only the first time when imported -If a same module is imported into multiple other places, it's code is executed only the first time, then exports are given to all importers. +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. That has important consequences. Let's see that on examples. @@ -123,10 +126,10 @@ alert("Module is evaluated!"); import `./alert.js`; // Module is evaluated! // 📁 2.js -import `./alert.js`; // (nothing) +import `./alert.js`; // (shows nothing) ``` -In practice, top-level module code is mostly used for initialization. We create data structures, pre-fill them, and if we want something to be reusable -- export it. +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. Now, a more advanced example. @@ -158,9 +161,9 @@ alert(admin.name); // Pete */!* ``` -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 . +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. -Such behavior is great for modules that require configuration. We can set required properties on the first import, and then in further imports it's ready. +Such behavior allows to *configure* modules on first import. We can setup its properties once, and then in further imports it's ready. For instance, `admin.js` module may provide certain functionality, but expect the credentials to come into the `admin` object from outside: @@ -173,7 +176,7 @@ export function sayHi() { } ``` -Now, 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: +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: ```js // 📁 init.js @@ -181,6 +184,8 @@ import {admin} from './admin.js'; admin.name = "Pete"; ``` +Another module can also see `admin.name`: + ```js // 📁 other.js import {admin, sayHi} from './admin.js'; @@ -202,11 +207,13 @@ Its content depends on the environment. In the browser, it contains the url of t ``` -### Top-level "this" is undefined +### In a module, "this" is undefined That's kind of a minor feature, but for completeness we should mention it. -In a module, top-level `this` is undefined, as opposed to a global object in non-module scripts: +In a module, top-level `this` is undefined. + +Compare it to non-module scripts, where `this` is a global object: ```html run height=0 +Compare to regular script below: + ``` -2. External scripts that are fetched from another domain require [CORS](mdn:Web/HTTP/CORS) headers. In other words, if a module script is fetched from another domain, the remote server must supply a header `Access-Control-Allow-Origin: *` (may use fetching domain instead of `*`) to indicate that the fetch is allowed. +2. External scripts that are fetched from another origin (e.g. another site) require [CORS](mdn:Web/HTTP/CORS) headers, as described in the chapter . In other words, if a module script is fetched from another origin, the remote server must supply a header `Access-Control-Allow-Origin` allowing the fetch. ```html @@ -301,17 +312,17 @@ There are two notable differences of external module scripts: That ensures better security by default. -### No bare modules allowed +### No "bare" modules allowed -In the browser, in scripts (not in HTML), `import` must get either a relative or absolute URL. So-called "bare" modules, without a path, are not allowed. +In the browser, `import` must get either a relative or absolute URL. Modules without any path are called "bare" modules. Such modules are not allowed in `import`. For instance, this `import` is invalid: ```js import {sayHi} from 'sayHi'; // Error, "bare" module -// must be './sayHi.js' or wherever the module is +// 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, 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 own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet. ### Compatibility, "nomodule" @@ -328,13 +339,6 @@ Old browsers do not understand `type="module"`. Scripts of the unknown type are ``` -If we use bundle tools, then as modules are bundled together, their `import/export` statements are replaced by special bundler calls, so the resulting build does not require `type="module"`, and we can put it into a regular script: - -```html - - -``` - ## Build tools In real-life, browser modules are rarely used in their "raw" form. Usually, we bundle them together with a special tool such as [Webpack](https://webpack.js.org/) and deploy to the production server. @@ -350,25 +354,32 @@ Build tools do the following: - Unreachable code removed. - Unused exports removed ("tree-shaking"). - Development-specific statements like `console` and `debugger` removed. - - Modern, bleeding-edge Javascript syntax may be transformed to older one with similar functionality using [Babel](https://babeljs.io/). + - Modern, bleeding-edge JavaScript syntax may be transformed to older one with similar functionality using [Babel](https://babeljs.io/). - The resulting file is minified (spaces removed, variables replaced with shorter named etc). +If we use bundle tools, then as scripts are bundled together into a single file (or few files), `import/export` statements inside those scripts are replaced by special bundler functions. So the resulting "bundled" script does not contain any `import/export`, it doesn't require `type="module"`, and we can put it into a regular script: + +```html + + +``` + That said, native modules are also usable. So we won't be using Webpack here: you can configure it later. ## Summary To summarize, the core concepts are: -1. A module is a file. To make `import/export` work, browsers need ` -```smart header="Edge spaces and in-between empty text are usually hidden in tools" +```smart header="Spaces at string start/end and space-only text nodes are usually hidden in tools" Browser tools (to be covered soon) that work with DOM usually do not show spaces at the start/end of the text and empty text nodes (line-breaks) between tags. -That's because they are mainly used to decorate HTML, and do not affect how it is shown (in most cases). +Developer tools save screen space this way. -On further DOM pictures we'll sometimes omit them where they are irrelevant, to keep things short. +On further DOM pictures we'll sometimes omit them when they are irrelevant. Such spaces usually do not affect how the document is displayed. ``` - ## Autocorrection If the browser encounters malformed HTML, it automatically corrects it when making DOM. @@ -106,7 +117,7 @@ drawHtmlTree(node3, 'div.domtree', 690, 150); While generating the DOM, browsers automatically process errors in the document, close tags and so on. -Such an "invalid" document: +Such document with unclosed tags: ```html no-beautify

Hello @@ -148,7 +159,9 @@ You see? The `` appeared out of nowhere. You should keep this in mind whi ## Other node types -Let's add more tags and a comment to the page: +There are some other node types besides elements and text nodes. + +For example, comments: ```html @@ -174,7 +187,7 @@ let node6 = {"name":"HTML","nodeType":1,"children":[{"name":"HEAD","nodeType":1, drawHtmlTree(node6, 'div.domtree', 690, 500); -Here we see a new tree node type -- *comment node*, labeled as `#comment`. +We can see here a new tree node type -- *comment node*, labeled as `#comment`, between two text nodes. We may think -- why is a comment added to the DOM? It doesn't affect the visual representation in any way. But there's a rule -- if something's in HTML, then it also must be in the DOM tree. @@ -195,8 +208,6 @@ There are [12 node types](https://dom.spec.whatwg.org/#node). In practice we usu To see the DOM structure in real-time, try [Live DOM Viewer](http://software.hixie.ch/utilities/js/live-dom-viewer/). Just type in the document, and it will show up DOM at an instant. -## In the browser inspector - 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. @@ -225,10 +236,12 @@ The best way to study them is to click around. Most values are editable in-place ## Interaction with console -As we explore the DOM, we also may want to apply JavaScript to it. Like: get a node and run some code to modify it, to see how it looks. Here are few tips to travel between the Elements tab and the console. +As we work the DOM, we also may want to apply JavaScript to it. Like: get a node and run some code to modify it, to see the result. Here are few tips to travel between the Elements tab and the console. -- Select the first `

  • ` in the Elements tab. -- Press `key:Esc` -- it will open console right below the Elements tab. +For the start: + +1. Select the first `
  • ` in the Elements tab. +2. Press `key:Esc` -- it will open console right below the Elements tab. Now the last selected element is available as `$0`, the previously selected is `$1` etc. @@ -236,9 +249,11 @@ We can run commands on them. For instance, `$0.style.background = 'red'` makes t ![](domconsole0.png) -From the other side, if we're in console and have a variable referencing a DOM node, then we can use the command `inspect(node)` to see it in the Elements pane. +That's how to get a node from Elements in Console. -Or we can just output it in the console and explore "at-place", like `document.body` below: +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: ![](domconsole1.png) diff --git a/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg b/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg deleted file mode 100644 index fa37b2cc..00000000 --- a/2-ui/1-document/02-dom-nodes/statusbarButtonGlyphs.svg +++ /dev/null @@ -1 +0,0 @@ -! \ No newline at end of file diff --git a/2-ui/1-document/03-dom-navigation/1-dom-children/task.md b/2-ui/1-document/03-dom-navigation/1-dom-children/task.md index 4a9e741a..d97f2748 100644 --- a/2-ui/1-document/03-dom-navigation/1-dom-children/task.md +++ b/2-ui/1-document/03-dom-navigation/1-dom-children/task.md @@ -4,7 +4,7 @@ importance: 5 # DOM children -For the page: +Look at this page: ```html @@ -18,7 +18,7 @@ For the page: ``` -How to access: +For each of the following, give at least one way of how to access them: - The `
    ` DOM node? - The `