diff --git a/1-js/01-getting-started/1-intro/article.md b/1-js/01-getting-started/1-intro/article.md index fd7ca66da..b62894b91 100644 --- a/1-js/01-getting-started/1-intro/article.md +++ b/1-js/01-getting-started/1-intro/article.md @@ -4,7 +4,11 @@ Pažvelkime kuo įpatinga JavaScript kalba, ką mes galime su ja padaryti ir kok ## Kas yra JavaScript? +<<<<<<< HEAD Iš pat pradžių *JavaScript* buvo sukurtas tam, kad *"padaryti tinklalapius gyvus"*. +======= +*JavaScript* was initially created to "make web pages alive". +>>>>>>> b52aa942a8e9b75ba8a65124c22593171e273bb6 Programos, parašytos šia kalba yra vadinamos *skriptais*. Jos gali būti parašytos tinklalapio HTML ir veikti automatiškai, kuomet tinklalapis kraunamas. @@ -64,7 +68,11 @@ JavaScript'o galimybės naryklėje yra ribojamos dėl vartotojų saugumo. Tiksla Ribojimų pavyzdžiai: - JavaScript'as tinklalapyje negali skaityti/rašyti failus kietajame diske, juos kopijuoti arba vykdyti programas. JavaScript'as neturi tiesioginios prieigos prie operacinės sistemos funkcijų. +<<<<<<< HEAD Modernios naršklės leidžia dirbti su failais, bet prieiga ribojama ir tai leidžiama tik jeigu vartotojas įvykdo kažką konkretaus. Pavyzdžiui, dragg'ina failą į naršklę arba pažymi failą per `` tagą. +======= +- JavaScript on a webpage may not read/write arbitrary files on the hard disk, copy them or execute programs. It has no direct access to OS functions. +>>>>>>> b52aa942a8e9b75ba8a65124c22593171e273bb6 Yra būdų komunikuoti su kamera/mikrofonu ir kitais įrenginiais, bet tai reikalauja išreikštinio vartotojo leidimo. Taigi, JavaScript'inis tinklalapis negali suktai įjungti web kamerą, stebėti aplinką ir siųsti informaciją į [NSA](https://en.wikipedia.org/wiki/National_Security_Agency). - Skirtingi tabai dažniausiai nežino vienas apie kitą. Tačiau kartais vienas tab'as naudoja JavaScript'ą tam, kad atidarytų kitą tab'ą, bet netgi tokiu atveju, JavaScript'as viename tab'e negali pasiekti kito tab'o, jeigu jie ateina iš skirtingų tinklalapių (skirtingas domenas, protokolas arba portas). diff --git a/1-js/01-getting-started/4-devtools/article.md b/1-js/01-getting-started/4-devtools/article.md index b83fbcb94..5124f182d 100644 --- a/1-js/01-getting-started/4-devtools/article.md +++ b/1-js/01-getting-started/4-devtools/article.md @@ -29,10 +29,19 @@ Konkretus vaizdas priklauso nuo Chrome versijos, kurią naudoji. Kartais atsiran - Čia mes galim pamatyt raudoną error message. Šiuo atveju skriptas turi nežinomą "lalala" komandą. - Dešinėje yra clickable link'as į `bug.html:12` su skaičiumi eilutės, kurioje yra error'as. +<<<<<<< HEAD Žemiau error message'o yra mėlynas `>` simbolis. Jis parodo "command line", kuriame mes galime rašyti JavaScript komandas. Spausk `key:Enter` kad jas paleisti (`key:Shift+Enter` kad rašyti komandas per daugiau nei vieną eilutę). +======= +Below the error message, there is a blue `>` symbol. It marks a "command line" where we can type JavaScript commands. Press `key:Enter` to run them. +>>>>>>> b52aa942a8e9b75ba8a65124c22593171e273bb6 Dabar mes galime matyti error'us. Kaip pradžiai, to pakanka. Vėliau mes grįšim į developer tools'us ir kalbėsim apie debugginimą chapteryje . +```smart header="Multi-line input" +Usually, when we put a line of code into the console, and then press `key:Enter`, it executes. + +To insert multiple lines, press `key:Shift+Enter`. This way one can enter long fragments of JavaScript code. +``` ## Firefox, Edge ir kiti @@ -50,6 +59,7 @@ Atidaryk Preferences ir eik į "Advanced". Apačioj bus checkbox'as: Dabar `key:Cmd+Opt+C` įjungs konsolę. Taip pat turėk omeny, kad naujas item'as "Develop" atsirado viršutiniam meniu. Jame yra daug komandų ir nustatymų. +<<<<<<< HEAD ```smart header="Kelių eilučių komandos" Dažniausiai, jeigu konsolėje parašome vieną eilutę kodo ir paspaudžiame `key:Enter`, ji suveikia. @@ -57,6 +67,9 @@ Tam, kad parašyti kelias eilutes, spausk `key:Shift+Enter`. Tokiu būdu mes gal ``` ## Reziumė +======= +## Summary +>>>>>>> b52aa942a8e9b75ba8a65124c22593171e273bb6 - Developer tools'ai leidžia mum pamatyti errorus, paleisti komandas, analizuoti kintamuosiuos ir daugiau. - Juos paleisti galime su `key:F12` dauguma naršklių Windows'uose. Chrome, jeigu naudojame Mac, reikaluaja `key:Cmd+Opt+J`, Safari: `key:Cmd+Opt+C` (iš pradžių reikia aktyvuoti). diff --git a/1-js/02-first-steps/01-hello-world/1-hello-alert/index.html b/1-js/02-first-steps/01-hello-world/1-hello-alert/index.html new file mode 100644 index 000000000..ff1d871b0 --- /dev/null +++ b/1-js/02-first-steps/01-hello-world/1-hello-alert/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md index e69de29bb..81552913b 100644 --- a/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md +++ b/1-js/02-first-steps/01-hello-world/1-hello-alert/solution.md @@ -0,0 +1,2 @@ + +[html src="index.html"] diff --git a/1-js/02-first-steps/01-hello-world/article.md b/1-js/02-first-steps/01-hello-world/article.md index f5487b915..b3149f112 100644 --- a/1-js/02-first-steps/01-hello-world/article.md +++ b/1-js/02-first-steps/01-hello-world/article.md @@ -46,7 +46,7 @@ The ` + ``` To attach several scripts, use multiple tags: diff --git a/1-js/02-first-steps/02-structure/article.md b/1-js/02-first-steps/02-structure/article.md index b18aab19e..cf1dd53d2 100644 --- a/1-js/02-first-steps/02-structure/article.md +++ b/1-js/02-first-steps/02-structure/article.md @@ -94,7 +94,7 @@ But it should be two separate statements, not one. Such a merging in this case i We recommend putting semicolons between statements even if they are separated by newlines. This rule is widely adopted by the community. Let's note once again -- *it is possible* to leave out semicolons most of the time. But it's safer -- especially for a beginner -- to use them. -## Comments +## Comments [#code-comments] As time goes on, programs become more and more complex. It becomes necessary to add *comments* which describe what the code does and why. @@ -136,7 +136,7 @@ alert('World'); ``` ```smart header="Use hotkeys!" -In most editors, a line of code can be commented out by pressing the `key:Ctrl+/` hotkey for a single-line comment and something like `key:Ctrl+Shift+/` -- for multiline comments (select a piece of code and press the hotkey). For Mac, try `key:Cmd` instead of `key:Ctrl`. +In most editors, a line of code can be commented out by pressing the `key:Ctrl+/` hotkey for a single-line comment and something like `key:Ctrl+Shift+/` -- for multiline comments (select a piece of code and press the hotkey). For Mac, try `key:Cmd` instead of `key:Ctrl` and `key:Option` instead of `key:Shift`. ``` ````warn header="Nested comments are not supported!" diff --git a/1-js/02-first-steps/03-strict-mode/article.md b/1-js/02-first-steps/03-strict-mode/article.md index 573d76bc5..9586733cc 100644 --- a/1-js/02-first-steps/03-strict-mode/article.md +++ b/1-js/02-first-steps/03-strict-mode/article.md @@ -19,8 +19,7 @@ For example: ... ``` -We will learn functions (a way to group commands) soon. Looking ahead, let's note that `"use strict"` can be put at the beginning of the function body instead of the whole script. Doing that enables strict mode in that function only. But usually, people use it for the whole script. - +Quite soon we're going to learn functions (a way to group commands), so let's note in advance that `"use strict"` can be put at the beginning of a function. Doing that enables strict mode in that function only. But usually people use it for the whole script. ````warn header="Ensure that \"use strict\" is at the top" Please make sure that `"use strict"` is at the top of your scripts, otherwise strict mode may not be enabled. @@ -42,16 +41,18 @@ Only comments may appear above `"use strict"`. ```warn header="There's no way to cancel `use strict`" There is no directive like `"no use strict"` that reverts the engine to old behavior. -Once we enter strict mode, there's no return. +Once we enter strict mode, there's no going back. ``` ## Browser console -For the future, when you use a browser console to test features, please note that it doesn't `use strict` by default. +When you use a [developer console](info:devtools) to run code, please note that it doesn't `use strict` by default. Sometimes, when `use strict` makes a difference, you'll get incorrect results. -You can try to press `key:Shift+Enter` to input multiple lines, and put `use strict` on top, like this: +So, how to actually `use strict` in the console? + +First, you can try to press `key:Shift+Enter` to input multiple lines, and put `use strict` on top, like this: ```js 'use strict'; @@ -61,25 +62,28 @@ You can try to press `key:Shift+Enter` to input multiple lines, and put `use str It works in most browsers, namely Firefox and Chrome. -If it doesn't, the most reliable way to ensure `use strict` would be to input the code into console like this: +If it doesn't, e.g. in an old browser, there's an ugly, but reliable way to ensure `use strict`. Put it inside this kind of wrapper: ```js (function() { 'use strict'; - // ...your code... + // ...your code here... })() ``` -## Always "use strict" +## Should we "use strict"? + +The question may sound obvious, but it's not so. + +One could recommend to start scripts with `"use strict"`... But you know what's cool? + +Modern JavaScript supports "classes" and "modules" - advanced language structures (we'll surely get to them), that enable `use strict` automatically. So we don't need to add the `"use strict"` directive, if we use them. -We have yet to cover the differences between strict mode and the "default" mode. +**So, for now `"use strict";` is a welcome guest at the top of your scripts. Later, when your code is all in classes and modules, you may omit it.** -In the next chapters, as we learn language features, we'll note the differences between the strict and default modes. Luckily, there aren't many and they actually make our lives better. +As of now, we've got to know about `use strict` in general. -For now, it's enough to know about it in general: +In the next chapters, as we learn language features, we'll see the differences between the strict and old modes. Luckily, there aren't many and they actually make our lives better. -1. The `"use strict"` directive switches the engine to the "modern" mode, changing the behavior of some built-in features. We'll see the details later in the tutorial. -2. Strict mode is enabled by placing `"use strict"` at the top of a script or function. Several language features, like "classes" and "modules", enable strict mode automatically. -3. Strict mode is supported by all modern browsers. -4. We recommended always starting scripts with `"use strict"`. All examples in this tutorial assume strict mode unless (very rarely) specified otherwise. +All examples in this tutorial assume strict mode unless (very rarely) specified otherwise. diff --git a/1-js/02-first-steps/04-variables/article.md b/1-js/02-first-steps/04-variables/article.md index 6d680b3b0..03eeaa6d0 100644 --- a/1-js/02-first-steps/04-variables/article.md +++ b/1-js/02-first-steps/04-variables/article.md @@ -80,7 +80,6 @@ let user = 'John' Technically, all these variants do the same thing. So, it's a matter of personal taste and aesthetics. - ````smart header="`var` instead of `let`" In older scripts, you may also find another keyword: `var` instead of `let`: @@ -135,6 +134,20 @@ alert(hello); // Hello world! alert(message); // Hello world! ``` +````warn header="Declaring twice triggers an error" +A variable should be declared only once. + +A repeated declaration of the same variable is an error: + +```js run +let message = "This"; + +// repeated 'let' leads to an error +let message = "That"; // SyntaxError: 'message' has already been declared +``` +So, we should declare a variable once and then refer to it without `let`. +```` + ```smart header="Functional languages" It's interesting to note that there exist [functional](https://en.wikipedia.org/wiki/Functional_programming) programming languages, like [Scala](http://www.scala-lang.org/) or [Erlang](http://www.erlang.org/) that forbid changing variable values. @@ -190,7 +203,7 @@ let имя = '...'; let 我 = '...'; ``` -Technically, there is no error here, such names are allowed, but there is an international tradition to use English in variable names. Even if we're writing a small script, it may have a long life ahead. People from other countries may need to read it some time. +Technically, there is no error here. Such names are allowed, but there is an international convention to use English in variable names. Even if we're writing a small script, it may have a long life ahead. People from other countries may need to read it some time. ```` ````warn header="Reserved names" diff --git a/1-js/02-first-steps/05-types/article.md b/1-js/02-first-steps/05-types/article.md index 0da617373..c4a67e83f 100644 --- a/1-js/02-first-steps/05-types/article.md +++ b/1-js/02-first-steps/05-types/article.md @@ -1,6 +1,10 @@ # Data types -A variable in JavaScript can contain any data. A variable can at one moment be a string and at another be a number: +A value in JavaScript is always of a certain type. For example, a string or a number. + +There are eight basic data types in JavaScript. Here, we'll cover them in general and in the next chapters we'll talk about each of them in detail. + +We can put any type in a variable. For example, a variable can at one moment be a string and then store a number: ```js // no error @@ -8,11 +12,9 @@ let message = "hello"; message = 123456; ``` -Programming languages that allow such things are called "dynamically typed", meaning that there are data types, but variables are not bound to any of them. - -There are seven basic data types in JavaScript. Here, we'll cover them in general and in the next chapters we'll talk about each of them in detail. +Programming languages that allow such things, such as JavaScript, are called "dynamically typed", meaning that there exist data types, but variables are not bound to any of them. -## A number +## Number ```js let n = 123; @@ -62,14 +64,35 @@ Special numeric values formally belong to the "number" type. Of course they are We'll see more about working with numbers in the chapter . -## A string +## BigInt + +In JavaScript, the "number" type cannot represent integer values larger than (253-1) (that's `9007199254740991`), or less than -(-253-1) for negatives. It's a technical limitation caused by their internal representation. + +For most purposes that's quite enough, but sometimes we need really big numbers, e.g. for cryptography or microsecond-precision timestamps. + +`BigInt` type was recently added to the language to represent integers of arbitrary length. + +A `BigInt` value is created by appending `n` to the end of an integer: + +```js +// the "n" at the end means it's a BigInt +const bigInt = 1234567890123456789012345678901234567890n; +``` + +As `BigInt` numbers are rarely needed, we don't cover them here, but devoted them a separate chapter . Read it when you need such big numbers. + +```smart header="Compatability issues" +Right now `BigInt` is supported in Firefox/Chrome/Edge, but not in Safari/IE. +``` + +## String A string in JavaScript must be surrounded by quotes. ```js let str = "Hello"; let str2 = 'Single quotes are ok too'; -let phrase = `can embed ${str}`; +let phrase = `can embed another ${str}`; ``` In JavaScript, there are 3 types of quotes. @@ -78,7 +101,7 @@ In JavaScript, there are 3 types of quotes. 2. Single quotes: `'Hello'`. 3. Backticks: `Hello`. -Double and single quotes are "simple" quotes. There's no difference between them in JavaScript. +Double and single quotes are "simple" quotes. There's practically no difference between them in JavaScript. Backticks are "extended functionality" quotes. They allow us to embed variables and expressions into a string by wrapping them in `${…}`, for example: @@ -102,12 +125,12 @@ alert( "the result is ${1 + 2}" ); // the result is ${1 + 2} (double quotes do n We'll cover strings more thoroughly in the chapter . ```smart header="There is no *character* type." -In some languages, there is a special "character" type for a single character. For example, in the C language and in Java it is `char`. +In some languages, there is a special "character" type for a single character. For example, in the C language and in Java it is called "char". In JavaScript, there is no such type. There's only one type: `string`. A string may consist of only one character or many of them. ``` -## A boolean (logical type) +## Boolean (logical type) The boolean type has only two values: `true` and `false`. @@ -144,7 +167,7 @@ In JavaScript, `null` is not a "reference to a non-existing object" or a "null p It's just a special value which represents "nothing", "empty" or "value unknown". -The code above states that `age` is unknown or empty for some reason. +The code above states that `age` is unknown. ## The "undefined" value @@ -155,30 +178,33 @@ The meaning of `undefined` is "value is not assigned". If a variable is declared, but not assigned, then its value is `undefined`: ```js run -let x; +let age; -alert(x); // shows "undefined" +alert(age); // shows "undefined" ``` -Technically, it is possible to assign `undefined` to any variable: +Technically, it is possible to explicitly assign `undefined` to a variable: ```js run -let x = 123; +let age = 100; -x = undefined; +// change the value to undefined +age = undefined; -alert(x); // "undefined" +alert(age); // "undefined" ``` -...But we don't recommend doing that. Normally, we use `null` to assign an "empty" or "unknown" value to a variable, and we use `undefined` for checks like seeing if a variable has been assigned. +...But we don't recommend doing that. Normally, one uses `null` to assign an "empty" or "unknown" value to a variable, while `undefined` is reserved as a default initial value for unassigned things. ## Objects and Symbols The `object` type is special. -All other types are called "primitive" because their values can contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store collections of data and more complex entities. We'll deal with them later in the chapter after we learn more about primitives. +All other types are called "primitive" because their values can contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store collections of data and more complex entities. + +Being that important, objects deserve a special treatment. We'll deal with them later in the chapter , after we learn more about primitives. -The `symbol` type is used to create unique identifiers for objects. We mention it here for completeness, but we'll study it after objects. +The `symbol` type is used to create unique identifiers for objects. We have to mention it here for the sake of completeness, but also postpone the details till we know objects. ## The typeof operator [#type-typeof] @@ -198,6 +224,8 @@ typeof undefined // "undefined" typeof 0 // "number" +typeof 10n // "bigint" + typeof true // "boolean" typeof "foo" // "string" @@ -220,16 +248,16 @@ typeof alert // "function" (3) The last three lines may need additional explanation: 1. `Math` is a built-in object that provides mathematical operations. We will learn it in the chapter . Here, it serves just as an example of an object. -2. The result of `typeof null` is `"object"`. That's wrong. It is an officially recognized error in `typeof`, kept for compatibility. Of course, `null` is not an object. It is a special value with a separate type of its own. So, again, this is an error in the language. -3. The result of `typeof alert` is `"function"`, because `alert` is a function. We'll study functions in the next chapters where we'll also see that there's no special "function" type in JavaScript. Functions belong to the object type. But `typeof` treats them differently, returning `"function"`. That's not quite correct, but very convenient in practice. - +2. The result of `typeof null` is `"object"`. That's an officially recognized error in `typeof` behavior, coming from the early days of JavaScript and kept for compatibility. Definitely, `null` is not an object. It is a special value with a separate type of its own. +3. The result of `typeof alert` is `"function"`, because `alert` is a function. We'll study functions in the next chapters where we'll also see that there's no special "function" type in JavaScript. Functions belong to the object type. But `typeof` treats them differently, returning `"function"`. That also comes from the early days of JavaScript. Technically, such behavior isn't correct, but can be convenient in practice. ## Summary -There are 7 basic data types in JavaScript. +There are 8 basic data types in JavaScript. -- `number` for numbers of any kind: integer or floating-point. -- `string` for strings. A string may have one or more characters, there's no separate single-character type. +- `number` for numbers of any kind: integer or floating-point, integers are limited by ±253. +- `bigint` is for integer numbers of arbitrary length. +- `string` for strings. A string may have zero or more characters, there's no separate single-character type. - `boolean` for `true`/`false`. - `null` for unknown values -- a standalone type that has a single value `null`. - `undefined` for unassigned values -- a standalone type that has a single value `undefined`. diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/solution.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/solution.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md b/1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md similarity index 100% rename from 1-js/02-first-steps/09-alert-prompt-confirm/1-simple-page/task.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/1-simple-page/task.md diff --git a/1-js/02-first-steps/09-alert-prompt-confirm/article.md b/1-js/02-first-steps/06-alert-prompt-confirm/article.md similarity index 79% rename from 1-js/02-first-steps/09-alert-prompt-confirm/article.md rename to 1-js/02-first-steps/06-alert-prompt-confirm/article.md index 8ba414e9c..25bf72482 100644 --- a/1-js/02-first-steps/09-alert-prompt-confirm/article.md +++ b/1-js/02-first-steps/06-alert-prompt-confirm/article.md @@ -1,18 +1,10 @@ # Interaction: alert, prompt, confirm -In this part of the tutorial we cover JavaScript language "as is", without environment-specific tweaks. - -But we'll still be using the browser as our demo environment, so we should know at least a few of its user-interface functions. In this chapter, we'll get familiar with the browser functions `alert`, `prompt` and `confirm`. +As we'll be using the browser as our demo environment, let's see a couple of functions to interact with the user: `alert`, `prompt` and `confirm`. ## alert -Syntax: - -```js -alert(message); -``` - -This shows a message and pauses script execution until the user presses "OK". +This one we've seen already. It shows a message and waits for the user to presses "OK". For example: @@ -20,7 +12,7 @@ For example: alert("Hello"); ``` -The mini-window with the message is called a *modal window*. The word "modal" means that the visitor can't interact with the rest of the page, press other buttons, etc. until they have dealt with the window. In this case -- until they press "OK". +The mini-window with the message is called a *modal window*. The word "modal" means that the visitor can't interact with the rest of the page, press other buttons, etc, until they have dealt with the window. In this case -- until they press "OK". ## prompt @@ -38,7 +30,11 @@ It shows a modal window with a text message, an input field for the visitor, and `default` : An optional second parameter, the initial value for the input field. -The visitor may type something in the prompt input field and press OK. Or they can cancel the input by pressing Cancel or hitting the `key:Esc` key. +```smart header="The square brackets in syntax `[...]`" +The square brackets around `default` in the syntax above denote that the parameter as optional, not required. +``` + +The visitor can type something in the prompt input field and press OK. Then we get that text in the `result`. Or they can cancel the input by pressing Cancel or hitting the `key:Esc` key, then we get `null` as the `result`. The call to `prompt` returns the text from the input field or `null` if the input was canceled. diff --git a/1-js/02-first-steps/06-type-conversions/article.md b/1-js/02-first-steps/07-type-conversions/article.md similarity index 85% rename from 1-js/02-first-steps/06-type-conversions/article.md rename to 1-js/02-first-steps/07-type-conversions/article.md index 20d093ea4..cf97b3307 100644 --- a/1-js/02-first-steps/06-type-conversions/article.md +++ b/1-js/02-first-steps/07-type-conversions/article.md @@ -1,13 +1,15 @@ # Type Conversions -Most of the time, operators and functions automatically convert the values given to them to the right type. +Most of the time, operators and functions automatically convert the values given to them to the right type. For example, `alert` automatically converts any value to a string to show it. Mathematical operations convert values to numbers. There are also cases when we need to explicitly convert a value to the expected type. ```smart header="Not talking about objects yet" -In this chapter, we won't cover objects. Instead, we'll study primitives first. Later, after we learn about objects, we'll see how object conversion works in the chapter . +In this chapter, we won't cover objects. For now we'll just be talking about primitives. + +Later, after we learn about objects, in the chapter we'll see how objects fit in. ``` ## String Conversion @@ -81,18 +83,7 @@ alert( Number(false) ); // 0 Please note that `null` and `undefined` behave differently here: `null` becomes zero while `undefined` becomes `NaN`. -````smart header="Addition '+' concatenates strings" -Almost all mathematical operations convert values to numbers. A notable exception is addition `+`. If one of the added values is a string, the other one is also converted to a string. - -Then, it concatenates (joins) them: - -```js run -alert( 1 + '2' ); // '12' (string to the right) -alert( '1' + 2 ); // '12' (string to the left) -``` - -This only happens when at least one of the arguments is a string. Otherwise, values are converted to numbers. -```` +Most mathematical operators also perform such conversion, we'll see that in the next chapter. ## Boolean Conversion diff --git a/1-js/02-first-steps/07-operators/1-increment-order/solution.md b/1-js/02-first-steps/08-operators/1-increment-order/solution.md similarity index 100% rename from 1-js/02-first-steps/07-operators/1-increment-order/solution.md rename to 1-js/02-first-steps/08-operators/1-increment-order/solution.md diff --git a/1-js/02-first-steps/07-operators/1-increment-order/task.md b/1-js/02-first-steps/08-operators/1-increment-order/task.md similarity index 100% rename from 1-js/02-first-steps/07-operators/1-increment-order/task.md rename to 1-js/02-first-steps/08-operators/1-increment-order/task.md diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/solution.md b/1-js/02-first-steps/08-operators/2-assignment-result/solution.md similarity index 100% rename from 1-js/02-first-steps/07-operators/2-assignment-result/solution.md rename to 1-js/02-first-steps/08-operators/2-assignment-result/solution.md diff --git a/1-js/02-first-steps/07-operators/2-assignment-result/task.md b/1-js/02-first-steps/08-operators/2-assignment-result/task.md similarity index 100% rename from 1-js/02-first-steps/07-operators/2-assignment-result/task.md rename to 1-js/02-first-steps/08-operators/2-assignment-result/task.md diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md similarity index 94% rename from 1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md rename to 1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md index 4964a623a..9a8411fbe 100644 --- a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/solution.md +++ b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/solution.md @@ -10,8 +10,8 @@ true + false = 1 "4" - 2 = 2 "4px" - 2 = NaN 7 / 0 = Infinity -" -9 " + 5 = " -9 5" // (3) -" -9 " - 5 = -14 // (4) +" -9 " + 5 = " -9 5" // (3) +" -9 " - 5 = -14 // (4) null + 1 = 1 // (5) undefined + 1 = NaN // (6) " \t \n" - 2 = -2 // (7) diff --git a/1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md b/1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md similarity index 100% rename from 1-js/02-first-steps/06-type-conversions/1-primitive-conversions-questions/task.md rename to 1-js/02-first-steps/08-operators/3-primitive-conversions-questions/task.md diff --git a/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md b/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md new file mode 100644 index 000000000..04f73fbd0 --- /dev/null +++ b/1-js/02-first-steps/08-operators/4-fix-prompt/solution.md @@ -0,0 +1,32 @@ +The reason is that prompt returns user input as a string. + +So variables have values `"1"` and `"2"` respectively. + +```js run +let a = "1"; // prompt("First number?", 1); +let b = "2"; // prompt("Second number?", 2); + +alert(a + b); // 12 +``` + +What we should to is to convert strings to numbers before `+`. For example, using `Number()` or prepending them with `+`. + +For example, right before `prompt`: + +```js run +let a = +prompt("First number?", 1); +let b = +prompt("Second number?", 2); + +alert(a + b); // 3 +``` + +Or in the `alert`: + +```js run +let a = prompt("First number?", 1); +let b = prompt("Second number?", 2); + +alert(+a + +b); // 3 +``` + +Using both unary and binary `+` in the latest code. Looks funny, doesn't it? diff --git a/1-js/02-first-steps/08-operators/4-fix-prompt/task.md b/1-js/02-first-steps/08-operators/4-fix-prompt/task.md new file mode 100644 index 000000000..b3ea4a3a3 --- /dev/null +++ b/1-js/02-first-steps/08-operators/4-fix-prompt/task.md @@ -0,0 +1,18 @@ +importance: 5 + +--- + +# Fix the addition + +Here's a code that asks the user for two numbers and shows their sum. + +It works incorrectly. The output in the example below is `12` (for default prompt values). + +Why? Fix it. The result should be `3`. + +```js run +let a = prompt("First number?", 1); +let b = prompt("Second number?", 2); + +alert(a + b); // 12 +``` diff --git a/1-js/02-first-steps/07-operators/article.md b/1-js/02-first-steps/08-operators/article.md similarity index 78% rename from 1-js/02-first-steps/07-operators/article.md rename to 1-js/02-first-steps/08-operators/article.md index a1373eade..48c453cec 100644 --- a/1-js/02-first-steps/07-operators/article.md +++ b/1-js/02-first-steps/08-operators/article.md @@ -1,8 +1,8 @@ -# Operators +# Basic operators, maths We know many operators from school. They are things like addition `+`, multiplication `*`, subtraction `-`, and so on. -In this chapter, we'll concentrate on aspects of operators that are not covered by school arithmetic. +In this chapter, we’ll start with simple operators, then concentrate on JavaScript-specific aspects, not covered by school arithmetic. ## Terms: "unary", "binary", "operand" @@ -28,9 +28,55 @@ Before we move on, let's grasp some common terminology. Formally, in the examples above we have two different operators that share the same symbol: the negation operator, a unary operator that reverses the sign, and the subtraction operator, a binary operator that subtracts one number from another. -## String concatenation, binary + +## Maths -Now, let's see special features of JavaScript operators that are beyond school arithmetics. +The following math operations are supported: + +- Addition `+`, +- Subtraction `-`, +- Multiplication `*`, +- Division `/`, +- Remainder `%`, +- Exponentiation `**`. + +The first four are straightforward, while `%` and `**` need a few words about them. + +### Remainder % + +The remainder operator `%`, despite its appearance, is not related to percents. + +The result of `a % b` is the [remainder](https://en.wikipedia.org/wiki/Remainder) of the integer division of `a` by `b`. + +For instance: + +```js run +alert( 5 % 2 ); // 1, a remainder of 5 divided by 2 +alert( 8 % 3 ); // 2, a remainder of 8 divided by 3 +``` + +### Exponentiation ** + +The exponentiation operator `a ** b` multiplies `a` by itself `b` times. + +For instance: + +```js run +alert( 2 ** 2 ); // 4 (2 multiplied by itself 2 times) +alert( 2 ** 3 ); // 8 (2 * 2 * 2, 3 times) +alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2, 4 times) +``` + +Mathematically, the exponentiation is defined for non-integer numbers as well. For example, a square root is an exponentiation by `1/2`: + +```js run +alert( 4 ** (1/2) ); // 2 (power of 1/2 is the same as a square root) +alert( 8 ** (1/3) ); // 2 (power of 1/3 is the same as a cubic root) +``` + + +## String concatenation with binary + + +Let's meet features of JavaScript operators that are beyond school arithmetics. Usually, the plus operator `+` sums numbers. @@ -41,7 +87,7 @@ let s = "my" + "string"; alert(s); // mystring ``` -Note that if one of the operands is a string, the other one is converted to a string too. +Note that if any of the operands is a string, then the other one is converted to a string too. For example: @@ -50,22 +96,23 @@ alert( '1' + 2 ); // "12" alert( 2 + '1' ); // "21" ``` -See, it doesn't matter whether the first operand is a string or the second one. The rule is simple: if either operand is a string, the other one is converted into a string as well. - -However, note that operations run from left to right. If there are two numbers followed by a string, the numbers will be added before being converted to a string: +See, it doesn't matter whether the first operand is a string or the second one. +Here's a more complex example: ```js run alert(2 + 2 + '1' ); // "41" and not "221" ``` -String concatenation and conversion is a special feature of the binary plus `+`. Other arithmetic operators work only with numbers and always convert their operands to numbers. +Here, operators work one after another. The first `+` sums two numbers, so it returns `4`, then the next `+` adds the string `1` to it, so it's like `4 + '1' = 41`. -For instance, subtraction and division: +The binary `+` is the only operator that supports strings in such a way. Other arithmetic operators work only with numbers and always convert their operands to numbers. + +Here's the demo for subtraction and division: ```js run -alert( 2 - '1' ); // 1 -alert( '6' / '2' ); // 3 +alert( 6 - '2' ); // 4, converts '2' to a number +alert( '6' / '2' ); // 3, converts both operands to numbers ``` ## Numeric conversion, unary + @@ -138,17 +185,18 @@ Here's an extract from the [precedence table](https://developer.mozilla.org/en/J | Precedence | Name | Sign | |------------|------|------| | ... | ... | ... | -| 16 | unary plus | `+` | -| 16 | unary negation | `-` | -| 14 | multiplication | `*` | -| 14 | division | `/` | +| 17 | unary plus | `+` | +| 17 | unary negation | `-` | +| 16 | exponentiation | `**` | +| 15 | multiplication | `*` | +| 15 | division | `/` | | 13 | addition | `+` | | 13 | subtraction | `-` | | ... | ... | ... | | 3 | assignment | `=` | | ... | ... | ... | -As we can see, the "unary plus" has a priority of `16` which is higher than the `13` of "addition" (binary plus). That's why, in the expression `"+apples + +oranges"`, unary pluses work before the addition. +As we can see, the "unary plus" has a priority of `17` which is higher than the `13` of "addition" (binary plus). That's why, in the expression `"+apples + +oranges"`, unary pluses work before the addition. ## Assignment @@ -162,24 +210,11 @@ let x = 2 * 2 + 1; alert( x ); // 5 ``` -It is possible to chain assignments: - -```js run -let a, b, c; - -*!* -a = b = c = 2 + 2; -*/!* +### Assignment = returns a value -alert( a ); // 4 -alert( b ); // 4 -alert( c ); // 4 -``` - -Chained assignments evaluate from right to left. First, the rightmost expression `2 + 2` is evaluated and then assigned to the variables on the left: `c`, `b` and `a`. At the end, all the variables share a single value. +The fact of `=` being an operator, not a "magical" language construct has an interesting implication. -````smart header="The assignment operator `\"=\"` returns a value" -An operator always returns a value. That's obvious for most of them like addition `+` or multiplication `*`. But the assignment operator follows this rule too. +Most operators in JavaScript return a value. That's obvious for `+` and `-`, but also true for `=`. The call `x = value` writes the `value` into `x` *and then returns it*. @@ -199,49 +234,74 @@ alert( c ); // 0 In the example above, the result of expression `(a = b + 1)` is the value which was assigned to `a` (that is `3`). It is then used for further evaluations. -Funny code, isn't it? We should understand how it works, because sometimes we see it in JavaScript libraries, but shouldn't write anything like that ourselves. Such tricks definitely don't make code clearer or readable. -```` +Funny code, isn't it? We should understand how it works, because sometimes we see it in JavaScript libraries. -## Remainder % +Although, please don't write the code like that. Such tricks definitely don't make code clearer or readable. -The remainder operator `%`, despite its appearance, is not related to percents. - -The result of `a % b` is the remainder of the integer division of `a` by `b`. +### Chaining assignments -For instance: +Another interesting feature is the ability to chain assignments: ```js run -alert( 5 % 2 ); // 1 is a remainder of 5 divided by 2 -alert( 8 % 3 ); // 2 is a remainder of 8 divided by 3 -alert( 6 % 3 ); // 0 is a remainder of 6 divided by 3 +let a, b, c; + +*!* +a = b = c = 2 + 2; +*/!* + +alert( a ); // 4 +alert( b ); // 4 +alert( c ); // 4 ``` -## Exponentiation ** +Chained assignments evaluate from right to left. First, the rightmost expression `2 + 2` is evaluated and then assigned to the variables on the left: `c`, `b` and `a`. At the end, all the variables share a single value. -The exponentiation operator `**` is a recent addition to the language. +Once again, for the purposes of readability it's better to split such code into few lines: -For a natural number `b`, the result of `a ** b` is `a` multiplied by itself `b` times. +```js +c = 2 + 2; +b = c; +a = c; +``` +That's easier to read, especially when eye-scanning the code fast. -For instance: +## Modify-in-place + +We often need to apply an operator to a variable and store the new result in that same variable. + +For example: + +```js +let n = 2; +n = n + 5; +n = n * 2; +``` + +This notation can be shortened using the operators `+=` and `*=`: ```js run -alert( 2 ** 2 ); // 4 (2 * 2) -alert( 2 ** 3 ); // 8 (2 * 2 * 2) -alert( 2 ** 4 ); // 16 (2 * 2 * 2 * 2) +let n = 2; +n += 5; // now n = 7 (same as n = n + 5) +n *= 2; // now n = 14 (same as n = n * 2) + +alert( n ); // 14 ``` -The operator works for non-integer numbers as well. +Short "modify-and-assign" operators exist for all arithmetical and bitwise operators: `/=`, `-=`, etc. -For instance: +Such operators have the same precedence as a normal assignment, so they run after most other calculations: ```js run -alert( 4 ** (1/2) ); // 2 (power of 1/2 is the same as a square root, that's maths) -alert( 8 ** (1/3) ); // 2 (power of 1/3 is the same as a cubic root) +let n = 2; + +n *= 3 + 5; + +alert( n ); // 16 (right part evaluated first, same as n *= 8) ``` ## Increment/decrement - + Increasing or decreasing a number by one is among the most common numerical operations. @@ -368,41 +428,7 @@ The list of operators: - RIGHT SHIFT ( `>>` ) - ZERO-FILL RIGHT SHIFT ( `>>>` ) -These operators are used very rarely. To understand them, we need to delve into low-level number representation and it would not be optimal to do that right now, especially since we won't need them any time soon. If you're curious, you can read the [Bitwise Operators](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) article on MDN. It would be more practical to do that when a real need arises. - -## Modify-in-place - -We often need to apply an operator to a variable and store the new result in that same variable. - -For example: - -```js -let n = 2; -n = n + 5; -n = n * 2; -``` - -This notation can be shortened using the operators `+=` and `*=`: - -```js run -let n = 2; -n += 5; // now n = 7 (same as n = n + 5) -n *= 2; // now n = 14 (same as n = n * 2) - -alert( n ); // 14 -``` - -Short "modify-and-assign" operators exist for all arithmetical and bitwise operators: `/=`, `-=`, etc. - -Such operators have the same precedence as a normal assignment, so they run after most other calculations: - -```js run -let n = 2; - -n *= 3 + 5; - -alert( n ); // 16 (right part evaluated first, same as n *= 8) -``` +These operators are used very rarely, when we need to fiddle with numbers on the very lowest (bitwise) level. We won't need these operators any time soon, as web development has little use of them, but in some special areas, such as cryptography, they are useful. You can read the [Bitwise Operators](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) article on MDN when a need arises. ## Comma diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md similarity index 89% rename from 1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md rename to 1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md index 6437b512e..a86a9f73e 100644 --- a/1-js/02-first-steps/08-comparison/1-comparison-questions/solution.md +++ b/1-js/02-first-steps/09-comparison/1-comparison-questions/solution.md @@ -13,7 +13,7 @@ null === +"\n0\n" → false Some of the reasons: 1. Obviously, true. -2. Dictionary comparison, hence false. +2. Dictionary comparison, hence false. `"a"` is smaller than `"p"`. 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. diff --git a/1-js/02-first-steps/08-comparison/1-comparison-questions/task.md b/1-js/02-first-steps/09-comparison/1-comparison-questions/task.md similarity index 100% rename from 1-js/02-first-steps/08-comparison/1-comparison-questions/task.md rename to 1-js/02-first-steps/09-comparison/1-comparison-questions/task.md diff --git a/1-js/02-first-steps/08-comparison/article.md b/1-js/02-first-steps/09-comparison/article.md similarity index 94% rename from 1-js/02-first-steps/08-comparison/article.md rename to 1-js/02-first-steps/09-comparison/article.md index d889b1328..012d359e0 100644 --- a/1-js/02-first-steps/08-comparison/article.md +++ b/1-js/02-first-steps/09-comparison/article.md @@ -1,15 +1,19 @@ # Comparisons -We know many comparison operators from maths: +We know many comparison operators from maths. + +In JavaScript they are written like this: - Greater/less than: a > b, a < b. - Greater/less than or equals: a >= b, a <= b. -- Equals: `a == b` (please note the double equals sign `=`. A single symbol `a = b` would mean an assignment). -- Not equals. In maths the notation is , but in JavaScript it's written as an assignment with an exclamation sign before it: a != b. +- Equals: `a == b`, please note the double equality sign `=` means the equality test, while a single one `a = b` means an assignment. +- Not equals. In maths the notation is , but in JavaScript it's written as a != b. + +In this article we'll learn more about different types of comparisons, how JavaScript makes them, including important peculiarities. ## Boolean is the result -Like all other operators, a comparison returns a value. In this case, the value is a boolean. +All comparison operators return a boolean value: - `true` -- means "yes", "correct" or "the truth". - `false` -- means "no", "wrong" or "not the truth". diff --git a/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md b/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md index 8f4d664e8..f85b56366 100644 --- a/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md +++ b/1-js/02-first-steps/11-logical-operators/2-alert-or/solution.md @@ -6,7 +6,7 @@ alert( alert(1) || 2 || alert(3) ); The call to `alert` does not return a value. Or, in other words, it returns `undefined`. -1. The first OR `||` evaluates it's left operand `alert(1)`. That shows the first message with `1`. +1. The first OR `||` evaluates its left operand `alert(1)`. That shows the first message with `1`. 2. The `alert` returns `undefined`, so OR goes on to the second operand searching for a truthy value. 3. The second operand `2` is truthy, so the execution is halted, `2` is returned and then shown by the outer alert. diff --git a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg index cbc8c7840..ca3e0aead 100644 --- a/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg +++ b/1-js/02-first-steps/11-logical-operators/9-check-login/ifelse_task.svg @@ -1 +1 @@ -BeginCanceledCanceledWelcome!I don't know youWrong passwordWho's there?Password?CancelCancelAdminTheMasterOtherOther \ No newline at end of file +BeginCanceledCanceledWelcome!I don't know youWrong passwordWho's there?Password?CancelCancelAdminTheMasterOtherOther \ No newline at end of file diff --git a/1-js/02-first-steps/11-logical-operators/article.md b/1-js/02-first-steps/11-logical-operators/article.md index 25f8ff7f5..f45cbe456 100644 --- a/1-js/02-first-steps/11-logical-operators/article.md +++ b/1-js/02-first-steps/11-logical-operators/article.md @@ -90,10 +90,10 @@ For instance: ```js run alert( 1 || 0 ); // 1 (1 is truthy) -alert( true || 'no matter what' ); // (true is truthy) alert( null || 1 ); // 1 (1 is the first truthy value) alert( null || 0 || 1 ); // 1 (the first truthy value) + alert( undefined || null || 0 ); // 0 (all falsy, returns the last value) ``` @@ -101,53 +101,40 @@ This leads to some interesting usage compared to a "pure, classical, boolean-onl 1. **Getting the first truthy value from a list of variables or expressions.** - Imagine we have a list of variables which can either contain data or be `null/undefined`. How can we find the first one with data? + For instance, we have `firstName`, `lastName` and `nickName` variables, all optional. - We can use OR `||`: + Let's use OR `||` to choose the one that has the data and show it (or `anonymous` if nothing set): ```js run - let currentUser = null; - let defaultUser = "John"; + let firstName = ""; + let lastName = ""; + let nickName = "SuperCoder"; *!* - let name = currentUser || defaultUser || "unnamed"; + alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder */!* - - alert( name ); // selects "John" – the first truthy value ``` - If both `currentUser` and `defaultUser` were falsy, `"unnamed"` would be the result. -2. **Short-circuit evaluation.** - - Operands can be not only values, but arbitrary expressions. OR evaluates and tests them from left to right. The evaluation stops when a truthy value is reached, and the value is returned. This process is called "a short-circuit evaluation" because it goes as short as possible from left to right. + If all variables were falsy, `Anonymous` would show up. - This is clearly seen when the expression given as the second argument has a side effect like a variable assignment. +2. **Short-circuit evaluation.** - In the example below, `x` does not get assigned: + Another feature of OR `||` operator is the so-called "short-circuit" evaluation. - ```js run no-beautify - let x; + It means that `||` processes its arguments until the first truthy value is reached, and then the value is returned immediately, without even touching the other argument. - *!*true*/!* || (x = 1); + That importance of this feature becomes obvious if an operand isn't just a value, but an expression with a side effect, such as a variable assignment or a function call. - alert(x); // undefined, because (x = 1) not evaluated - ``` - - If, instead, the first argument is `false`, `||` evaluates the second one, thus running the assignment: + In the example below, only the second message is printed: ```js run no-beautify - let x; - - *!*false*/!* || (x = 1); - - alert(x); // 1 + *!*true*/!* || alert("not printed"); + *!*false*/!* || alert("printed"); ``` - An assignment is a simple case. There may be side effects, that won't show up if the evaluation doesn't reach them. + In the first line, the OR `||` operator stops the evaluation immediately upon seeing `true`, so the `alert` isn't run. - As we can see, such a use case is a "shorter way of doing `if`". The first operand is converted to boolean. If it's false, the second one is evaluated. - - Most of time, it's better to use a "regular" `if` to keep the code easy to understand, but sometimes this can be handy. + Sometimes, people use this feature to execute commands only if the condition on the left part is falsy. ## && (AND) @@ -236,7 +223,8 @@ The precedence of AND `&&` operator is higher than OR `||`. So the code `a && b || c && d` is essentially the same as if the `&&` expressions were in parentheses: `(a && b) || (c && d)`. ```` -Just like OR, the AND `&&` operator can sometimes replace `if`. +````warn header="Don't replace `if` with || or &&" +Sometimes, people use the AND `&&` operator as a "shorter to write `if`". For instance: @@ -253,14 +241,12 @@ So we basically have an analogue for: ```js run let x = 1; -if (x > 0) { - alert( 'Greater than zero!' ); -} +if (x > 0) alert( 'Greater than zero!' ); ``` -The variant with `&&` appears shorter. But `if` is more obvious and tends to be a little bit more readable. +Although, the variant with `&&` appears shorter, `if` is more obvious and tends to be a little bit more readable. So we recommend using every construct for its purpose: use `if` if we want if and use `&&` if we want AND. +```` -So we recommend using every construct for its purpose: use `if` if we want if and use `&&` if we want AND. ## ! (NOT) diff --git a/1-js/02-first-steps/12-nullish-coalescing-operator/article.md b/1-js/02-first-steps/12-nullish-coalescing-operator/article.md new file mode 100644 index 000000000..f0f6687a3 --- /dev/null +++ b/1-js/02-first-steps/12-nullish-coalescing-operator/article.md @@ -0,0 +1,130 @@ +# Nullish coalescing operator '??' + +[recent browser="new"] + +The nullish coalescing operator `??` provides a short syntax for selecting a first "defined" variable from the list. + +The result of `a ?? b` is: +- `a` if it's not `null` or `undefined`, +- `b`, otherwise. + +So, `x = a ?? b` is a short equivalent to: + +```js +x = (a !== null && a !== undefined) ? a : b; +``` + +Here's a longer example. + +Imagine, we have a user, and there are variables `firstName`, `lastName` or `nickName` for their first name, last name and the nick name. All of them may be undefined, if the user decided not to enter any value. + +We'd like to display the user name: one of these three variables, or show "Anonymous" if nothing is set. + +Let's use the `??` operator to select the first defined one: + +```js run +let firstName = null; +let lastName = null; +let nickName = "Supercoder"; + +// show the first not-null/undefined value +*!* +alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder +*/!* +``` + +## Comparison with || + +The OR `||` operator can be used in the same way as `??`. Actually, we can replace `??` with `||` in the code above and get the same result, as it was described in the [previous chapter](info:logical-operators#or-finds-the-first-truthy-value). + +The important difference is that: +- `||` returns the first *truthy* value. +- `??` returns the first *defined* value. + +This matters a lot when we'd like to treat `null/undefined` differently from `0`. + +For example, consider this: + +```js +height = height ?? 100; +``` + +This sets `height` to `100` if it's not defined. + +Let's compare it with `||`: + +```js run +let height = 0; + +alert(height || 100); // 100 +alert(height ?? 100); // 0 +``` + +Here, `height || 100` treats zero height as unset, same as `null`, `undefined` or any other falsy value. So the result is `100`. + +The `height ?? 100` returns `100` only if `height` is exactly `null` or `undefined`. So the `alert` shows the height value `0` "as is". + +Which behavior is better depends on a particular use case. When zero height is a valid value, then `??` is preferrable. + +## Precedence + +The precedence of the `??` operator is rather low: `7` in the [MDN table](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table). + +So `??` is evaluated after most other operations, but before `=` and `?`. + +If we need to choose a value with `??` in a complex expression, then consider adding parentheses: + +```js run +let height = null; +let width = null; + +// important: use parentheses +let area = (height ?? 100) * (width ?? 50); + +alert(area); // 5000 +``` + +Otherwise, if we omit parentheses, `*` has the higher precedence than `??` and would run first. + +That would work be the same as: + +```js +// probably not correct +let area = height ?? (100 * width) ?? 50; +``` + +There's also a related language-level limitation. + +**Due to safety reasons, it's forbidden to use `??` together with `&&` and `||` operators.** + +The code below triggers a syntax error: + +```js run +let x = 1 && 2 ?? 3; // Syntax error +``` + +The limitation is surely debatable, but it was added to the language specification with the purpose to avoid programming mistakes, as people start to switch to `??` from `||`. + +Use explicit parentheses to work around it: + +```js run +*!* +let x = (1 && 2) ?? 3; // Works +*/!* + +alert(x); // 2 +``` + +## Summary + +- The nullish coalescing operator `??` provides a short way to choose a "defined" value from the list. + + It's used to assign default values to variables: + + ```js + // set height=100, if height is null or undefined + height = height ?? 100; + ``` + +- The operator `??` has a very low precedence, a bit higher than `?` and `=`. +- It's forbidden to use it with `||` or `&&` without explicit parentheses. diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/solution.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/solution.md diff --git a/1-js/02-first-steps/12-while-for/1-loop-last-value/task.md b/1-js/02-first-steps/13-while-for/1-loop-last-value/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/1-loop-last-value/task.md rename to 1-js/02-first-steps/13-while-for/1-loop-last-value/task.md diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/solution.md b/1-js/02-first-steps/13-while-for/2-which-value-while/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/2-which-value-while/solution.md rename to 1-js/02-first-steps/13-while-for/2-which-value-while/solution.md diff --git a/1-js/02-first-steps/12-while-for/2-which-value-while/task.md b/1-js/02-first-steps/13-while-for/2-which-value-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/2-which-value-while/task.md rename to 1-js/02-first-steps/13-while-for/2-which-value-while/task.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/solution.md b/1-js/02-first-steps/13-while-for/3-which-value-for/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/solution.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/solution.md diff --git a/1-js/02-first-steps/12-while-for/3-which-value-for/task.md b/1-js/02-first-steps/13-while-for/3-which-value-for/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/3-which-value-for/task.md rename to 1-js/02-first-steps/13-while-for/3-which-value-for/task.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/solution.md b/1-js/02-first-steps/13-while-for/4-for-even/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/solution.md rename to 1-js/02-first-steps/13-while-for/4-for-even/solution.md diff --git a/1-js/02-first-steps/12-while-for/4-for-even/task.md b/1-js/02-first-steps/13-while-for/4-for-even/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/4-for-even/task.md rename to 1-js/02-first-steps/13-while-for/4-for-even/task.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/solution.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/solution.md diff --git a/1-js/02-first-steps/12-while-for/5-replace-for-while/task.md b/1-js/02-first-steps/13-while-for/5-replace-for-while/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/5-replace-for-while/task.md rename to 1-js/02-first-steps/13-while-for/5-replace-for-while/task.md diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/6-repeat-until-correct/solution.md rename to 1-js/02-first-steps/13-while-for/6-repeat-until-correct/solution.md diff --git a/1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md b/1-js/02-first-steps/13-while-for/6-repeat-until-correct/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/6-repeat-until-correct/task.md rename to 1-js/02-first-steps/13-while-for/6-repeat-until-correct/task.md diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/solution.md b/1-js/02-first-steps/13-while-for/7-list-primes/solution.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/7-list-primes/solution.md rename to 1-js/02-first-steps/13-while-for/7-list-primes/solution.md diff --git a/1-js/02-first-steps/12-while-for/7-list-primes/task.md b/1-js/02-first-steps/13-while-for/7-list-primes/task.md similarity index 100% rename from 1-js/02-first-steps/12-while-for/7-list-primes/task.md rename to 1-js/02-first-steps/13-while-for/7-list-primes/task.md diff --git a/1-js/02-first-steps/12-while-for/article.md b/1-js/02-first-steps/13-while-for/article.md similarity index 98% rename from 1-js/02-first-steps/12-while-for/article.md rename to 1-js/02-first-steps/13-while-for/article.md index 382adadac..b3e3953b8 100644 --- a/1-js/02-first-steps/12-while-for/article.md +++ b/1-js/02-first-steps/13-while-for/article.md @@ -256,7 +256,7 @@ For even values of `i`, the `continue` directive stops executing the body and pa ````smart header="The `continue` directive helps decrease nesting" A loop that shows odd values could look like this: -```js +```js run for (let i = 0; i < 10; i++) { if (i % 2) { @@ -268,7 +268,7 @@ for (let i = 0; i < 10; i++) { From a technical point of view, this is identical to the example above. Surely, we can just wrap the code in an `if` block instead of using `continue`. -But as a side-effect, this created one more level of nesting (the `alert` call inside the curly braces). If the code inside of`if` is longer than a few lines, that may decrease the overall readability. +But as a side-effect, this created one more level of nesting (the `alert` call inside the curly braces). If the code inside of `if` is longer than a few lines, that may decrease the overall readability. ```` ````warn header="No `break/continue` to the right side of '?'" diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/solution.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/solution.md diff --git a/1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md b/1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/1-rewrite-switch-if-else/task.md rename to 1-js/02-first-steps/14-switch/1-rewrite-switch-if-else/task.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/solution.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/solution.md diff --git a/1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md b/1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md similarity index 100% rename from 1-js/02-first-steps/13-switch/2-rewrite-if-switch/task.md rename to 1-js/02-first-steps/14-switch/2-rewrite-if-switch/task.md diff --git a/1-js/02-first-steps/13-switch/article.md b/1-js/02-first-steps/14-switch/article.md similarity index 99% rename from 1-js/02-first-steps/13-switch/article.md rename to 1-js/02-first-steps/14-switch/article.md index dec40a537..314c6cef8 100644 --- a/1-js/02-first-steps/13-switch/article.md +++ b/1-js/02-first-steps/14-switch/article.md @@ -117,7 +117,7 @@ Several variants of `case` which share the same code can be grouped. For example, if we want the same code to run for `case 3` and `case 5`: ```js run no-beautify -let a = 2 + 2; +let a = 3; switch (a) { case 4: diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md b/1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/1-if-else-required/solution.md rename to 1-js/02-first-steps/15-function-basics/1-if-else-required/solution.md diff --git a/1-js/02-first-steps/14-function-basics/1-if-else-required/task.md b/1-js/02-first-steps/15-function-basics/1-if-else-required/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/1-if-else-required/task.md rename to 1-js/02-first-steps/15-function-basics/1-if-else-required/task.md diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/solution.md rename to 1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/solution.md diff --git a/1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md b/1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/2-rewrite-function-question-or/task.md rename to 1-js/02-first-steps/15-function-basics/2-rewrite-function-question-or/task.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/solution.md b/1-js/02-first-steps/15-function-basics/3-min/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/solution.md rename to 1-js/02-first-steps/15-function-basics/3-min/solution.md diff --git a/1-js/02-first-steps/14-function-basics/3-min/task.md b/1-js/02-first-steps/15-function-basics/3-min/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/3-min/task.md rename to 1-js/02-first-steps/15-function-basics/3-min/task.md diff --git a/1-js/02-first-steps/14-function-basics/4-pow/solution.md b/1-js/02-first-steps/15-function-basics/4-pow/solution.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/4-pow/solution.md rename to 1-js/02-first-steps/15-function-basics/4-pow/solution.md diff --git a/1-js/02-first-steps/14-function-basics/4-pow/task.md b/1-js/02-first-steps/15-function-basics/4-pow/task.md similarity index 100% rename from 1-js/02-first-steps/14-function-basics/4-pow/task.md rename to 1-js/02-first-steps/15-function-basics/4-pow/task.md diff --git a/1-js/02-first-steps/14-function-basics/article.md b/1-js/02-first-steps/15-function-basics/article.md similarity index 93% rename from 1-js/02-first-steps/14-function-basics/article.md rename to 1-js/02-first-steps/15-function-basics/article.md index b1881e311..b56fbc67d 100644 --- a/1-js/02-first-steps/14-function-basics/article.md +++ b/1-js/02-first-steps/15-function-basics/article.md @@ -214,36 +214,48 @@ In JavaScript, a default parameter is evaluated every time the function is calle 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. +### Alternative default parameters -For instance, an explicit check for being `undefined`: +Sometimes it makes sense to set default values for parameters not in the function declaration, but at a later stage, during its execution. -```js -function showMessage(from, text) { +To check for an omitted parameter, we can compare it with `undefined`: + +```js run +function showMessage(text) { *!* if (text === undefined) { - text = 'no text given'; + text = 'empty message'; } */!* - alert( from + ": " + text ); + alert(text); } + +showMessage(); // empty message ``` -...Or the `||` operator: +...Or we could use the `||` operator: ```js -function showMessage(from, text) { - // if text is falsy then text gets the "default" value - text = text || 'no text given'; +// if text parameter is omitted or "" is passed, set it to 'empty' +function showMessage(text) { + text = text || 'empty'; ... } ``` +Modern JavaScript engines support the [nullish coalescing operator](info:nullish-coalescing-operator) `??`, it's better when falsy values, such as `0`, are considered regular: -```` +```js run +// if there's no "count" parameter, show "unknown" +function showCount(count) { + alert(count ?? "unknown"); +} +showCount(0); // 0 +showCount(null); // unknown +showCount(); // unknown +``` ## Returning a value @@ -266,7 +278,7 @@ There may be many occurrences of `return` in a single function. For instance: ```js run function checkAge(age) { - if (age > 18) { + if (age >= 18) { *!* return true; */!* diff --git a/1-js/02-first-steps/15-function-expressions/article.md b/1-js/02-first-steps/16-function-expressions/article.md similarity index 100% rename from 1-js/02-first-steps/15-function-expressions/article.md rename to 1-js/02-first-steps/16-function-expressions/article.md diff --git a/1-js/02-first-steps/16-arrow-functions-basics/1-rewrite-arrow/solution.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md similarity index 100% rename from 1-js/02-first-steps/16-arrow-functions-basics/1-rewrite-arrow/solution.md rename to 1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/solution.md diff --git a/1-js/02-first-steps/16-arrow-functions-basics/1-rewrite-arrow/task.md b/1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md similarity index 100% rename from 1-js/02-first-steps/16-arrow-functions-basics/1-rewrite-arrow/task.md rename to 1-js/02-first-steps/17-arrow-functions-basics/1-rewrite-arrow/task.md diff --git a/1-js/02-first-steps/16-arrow-functions-basics/article.md b/1-js/02-first-steps/17-arrow-functions-basics/article.md similarity index 99% rename from 1-js/02-first-steps/16-arrow-functions-basics/article.md rename to 1-js/02-first-steps/17-arrow-functions-basics/article.md index 02090f3c1..e0fb5bda5 100644 --- a/1-js/02-first-steps/16-arrow-functions-basics/article.md +++ b/1-js/02-first-steps/17-arrow-functions-basics/article.md @@ -67,7 +67,7 @@ let welcome = (age < 18) ? () => alert('Hello') : () => alert("Greetings!"); -welcome(); // ok now +welcome(); ``` Arrow functions may appear unfamiliar and not very readable at first, but that quickly changes as the eyes get used to the structure. diff --git a/1-js/02-first-steps/17-javascript-specials/article.md b/1-js/02-first-steps/18-javascript-specials/article.md similarity index 96% rename from 1-js/02-first-steps/17-javascript-specials/article.md rename to 1-js/02-first-steps/18-javascript-specials/article.md index cfc043d7d..91be0aa45 100644 --- a/1-js/02-first-steps/17-javascript-specials/article.md +++ b/1-js/02-first-steps/18-javascript-specials/article.md @@ -81,9 +81,10 @@ let x = 5; x = "John"; ``` -There are 7 data types: +There are 8 data types: - `number` for both floating-point and integer numbers, +- `bigint` for integer numbers of arbitrary length, - `string` for strings, - `boolean` for logical values: `true/false`, - `null` -- a type with a single value `null`, meaning "empty" or "does not exist", @@ -151,6 +152,9 @@ Conditional Logical operators : Logical AND `&&` and OR `||` perform short-circuit evaluation and then return the value where it stopped (not necessary `true`/`false`). Logical NOT `!` converts the operand to boolean type and returns the inverse value. +Nullish coalescing operator +: The `??` operator provides a way to choose a defined value from a list of variables. The result of `a ?? b` is `a` unless it's `null/undefined`, then `b`. + Comparisons : Equality check `==` for values of different types converts them to a number (except `null` and `undefined` that equal each other and nothing else), so these are equal: @@ -170,7 +174,7 @@ Comparisons Other operators : There are few others, like a comma operator. -More in: , , . +More in: , , , . ## Loops @@ -212,6 +216,7 @@ let age = prompt('Your age?', 18); switch (age) { case 18: alert("Won't work"); // the result of prompt is a string, not a number + break; case "18": alert("This works!"); 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 1b0f4e377..ee7dea4c4 100644 --- a/1-js/03-code-quality/01-debugging-chrome/article.md +++ b/1-js/03-code-quality/01-debugging-chrome/article.md @@ -4,7 +4,7 @@ Before writing more complex code, let's talk about debugging. [Debugging](https://en.wikipedia.org/wiki/Debugging) is the process of finding and fixing errors within a script. All modern browsers and most other environments support debugging tools -- a special UI in developer tools that makes debugging much easier. It also allows to trace the code step by step to see what exactly is going on. -We'll be using Chrome here, because it has enough features, most other browsers have a similar process`. +We'll be using Chrome here, because it has enough features, most other browsers have a similar process. ## The "Sources" panel @@ -24,11 +24,11 @@ Let's click it and select `hello.js` in the tree view. Here's what should show u ![](chrome-tabs.svg) -Here we can see three zones: +The Sources panel has 3 parts: -1. The **Resources zone** lists HTML, JavaScript, CSS and other files, including images that are attached to the page. Chrome extensions may appear here too. -2. The **Source zone** shows the source code. -3. The **Information and control zone** is for debugging, we'll explore it soon. +1. The **File Navigator** pane lists HTML, JavaScript, CSS and other files, including images that are attached to the page. Chrome extensions may appear here too. +2. The **Code Editor** pane shows the source code. +3. The **JavaScript Debugging** pane is for debugging, we'll explore it soon. Now you could click the same toggler again to hide the resources list and give the code some space. diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg index 6f729fd3a..1f7d21288 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-open-sources.svg @@ -1 +1 @@ -open sources \ No newline at end of file +open sources \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg index e9b28655f..6fb4332f1 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-breakpoint.svg @@ -1 +1 @@ -here's the listbreakpoints \ No newline at end of file +here's the listbreakpoints \ No newline at end of file diff --git a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg index 61f08d25c..0d5bde9c4 100644 --- a/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg +++ b/1-js/03-code-quality/01-debugging-chrome/chrome-sources-debugger-trace-1.svg @@ -1 +1 @@ -nested calls \ No newline at end of file +nested calls \ No newline at end of file diff --git a/1-js/03-code-quality/02-coding-style/article.md b/1-js/03-code-quality/02-coding-style/article.md index bdcfec545..4cdb23808 100644 --- a/1-js/03-code-quality/02-coding-style/article.md +++ b/1-js/03-code-quality/02-coding-style/article.md @@ -86,7 +86,7 @@ For example: ```js // backtick quotes ` allow to split the string into multiple lines let str = ` - Ecma International's TC39 is a group of JavaScript developers, + ECMA International's TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript. `; @@ -285,7 +285,7 @@ Of course, a team can always write their own style guide, but usually there's no Some popular choices: -- [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml) +- [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) - [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) - [Idiomatic.JS](https://github.com/rwaldron/idiomatic.js) - [StandardJS](https://standardjs.com/) diff --git a/1-js/03-code-quality/02-coding-style/code-style.svg b/1-js/03-code-quality/02-coding-style/code-style.svg index e8e374048..bd62691c6 100644 --- a/1-js/03-code-quality/02-coding-style/code-style.svg +++ b/1-js/03-code-quality/02-coding-style/code-style.svg @@ -1 +1 @@ -2No space between the function name and parentheses between the parentheses and the parameterIndentation 2 spacesA space after for/if/while…} else { without a line breakSpaces around a nested callAn empty line between logical blocksLines are not very longA semicolon ; is mandatorySpaces around operatorsCurly brace { on the same line, after a spaceA space between parametersA space between parameters \ No newline at end of file +2No space between the function name and parentheses between the parentheses and the parameterIndentation 2 spacesA space after for/if/while…} else { without a line breakSpaces around a nested callAn empty line between logical blocksLines are not very longA semicolon ; is mandatorySpaces around operatorsCurly brace { on the same line, after a spaceA space between parametersA space between parameters \ No newline at end of file diff --git a/1-js/03-code-quality/03-comments/article.md b/1-js/03-code-quality/03-comments/article.md index 29ba701f8..0d11c6c52 100644 --- a/1-js/03-code-quality/03-comments/article.md +++ b/1-js/03-code-quality/03-comments/article.md @@ -125,25 +125,25 @@ Describe the architecture Document function parameters and usage : There's a special syntax [JSDoc](http://en.wikipedia.org/wiki/JSDoc) to document a function: usage, parameters, returned value. - For instance: - ```js - /** - * Returns x raised to the n-th power. - * - * @param {number} x The number to raise. - * @param {number} n The power, must be a natural number. - * @return {number} x raised to the n-th power. - */ - function pow(x, n) { - ... - } - ``` +For instance: +```js +/** + * Returns x raised to the n-th power. + * + * @param {number} x The number to raise. + * @param {number} n The power, must be a natural number. + * @return {number} x raised to the n-th power. + */ +function pow(x, n) { + ... +} +``` - Such comments allow us to understand the purpose of the function and use it the right way without looking in its code. +Such comments allow us to understand the purpose of the function and use it the right way without looking in its code. - By the way, many editors like [WebStorm](https://www.jetbrains.com/webstorm/) can understand them as well and use them to provide autocomplete and some automatic code-checking. +By the way, many editors like [WebStorm](https://www.jetbrains.com/webstorm/) can understand them as well and use them to provide autocomplete and some automatic code-checking. - Also, there are tools like [JSDoc 3](https://github.com/jsdoc3/jsdoc) that can generate HTML-documentation from the comments. You can read more information about JSDoc at . +Also, there are tools like [JSDoc 3](https://github.com/jsdoc3/jsdoc) that can generate HTML-documentation from the comments. You can read more information about JSDoc at . Why is the task solved this way? : What's written is important. But what's *not* written may be even more important to understand what's going on. Why is the task solved exactly this way? The code gives no answer. diff --git a/1-js/03-code-quality/04-ninja-code/article.md b/1-js/03-code-quality/04-ninja-code/article.md index 7846f6e21..982cc0214 100644 --- a/1-js/03-code-quality/04-ninja-code/article.md +++ b/1-js/03-code-quality/04-ninja-code/article.md @@ -43,7 +43,7 @@ The Dao hides in wordlessness. Only the Dao is well begun and well completed. ``` -Another way to code faster is to use single-letter variable names everywhere. Like `a`, `b` or `c`. +Another way to code shorter is to use single-letter variable names everywhere. Like `a`, `b` or `c`. A short variable disappears in the code like a real ninja in the forest. No one will be able to find it using "search" of the editor. And even if someone does, they won't be able to "decipher" what the name `a` or `b` means. 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 e9b5e96c1..68ffcae4d 100644 --- a/1-js/03-code-quality/05-testing-mocha/article.md +++ b/1-js/03-code-quality/05-testing-mocha/article.md @@ -159,8 +159,8 @@ We can select one of two ways to organize the test here: assert.equal(pow(2, 3), 8); }); - it("3 raised to power 3 is 27", function() { - assert.equal(pow(3, 3), 27); + it("3 raised to power 4 is 81", function() { + assert.equal(pow(3, 4), 81); }); }); @@ -182,7 +182,7 @@ The result: [iframe height=250 src="pow-2" edit border="1"] -As we could expect, the second test failed. Sure, our function always returns `8`, while the `assert` expects `27`. +As we could expect, the second test failed. Sure, our function always returns `8`, while the `assert` expects `81`. ## Improving the implementation diff --git a/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js b/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js index cad51d3ee..d3de82546 100644 --- a/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/beforeafter.view/test.js @@ -1,5 +1,11 @@ describe("test", function() { + + // Mocha usually waits for the tests for 2 seconds before considering them wrong + + this.timeout(200000); // With this code we increase this - in this case to 200,000 milliseconds + // This is because of the "alert" function, because if you delay pressing the "OK" button the tests will not pass! + before(() => alert("Testing started – before all tests")); after(() => alert("Testing finished – after all tests")); diff --git a/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js b/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js index 9a2f8fde7..c803f0e61 100644 --- a/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js +++ b/1-js/03-code-quality/05-testing-mocha/pow-2.view/test.js @@ -4,8 +4,8 @@ describe("pow", function() { assert.equal(pow(2, 3), 8); }); - it("3 raised to power 3 is 27", function() { - assert.equal(pow(3, 3), 27); + it("3 raised to power 4 is 81", function() { + assert.equal(pow(3, 4), 81); }); }); diff --git a/1-js/03-code-quality/06-polyfills/article.md b/1-js/03-code-quality/06-polyfills/article.md index b399fd428..75db49d2f 100644 --- a/1-js/03-code-quality/06-polyfills/article.md +++ b/1-js/03-code-quality/06-polyfills/article.md @@ -19,7 +19,7 @@ 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 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. +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 it's very easy to integrate into development process. 2. Second, the polyfill. diff --git a/1-js/04-object-basics/01-object/4-const-object/solution.md b/1-js/04-object-basics/01-object/4-const-object/solution.md deleted file mode 100644 index f73c2f92b..000000000 --- a/1-js/04-object-basics/01-object/4-const-object/solution.md +++ /dev/null @@ -1,19 +0,0 @@ -Sure, it works, no problem. - -The `const` only protects the variable itself from changing. - -In other words, `user` stores a reference to the object. And it can't be changed. But the content of the object can. - -```js run -const user = { - name: "John" -}; - -*!* -// works -user.name = "Pete"; -*/!* - -// error -user = 123; -``` diff --git a/1-js/04-object-basics/01-object/4-const-object/task.md b/1-js/04-object-basics/01-object/4-const-object/task.md deleted file mode 100644 index a9aada631..000000000 --- a/1-js/04-object-basics/01-object/4-const-object/task.md +++ /dev/null @@ -1,18 +0,0 @@ -importance: 5 - ---- - -# Constant objects? - -Is it possible to change an object declared with `const`? What do you think? - -```js -const user = { - name: "John" -}; - -*!* -// does it work? -user.name = "Pete"; -*/!* -``` diff --git a/1-js/04-object-basics/01-object/article.md b/1-js/04-object-basics/01-object/article.md index ea015e7ca..513f2f3e3 100644 --- a/1-js/04-object-basics/01-object/article.md +++ b/1-js/04-object-basics/01-object/article.md @@ -1,7 +1,7 @@ # Objects -As we know from the chapter , there are seven data types in JavaScript. Six of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever). +As we know from the chapter , there are eight data types in JavaScript. Seven of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store keyed collections of various data and more complex entities. In JavaScript, objects penetrate almost every aspect of the language. So we must understand them first before going in-depth anywhere else. @@ -92,6 +92,30 @@ let user = { ``` That is called a "trailing" or "hanging" comma. Makes it easier to add/remove/move around properties, because all lines become alike. +````smart header="Object with const can be changed" +Please note: an object declared as `const` *can* be modified. + +For instance: + +```js run +const user = { + name: "John" +}; + +*!* +user.name = "Pete"; // (*) +*/!* + +alert(user.name); // Pete +``` + +It might seem that the line `(*)` would cause an error, but no. The `const` fixes the value of `user`, but not its contents. + +The `const` would give an error only if we try to set `user=...` as a whole. + +There's another way to make constant object properties, we'll cover it later in the chapter . +```` + ## Square brackets For multiword properties, the dot access doesn't work: @@ -101,7 +125,9 @@ For multiword properties, the dot access doesn't work: user.likes birds = true ``` -That's because the dot requires the key to be a valid variable identifier. That is: no spaces and other limitations. +JavaScript doesn't understand that. It thinks that we address `user.likes`, and then gives a syntax error when comes across unexpected `birds`. + +The dot requires the key to be a valid variable identifier. That implies: contains no spaces, doesn't start with a digit and doesn't include special characters (`$` and `_` are allowed). There's an alternative "square bracket notation" that works with any string: @@ -159,7 +185,7 @@ alert( user.key ) // undefined ### Computed properties -We can use square brackets in an object literal. That's called *computed properties*. +We can use square brackets in an object literal, when creating an object. That's called *computed properties*. For instance: @@ -203,43 +229,6 @@ Square brackets are much more powerful than the dot notation. They allow any pro So most of the time, when property names are known and simple, the dot is used. And if we need something more complex, then we switch to square brackets. - - -````smart header="Reserved words are allowed as property names" -A variable cannot have a name equal to one of language-reserved words like "for", "let", "return" etc. - -But for an object property, there's no such restriction. Any name is fine: - -```js run -let obj = { - for: 1, - let: 2, - return: 3 -}; - -alert( obj.for + obj.let + obj.return ); // 6 -``` - -Basically, any name is allowed, but there's a special one: `"__proto__"` that gets special treatment for historical reasons. For instance, we can't set it to a non-object value: - -```js run -let obj = {}; -obj.__proto__ = 5; -alert(obj.__proto__); // [object Object], didn't work as intended -``` - -As we see from the code, the assignment to a primitive `5` is ignored. - -That can become a source of bugs and even vulnerabilities if we intend to store arbitrary key-value pairs in an object, and allow a visitor to specify the keys. - -In that case the visitor may choose `__proto__` as the key, and the assignment logic will be ruined (as shown above). - -There is a way to make objects treat `__proto__` as a regular property, which we'll cover later, but first we need to know more about objects. - -There's also another data structure [Map](info:map-set), that we'll learn in the chapter , which supports arbitrary keys. -```` - - ## Property value shorthand In real code we often use existing variables as values for property names. @@ -250,7 +239,7 @@ For instance: function makeUser(name, age) { return { name: name, - age: age + age: age, // ...other properties }; } @@ -268,7 +257,7 @@ function makeUser(name, age) { *!* return { name, // same as name: name - age // same as age: age + age, // same as age: age // ... }; */!* @@ -284,9 +273,57 @@ let user = { }; ``` -## Existence check -A notable objects feature is that it's possible to access any property. There will be no error if the property doesn't exist! Accessing a non-existing property just returns `undefined`. It provides a very common way to test whether the property exists -- to get it and compare vs undefined: +## Property names limitations + +As we already know, a variable cannot have a name equal to one of language-reserved words like "for", "let", "return" etc. + +But for an object property, there's no such restriction: + +```js run +// these properties are all right +let obj = { + for: 1, + let: 2, + return: 3 +}; + +alert( obj.for + obj.let + obj.return ); // 6 +``` + +In short, there are no limitations on property names. They can be any strings or symbols (a special type for identifiers, to be covered later). + +Other types are automatically converted to strings. + +For instance, a number `0` becomes a string `"0"` when used as a property key: + +```js run +let obj = { + 0: "test" // same as "0": "test" +}; + +// both alerts access the same property (the number 0 is converted to string "0") +alert( obj["0"] ); // test +alert( obj[0] ); // test (same property) +``` + +There's a minor gotcha with a special property named `__proto__`. We can't set it to a non-object value: + +```js run +let obj = {}; +obj.__proto__ = 5; // assign a number +alert(obj.__proto__); // [object Object] - the value is an object, didn't work as intended +``` + +As we see from the code, the assignment to a primitive `5` is ignored. + +We'll cover the special nature of `__proto__` in [subsequent chapters](info:prototype-inheritance), and suggest the [ways to fix](info:prototype-methods) such behavior. + +## Property existence test, "in" operator + +A notable feature of objects in JavaScript, compared to many other languages, is that it's possible to access any property. There will be no error if the property doesn't exist! + +Reading a non-existing property just returns `undefined`. So we can easily test whether the property exists: ```js run let user = {}; @@ -294,7 +331,7 @@ let user = {}; alert( user.noSuchProperty === undefined ); // true means "no such property" ``` -There also exists a special operator `"in"` to check for the existence of a property. +There's also a special operator `"in"` for that. The syntax is: ```js @@ -312,17 +349,18 @@ alert( "blabla" in user ); // false, user.blabla doesn't exist Please note that on the left side of `in` there must be a *property name*. That's usually a quoted string. -If we omit quotes, that would mean a variable containing the actual name will be tested. For instance: +If we omit quotes, that means a variable, it should contain the actual name to be tested. For instance: ```js run let user = { age: 30 }; let key = "age"; -alert( *!*key*/!* in user ); // true, takes the name from key and checks for such property +alert( *!*key*/!* in user ); // true, property "age" exists ``` -````smart header="Using \"in\" for properties that store `undefined`" -Usually, the strict comparison `"=== undefined"` check the property existance just fine. But there's a special case when it fails, but `"in"` works correctly. +Why does the `in` operator exist? Isn't it enough to compare against `undefined`? + +Well, most of the time the comparison with `undefined` works fine. But there's a special case when it fails, but `"in"` works correctly. It's when an object property exists, but stores `undefined`: @@ -336,11 +374,10 @@ alert( obj.test ); // it's undefined, so - no such property? alert( "test" in obj ); // true, the property does exist! ``` - In the code above, the property `obj.test` technically exists. So the `in` operator works right. -Situations like this happen very rarely, because `undefined` is usually not assigned. We mostly use `null` for "unknown" or "empty" values. So the `in` operator is an exotic guest in the code. -```` +Situations like this happen very rarely, because `undefined` should not be explicitly assigned. We mostly use `null` for "unknown" or "empty" values. So the `in` operator is an exotic guest in the code. + ## The "for..in" loop @@ -375,7 +412,6 @@ Note that all "for" constructs allow us to declare the looping variable inside t Also, we could use another variable name here instead of `key`. For instance, `"for (let prop in obj)"` is also widely used. - ### Ordered like an object Are objects ordered? In other words, if we loop over an object, do we get all properties in the same order they were added? Can we rely on this? @@ -459,262 +495,6 @@ for (let code in codes) { Now it works as intended. -## Copying by reference - -One of the fundamental differences of objects vs primitives is that they are stored and copied "by reference". - -Primitive values: strings, numbers, booleans -- are assigned/copied "as a whole value". - -For instance: - -```js -let message = "Hello!"; -let phrase = message; -``` - -As a result we have two independent variables, each one is storing the string `"Hello!"`. - -![](variable-copy-value.svg) - -Objects are not like that. - -**A variable stores not the object itself, but its "address in memory", in other words "a reference" to it.** - -Here's the picture for the object: - -```js -let user = { - name: "John" -}; -``` - -![](variable-contains-reference.svg) - -Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it. - -**When an object variable is copied -- the reference is copied, the object is not duplicated.** - -If we imagine an object as a cabinet, then a variable is a key to it. Copying a variable duplicates the key, but not the cabinet itself. - -For instance: - -```js no-beautify -let user = { name: "John" }; - -let admin = user; // copy the reference -``` - -Now we have two variables, each one with the reference to the same object: - -![](variable-copy-reference.svg) - -We can use any variable to access the cabinet and modify its contents: - -```js run -let user = { name: 'John' }; - -let admin = user; - -*!* -admin.name = 'Pete'; // changed by the "admin" reference -*/!* - -alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference -``` - -The example above demonstrates that there is only one object. As if we had a cabinet with two keys and used one of them (`admin`) to get into it. Then, if we later use the other key (`user`) we would see changes. - -### Comparison by reference - -The equality `==` and strict equality `===` operators for objects work exactly the same. - -**Two objects are equal only if they are the same object.** - -For instance, if two variables reference the same object, they are equal: - -```js run -let a = {}; -let b = a; // copy the reference - -alert( a == b ); // true, both variables reference the same object -alert( a === b ); // true -``` - -And here two independent objects are not equal, even though both are empty: - -```js run -let a = {}; -let b = {}; // two independent objects - -alert( a == b ); // false -``` - -For comparisons like `obj1 > obj2` or for a comparison against a primitive `obj == 5`, objects are converted to primitives. We'll study how object conversions work very soon, but to tell the truth, such comparisons are necessary very rarely and usually are a result of a coding mistake. - -### Const object - -An object declared as `const` *can* be changed. - -For instance: - -```js run -const user = { - name: "John" -}; - -*!* -user.age = 25; // (*) -*/!* - -alert(user.age); // 25 -``` - -It might seem that the line `(*)` would cause an error, but no, there's totally no problem. That's because `const` fixes only value of `user` itself. And here `user` stores the reference to the same object all the time. The line `(*)` goes *inside* the object, it doesn't reassign `user`. - -The `const` would give an error if we try to set `user` to something else, for instance: - -```js run -const user = { - name: "John" -}; - -*!* -// Error (can't reassign user) -*/!* -user = { - name: "Pete" -}; -``` - -...But what if we want to make constant object properties? So that `user.age = 25` would give an error. That's possible too. We'll cover it in the chapter . - -## Cloning and merging, Object.assign - -So, copying an object variable creates one more reference to the same object. - -But what if we need to duplicate an object? Create an independent copy, a clone? - -That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. Actually, that's rarely needed. Copying by reference is good most of the time. - -But if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level. - -Like this: - -```js run -let user = { - name: "John", - age: 30 -}; - -*!* -let clone = {}; // the new empty object - -// let's copy all user properties into it -for (let key in user) { - clone[key] = user[key]; -} -*/!* - -// now clone is a fully independent clone -clone.name = "Pete"; // changed the data in it - -alert( user.name ); // still John in the original object -``` - -Also we can use the method [Object.assign](mdn:js/Object/assign) for that. - -The syntax is: - -```js -Object.assign(dest, [src1, src2, src3...]) -``` - -- Arguments `dest`, and `src1, ..., srcN` (can be as many as needed) are objects. -- It copies the properties of all objects `src1, ..., srcN` into `dest`. In other words, properties of all arguments starting from the 2nd are copied into the 1st. Then it returns `dest`. - -For instance, we can use it to merge several objects into one: -```js -let user = { name: "John" }; - -let permissions1 = { canView: true }; -let permissions2 = { canEdit: true }; - -*!* -// copies all properties from permissions1 and permissions2 into user -Object.assign(user, permissions1, permissions2); -*/!* - -// now user = { name: "John", canView: true, canEdit: true } -``` - -If the receiving object (`user`) already has the same named property, it will be overwritten: - -```js -let user = { name: "John" }; - -// overwrite name, add isAdmin -Object.assign(user, { name: "Pete", isAdmin: true }); - -// now user = { name: "Pete", isAdmin: true } -``` - -We also can use `Object.assign` to replace the loop for simple cloning: - -```js -let user = { - name: "John", - age: 30 -}; - -*!* -let clone = Object.assign({}, user); -*/!* -``` - -It copies all properties of `user` into the empty object and returns it. Actually, the same as the loop, but shorter. - -Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. What to do with them? - -Like this: -```js run -let user = { - name: "John", - sizes: { - height: 182, - width: 50 - } -}; - -alert( user.sizes.height ); // 182 -``` - -Now it's not enough to copy `clone.sizes = user.sizes`, because the `user.sizes` is an object, it will be copied by reference. So `clone` and `user` will share the same sizes: - -Like this: -```js run -let user = { - name: "John", - sizes: { - height: 182, - width: 50 - } -}; - -let clone = Object.assign({}, user); - -alert( user.sizes === clone.sizes ); // true, same object - -// user and clone share sizes -user.sizes.width++; // change a property from one place -alert(clone.sizes.width); // 51, see the result from the other one -``` - -To fix that, we should use the cloning loop that examines each value of `user[key]` and, if it's an object, then replicate its structure as well. That is called a "deep cloning". - -There's a standard algorithm for deep cloning that handles the case above and more complex cases, called the [Structured cloning algorithm](https://html.spec.whatwg.org/multipage/structured-data.html#safe-passing-of-structured-data). In order not to reinvent the wheel, we can use a working implementation of it from the JavaScript library [lodash](https://lodash.com), the method is called [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). - - - ## Summary Objects are associative arrays with several special features. @@ -732,10 +512,6 @@ Additional operators: - To check if a property with the given key exists: `"key" in obj`. - To iterate over an object: `for (let key in obj)` loop. -Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object. All operations via copied references (like adding/removing properties) are performed on the same single object. - -To make a "real copy" (a clone) we can use `Object.assign` or [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). - What we've studied in this chapter is called a "plain object", or just `Object`. There are many other kinds of objects in JavaScript: diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg deleted file mode 100644 index 5cb54dc28..000000000 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-5.svg +++ /dev/null @@ -1 +0,0 @@ -<global>unreachables \ No newline at end of file diff --git a/1-js/04-object-basics/02-object-copy/article.md b/1-js/04-object-basics/02-object-copy/article.md new file mode 100644 index 000000000..c88872232 --- /dev/null +++ b/1-js/04-object-basics/02-object-copy/article.md @@ -0,0 +1,228 @@ +# Object copying, references + +One of the fundamental differences of objects vs primitives is that they are stored and copied "by reference". + +Primitive values: strings, numbers, booleans -- are assigned/copied "as a whole value". + +For instance: + +```js +let message = "Hello!"; +let phrase = message; +``` + +As a result we have two independent variables, each one is storing the string `"Hello!"`. + +![](variable-copy-value.svg) + +Objects are not like that. + +**A variable stores not the object itself, but its "address in memory", in other words "a reference" to it.** + +Here's the picture for the object: + +```js +let user = { + name: "John" +}; +``` + +![](variable-contains-reference.svg) + +Here, the object is stored somewhere in memory. And the variable `user` has a "reference" to it. + +**When an object variable is copied -- the reference is copied, the object is not duplicated.** + +For instance: + +```js no-beautify +let user = { name: "John" }; + +let admin = user; // copy the reference +``` + +Now we have two variables, each one with the reference to the same object: + +![](variable-copy-reference.svg) + +We can use any variable to access the object and modify its contents: + +```js run +let user = { name: 'John' }; + +let admin = user; + +*!* +admin.name = 'Pete'; // changed by the "admin" reference +*/!* + +alert(*!*user.name*/!*); // 'Pete', changes are seen from the "user" reference +``` + +The example above demonstrates that there is only one object. As if we had a cabinet with two keys and used one of them (`admin`) to get into it. Then, if we later use another key (`user`) we can see changes. + +## Comparison by reference + +The equality `==` and strict equality `===` operators for objects work exactly the same. + +**Two objects are equal only if they are the same object.** + +Here two variables reference the same object, thus they are equal: + +```js run +let a = {}; +let b = a; // copy the reference + +alert( a == b ); // true, both variables reference the same object +alert( a === b ); // true +``` + +And here two independent objects are not equal, even though both are empty: + +```js run +let a = {}; +let b = {}; // two independent objects + +alert( a == b ); // false +``` + +For comparisons like `obj1 > obj2` or for a comparison against a primitive `obj == 5`, objects are converted to primitives. We'll study how object conversions work very soon, but to tell the truth, such comparisons occur very rarely, usually as a result of a coding mistake. + +## Cloning and merging, Object.assign + +So, copying an object variable creates one more reference to the same object. + +But what if we need to duplicate an object? Create an independent copy, a clone? + +That's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. Actually, that's rarely needed. Copying by reference is good most of the time. + +But if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level. + +Like this: + +```js run +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = {}; // the new empty object + +// let's copy all user properties into it +for (let key in user) { + clone[key] = user[key]; +} +*/!* + +// now clone is a fully independent object with the same content +clone.name = "Pete"; // changed the data in it + +alert( user.name ); // still John in the original object +``` + +Also we can use the method [Object.assign](mdn:js/Object/assign) for that. + +The syntax is: + +```js +Object.assign(dest, [src1, src2, src3...]) +``` + +- The first argument `dest` is a target object. +- Further arguments `src1, ..., srcN` (can be as many as needed) are source objects. +- It copies the properties of all source objects `src1, ..., srcN` into the target `dest`. In other words, properties of all arguments starting from the second are copied into the first object. +- The call returns `dest`. + +For instance, we can use it to merge several objects into one: +```js +let user = { name: "John" }; + +let permissions1 = { canView: true }; +let permissions2 = { canEdit: true }; + +*!* +// copies all properties from permissions1 and permissions2 into user +Object.assign(user, permissions1, permissions2); +*/!* + +// now user = { name: "John", canView: true, canEdit: true } +``` + +If the copied property name already exists, it gets overwritten: + +```js run +let user = { name: "John" }; + +Object.assign(user, { name: "Pete" }); + +alert(user.name); // now user = { name: "Pete" } +``` + +We also can use `Object.assign` to replace `for..in` loop for simple cloning: + +```js +let user = { + name: "John", + age: 30 +}; + +*!* +let clone = Object.assign({}, user); +*/!* +``` + +It copies all properties of `user` into the empty object and returns it. + +## Nested cloning + +Until now we assumed that all properties of `user` are primitive. But properties can be references to other objects. What to do with them? + +Like this: +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +alert( user.sizes.height ); // 182 +``` + +Now it's not enough to copy `clone.sizes = user.sizes`, because the `user.sizes` is an object, it will be copied by reference. So `clone` and `user` will share the same sizes: + +Like this: + +```js run +let user = { + name: "John", + sizes: { + height: 182, + width: 50 + } +}; + +let clone = Object.assign({}, user); + +alert( user.sizes === clone.sizes ); // true, same object + +// user and clone share sizes +user.sizes.width++; // change a property from one place +alert(clone.sizes.width); // 51, see the result from the other one +``` + +To fix that, we should use the cloning loop that examines each value of `user[key]` and, if it's an object, then replicate its structure as well. That is called a "deep cloning". + +There's a standard algorithm for deep cloning that handles the case above and more complex cases, called the [Structured cloning algorithm](https://html.spec.whatwg.org/multipage/structured-data.html#safe-passing-of-structured-data). + +We can use recursion to implement it. Or, not to reinvent the wheel, take an existing implementation, for instance [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep) from the JavaScript library [lodash](https://lodash.com). + +## Summary + +Objects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object. + +All operations via copied references (like adding/removing properties) are performed on the same single object. + +To make a "real copy" (a clone) we can use `Object.assign` for the so-called "shallow copy" (nested objects are copied by reference) or a "deep cloning" function, such as [_.cloneDeep(obj)](https://lodash.com/docs#cloneDeep). diff --git a/1-js/04-object-basics/01-object/variable-contains-reference.svg b/1-js/04-object-basics/02-object-copy/variable-contains-reference.svg similarity index 100% rename from 1-js/04-object-basics/01-object/variable-contains-reference.svg rename to 1-js/04-object-basics/02-object-copy/variable-contains-reference.svg diff --git a/1-js/04-object-basics/01-object/variable-copy-reference.svg b/1-js/04-object-basics/02-object-copy/variable-copy-reference.svg similarity index 100% rename from 1-js/04-object-basics/01-object/variable-copy-reference.svg rename to 1-js/04-object-basics/02-object-copy/variable-copy-reference.svg diff --git a/1-js/04-object-basics/01-object/variable-copy-value.svg b/1-js/04-object-basics/02-object-copy/variable-copy-value.svg similarity index 100% rename from 1-js/04-object-basics/01-object/variable-copy-value.svg rename to 1-js/04-object-basics/02-object-copy/variable-copy-value.svg diff --git a/1-js/04-object-basics/02-garbage-collection/article.md b/1-js/04-object-basics/03-garbage-collection/article.md similarity index 100% rename from 1-js/04-object-basics/02-garbage-collection/article.md rename to 1-js/04-object-basics/03-garbage-collection/article.md diff --git a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg b/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg similarity index 71% rename from 1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg rename to 1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg index 133b3908d..2ae1f664c 100644 --- a/1-js/04-object-basics/02-garbage-collection/family-delete-refs.svg +++ b/1-js/04-object-basics/03-garbage-collection/family-delete-refs.svg @@ -1 +1 @@ -<global variable>ObjectObjectwifefamilyname: "John"name: "Ann"motherObjectfatherhusband \ No newline at end of file +<global variable>ObjectObjectwifefamilyname: "John"name: "Ann"motherObjectfatherhusband \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg similarity index 57% rename from 1-js/04-object-basics/02-garbage-collection/family-no-family.svg rename to 1-js/04-object-basics/03-garbage-collection/family-no-family.svg index 8ba3b9f9f..655d1982e 100644 --- a/1-js/04-object-basics/02-garbage-collection/family-no-family.svg +++ b/1-js/04-object-basics/03-garbage-collection/family-no-family.svg @@ -1 +1 @@ -<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file +<global>ObjectObjectfatherwifename: "John"name: "Ann"motherObjecthusbandfamily: null \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg similarity index 100% rename from 1-js/04-object-basics/02-garbage-collection/family-no-father-2.svg rename to 1-js/04-object-basics/03-garbage-collection/family-no-father-2.svg diff --git a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg similarity index 59% rename from 1-js/04-object-basics/02-garbage-collection/family-no-father.svg rename to 1-js/04-object-basics/03-garbage-collection/family-no-father.svg index 59f0b2afc..b76c868e0 100644 --- a/1-js/04-object-basics/02-garbage-collection/family-no-father.svg +++ b/1-js/04-object-basics/03-garbage-collection/family-no-father.svg @@ -1 +1 @@ -ObjectObjectwifefamilyname: "John"name: "Ann"motherObject<global> \ No newline at end of file +ObjectObjectwifefamilyname: "John"name: "Ann"motherObject<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/family.svg b/1-js/04-object-basics/03-garbage-collection/family.svg similarity index 75% rename from 1-js/04-object-basics/02-garbage-collection/family.svg rename to 1-js/04-object-basics/03-garbage-collection/family.svg index 8fbbc35ea..bec2f4ddc 100644 --- a/1-js/04-object-basics/02-garbage-collection/family.svg +++ b/1-js/04-object-basics/03-garbage-collection/family.svg @@ -1 +1 @@ -ObjectObjectfatherwifefamilyname: "John"name: "Ann"motherObjecthusband<global variable> \ No newline at end of file +ObjectObjectfatherwifefamilyname: "John"name: "Ann"motherObjecthusband<global variable> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg similarity index 74% rename from 1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg rename to 1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg index 125031ee4..2563c8185 100644 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-1.svg +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-1.svg @@ -1 +1 @@ -<global> \ No newline at end of file +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg similarity index 61% rename from 1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg rename to 1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg index b4f1a0870..acd5025e9 100644 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-2.svg +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-2.svg @@ -1 +1 @@ -<global> \ No newline at end of file +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg similarity index 54% rename from 1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg rename to 1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg index 7477c4ba7..4421ec784 100644 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-3.svg +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-3.svg @@ -1 +1 @@ -<global> \ No newline at end of file +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg similarity index 52% rename from 1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg rename to 1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg index 6b13c1dc0..74adc8135 100644 --- a/1-js/04-object-basics/02-garbage-collection/garbage-collection-4.svg +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-4.svg @@ -1 +1 @@ -<global> \ No newline at end of file +<global> \ No newline at end of file diff --git a/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg b/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg new file mode 100644 index 000000000..abb127ab2 --- /dev/null +++ b/1-js/04-object-basics/03-garbage-collection/garbage-collection-5.svg @@ -0,0 +1 @@ +<global>unreachables \ No newline at end of file diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg similarity index 100% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john-admin.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john-admin.svg diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg similarity index 100% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john-lost.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john-lost.svg diff --git a/1-js/04-object-basics/02-garbage-collection/memory-user-john.svg b/1-js/04-object-basics/03-garbage-collection/memory-user-john.svg similarity index 100% rename from 1-js/04-object-basics/02-garbage-collection/memory-user-john.svg rename to 1-js/04-object-basics/03-garbage-collection/memory-user-john.svg 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 ea00c970d..c1aaf4f97 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 @@ -22,6 +22,17 @@ The value of `this` is one for the whole function, code blocks and object litera So `ref: this` actually takes current `this` of the function. +We can rewrite the function and return the same `this` with `undefined` value: + +```js run +function makeUser(){ + return this; // this time there's no object literal +} + +alert( makeUser().name ); // Error: Cannot read property 'name' of undefined +``` +As you can see the result of `alert( makeUser().name )` is the same as the result of `alert( user.ref.name )` from the previous example. + Here's the opposite case: ```js run 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 2dda938d7..75bd1856a 100644 --- a/1-js/04-object-basics/04-object-methods/article.md +++ b/1-js/04-object-basics/04-object-methods/article.md @@ -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 [object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming), in short: "OOP". -OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E.Gamma, R.Helm, R.Johnson, J.Vissides or "Object-Oriented Analysis and Design with Applications" by G.Booch, and more. +OOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E. Gamma, R. Helm, R. Johnson, J. Vissides or "Object-Oriented Analysis and Design with Applications" by G. Booch, and more. ``` ### Method shorthand @@ -233,98 +233,6 @@ The concept of run-time evaluated `this` has both pluses and minuses. On the one Here our position is not to judge whether this language design decision is good or bad. We'll understand how to work with it, how to get benefits and avoid problems. ``` -## Internals: Reference Type - -```warn header="In-depth language feature" -This section covers an advanced topic, to understand certain edge-cases better. - -If you want to go on faster, it can be skipped or postponed. -``` - -An intricate method call can lose `this`, for instance: - -```js run -let user = { - name: "John", - hi() { alert(this.name); }, - bye() { alert("Bye"); } -}; - -user.hi(); // John (the simple call works) - -*!* -// now let's call user.hi or user.bye depending on the name -(user.name == "John" ? user.hi : user.bye)(); // Error! -*/!* -``` - -On the last line there is a conditional operator that chooses either `user.hi` or `user.bye`. In this case the result is `user.hi`. - -Then the method is immediately called with parentheses `()`. But it doesn't work correctly! - -As you can see, the call results in an error, because the value of `"this"` inside the call becomes `undefined`. - -This works (object dot method): -```js -user.hi(); -``` - -This doesn't (evaluated method): -```js -(user.name == "John" ? user.hi : user.bye)(); // Error! -``` - -Why? If we want to understand why it happens, let's get under the hood of how `obj.method()` call works. - -Looking closely, we may notice two operations in `obj.method()` statement: - -1. First, the dot `'.'` retrieves the property `obj.method`. -2. Then parentheses `()` execute it. - -So, how does the information about `this` get passed from the first part to the second one? - -If we put these operations on separate lines, then `this` will be lost for sure: - -```js run -let user = { - name: "John", - hi() { alert(this.name); } -} - -*!* -// split getting and calling the method in two lines -let hi = user.hi; -hi(); // Error, because this is undefined -*/!* -``` - -Here `hi = user.hi` puts the function into the variable, and then on the last line it is completely standalone, and so there's no `this`. - -**To make `user.hi()` calls work, JavaScript uses a trick -- the dot `'.'` returns not a function, but a value of the special [Reference Type](https://tc39.github.io/ecma262/#sec-reference-specification-type).** - -The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language. - -The value of Reference Type is a three-value combination `(base, name, strict)`, where: - -- `base` is the object. -- `name` is the property name. -- `strict` is true if `use strict` is in effect. - -The result of a property access `user.hi` is not a function, but a value of Reference Type. For `user.hi` in strict mode it is: - -```js -// Reference Type value -(user, "hi", true) -``` - -When parentheses `()` are called on the Reference Type, they receive the full information about the object and its method, and can set the right `this` (`=user` in this case). - -Reference type is a special "intermediary" internal type, with the purpose to pass information from dot `.` to calling parentheses `()`. - -Any other operation like assignment `hi = user.hi` discards the reference type as a whole, takes the value of `user.hi` (a function) and passes it on. So any further operation "loses" `this`. - -So, as the result, the value of `this` is only passed the right way if the function is called directly using a dot `obj.method()` or square brackets `obj['method']()` syntax (they do the same here). Later in this tutorial, we will learn various ways to solve this problem such as [func.bind()](/bind#solution-2-bind). - ## Arrow functions have no "this" Arrow functions are special: they don't have their "own" `this`. If we reference `this` from such a function, it's taken from the outer "normal" function. diff --git a/1-js/04-object-basics/07-optional-chaining/article.md b/1-js/04-object-basics/07-optional-chaining/article.md new file mode 100644 index 000000000..89363e432 --- /dev/null +++ b/1-js/04-object-basics/07-optional-chaining/article.md @@ -0,0 +1,176 @@ + +# Optional chaining '?.' + +[recent browser="new"] + +The optional chaining `?.` is an error-proof way to access nested object properties, even if an intermediate property doesn't exist. + +## The problem + +If you've just started to read the tutorial and learn JavaScript, maybe the problem hasn't touched you yet, but it's quite common. + +For example, some of our users have addresses, but few did not provide them. Then we can't safely read `user.address.street`: + +```js run +let user = {}; // the user happens to be without address + +alert(user.address.street); // Error! +``` + +Or, in the web development, we'd like to get an information about an element on the page, but it may not exist: + +```js run +// Error if the result of querySelector(...) is null +let html = document.querySelector('.my-element').innerHTML; +``` + +Before `?.` appeared in the language, the `&&` operator was used to work around that. + +For example: + +```js run +let user = {}; // user has no address + +alert( user && user.address && user.address.street ); // undefined (no error) +``` + +AND'ing the whole path to the property ensures that all components exist, but is cumbersome to write. + +## Optional chaining + +The optional chaining `?.` stops the evaluation and returns `undefined` if the part before `?.` is `undefined` or `null`. + +Further in this article, for brevity, we'll be saying that something "exists" if it's not `null` and not `undefined`. + + +Here's the safe way to access `user.address.street`: + +```js run +let user = {}; // user has no address + +alert( user?.address?.street ); // undefined (no error) +``` + +Reading the address with `user?.address` works even if `user` object doesn't exist: + +```js run +let user = null; + +alert( user?.address ); // undefined + +alert( user?.address.street ); // undefined +alert( user?.address.street.anything ); // undefined +``` + +Please note: the `?.` syntax works exactly where it's placed, not any further. + +In the last two lines the evaluation stops immediately after `user?.`, never accessing further properties. But if the `user` actually exists, then the further intermediate properties, such as `user.address` must exist. + +```warn header="Don't overuse the optional chaining" +We should use `?.` only where it's ok that something doesn't exist. + +For example, if according to our coding logic `user` object must be there, but `address` is optional, then `user.address?.street` would be better. + +So, if `user` happens to be undefined due to a mistake, we'll know about it and fix it. Otherwise, coding errors can be silenced where not appropriate, and become more difficult to debug. +``` + +````warn header="The variable before `?.` must exist" +If there's no variable `user`, then `user?.anything` triggers an error: + +```js run +// ReferenceError: user is not defined +user?.address; +``` +The optional chaining only tests for `null/undefined`, doesn't interfere with any other language mechanics. +```` + +## Short-circuiting + +As it was said before, the `?.` immediately stops ("short-circuits") the evaluation if the left part doesn't exist. + +So, if there are any further function calls or side effects, they don't occur: + +```js run +let user = null; +let x = 0; + +user?.sayHi(x++); // nothing happens + +alert(x); // 0, value not incremented +``` + +## Other cases: ?.(), ?.[] + +The optional chaining `?.` is not an operator, but a special syntax construct, that also works with functions and square brackets. + +For example, `?.()` is used to call a function that may not exist. + +In the code below, some of our users have `admin` method, and some don't: + +```js run +let user1 = { + admin() { + alert("I am admin"); + } +} + +let user2 = {}; + +*!* +user1.admin?.(); // I am admin +user2.admin?.(); +*/!* +``` + +Here, in both lines we first use the dot `.` to get `admin` property, because the user object must exist, so it's safe read from it. + +Then `?.()` checks the left part: if the admin function exists, then it runs (for `user1`). Otherwise (for `user2`) the evaluation stops without errors. + +The `?.[]` syntax also works, if we'd like to use brackets `[]` to access properties instead of dot `.`. Similar to previous cases, it allows to safely read a property from an object that may not exist. + +```js run +let user1 = { + firstName: "John" +}; + +let user2 = null; // Imagine, we couldn't authorize the user + +let key = "firstName"; + +alert( user1?.[key] ); // John +alert( user2?.[key] ); // undefined + +alert( user1?.[key]?.something?.not?.existing); // undefined +``` + +Also we can use `?.` with `delete`: + +```js run +delete user?.name; // delete user.name if user exists +``` + +```warn header="We can use `?.` for safe reading and deleting, but not writing" +The optional chaining `?.` has no use at the left side of an assignment: + +```js run +// the idea of the code below is to write user.name, if user exists + +user?.name = "John"; // Error, doesn't work +// because it evaluates to undefined = "John" +``` + +## Summary + +The `?.` syntax has three forms: + +1. `obj?.prop` -- returns `obj.prop` if `obj` exists, otherwise `undefined`. +2. `obj?.[prop]` -- returns `obj[prop]` if `obj` exists, otherwise `undefined`. +3. `obj?.method()` -- calls `obj.method()` if `obj` exists, otherwise returns `undefined`. + +As we can see, all of them are straightforward and simple to use. The `?.` checks the left part for `null/undefined` and allows the evaluation to proceed if it's not so. + +A chain of `?.` allows to safely access nested properties. + +Still, we should apply `?.` carefully, only where it's ok that the left part doesn't to exist. + +So that it won't hide programming errors from us, if they occur. diff --git a/1-js/04-object-basics/03-symbol/article.md b/1-js/04-object-basics/08-symbol/article.md similarity index 95% rename from 1-js/04-object-basics/03-symbol/article.md rename to 1-js/04-object-basics/08-symbol/article.md index a17f85fe2..1ed73ed4b 100644 --- a/1-js/04-object-basics/03-symbol/article.md +++ b/1-js/04-object-basics/08-symbol/article.md @@ -18,7 +18,7 @@ let id = Symbol(); Upon creation, we can give symbol a description (also called a symbol name), mostly useful for debugging purposes: -```js run +```js // id is a symbol with the description "id" let id = Symbol("id"); ``` @@ -178,22 +178,6 @@ alert( clone[id] ); // 123 There's no paradox here. That's by design. The idea is that when we clone an object or merge objects, we usually want *all* properties to be copied (including symbols like `id`). -````smart header="Property keys of other types are coerced to strings" -We can only use strings or symbols as keys in objects. Other types are converted to strings. - -For instance, a number `0` becomes a string `"0"` when used as a property key: - -```js run -let obj = { - 0: "test" // same as "0": "test" -}; - -// both alerts access the same property (the number 0 is converted to string "0") -alert( obj["0"] ); // test -alert( obj[0] ); // test (same property) -``` -```` - ## Global symbols As we've seen, usually all symbols are different, even if they have the same name. But sometimes we want same-named symbols to be same entities. For instance, different parts of our application want to access symbol `"id"` meaning exactly the same property. @@ -241,7 +225,7 @@ alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id ``` -The `Symbol.keyFor` internally uses the global symbol registry to look up the key for the symbol. So it doesn't work for non-global symbols. If the symbol is not global, it won't be able to find it and return `undefined`. +The `Symbol.keyFor` internally uses the global symbol registry to look up the key for the symbol. So it doesn't work for non-global symbols. If the symbol is not global, it won't be able to find it and returns `undefined`. That said, any symbols have `description` property. diff --git a/1-js/04-object-basics/05-object-toprimitive/article.md b/1-js/04-object-basics/09-object-toprimitive/article.md similarity index 100% rename from 1-js/04-object-basics/05-object-toprimitive/article.md rename to 1-js/04-object-basics/09-object-toprimitive/article.md 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 06138a004..6c13acda6 100644 --- a/1-js/05-data-types/01-primitives-methods/article.md +++ b/1-js/05-data-types/01-primitives-methods/article.md @@ -7,7 +7,7 @@ Let's look at the key distinctions between primitives and objects. A primitive - Is a value of a primitive type. -- There are 6 primitive types: `string`, `number`, `boolean`, `symbol`, `null` and `undefined`. +- There are 7 primitive types: `string`, `number`, `bigint`, `boolean`, `symbol`, `null` and `undefined`. An object diff --git a/1-js/05-data-types/02-number/article.md b/1-js/05-data-types/02-number/article.md index f9ffdd81b..f54a0c147 100644 --- a/1-js/05-data-types/02-number/article.md +++ b/1-js/05-data-types/02-number/article.md @@ -1,8 +1,12 @@ # Numbers -All numbers in JavaScript are stored in 64-bit format [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754-2008_revision), also known as "double precision floating point numbers". +In modern JavaScript, there are two types of numbers: -Let's expand upon what we currently know about them. +1. Regular numbers in JavaScript are stored in 64-bit format [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754-2008_revision), also known as "double precision floating point numbers". These are numbers that we're using most of the time, and we'll talk about them in this chapter. + +2. BigInt numbers, to represent integers of arbitrary length. They are sometimes needed, because a regular number can't exceed 253 or be less than -253. As bigints are used in few special areas, we devote them a special chapter . + +So here we'll talk about regular numbers. Let's expand our knowledge of them. ## More ways to write a number @@ -29,14 +33,13 @@ In other words, `"e"` multiplies the number by `1` with the given zeroes count. 1.23e6 = 1.23 * 1000000 ``` - Now let's write something very small. Say, 1 microsecond (one millionth of a second): ```js let ms = 0.000001; ``` -Just like before, using `"e"` can help. If we'd like to avoid writing the zeroes explicitly, we could say: +Just like before, using `"e"` can help. If we'd like to avoid writing the zeroes explicitly, we could say the same as: ```js let ms = 1e-6; // six zeroes to the left from 1 @@ -271,13 +274,11 @@ JavaScript doesn't trigger an error in such events. It does its best to fit the ```smart header="Two zeroes" Another funny consequence of the internal representation of numbers is the existence of two zeroes: `0` and `-0`. -That's because a sign is represented by a single bit, so every number can be positive or negative, including a zero. +That's because a sign is represented by a single bit, so it can be set or not set for any number including a zero. In most cases the distinction is unnoticeable, because operators are suited to treat them as the same. ``` - - ## Tests: isFinite and isNaN Remember these two special numeric values? @@ -323,7 +324,7 @@ Please note that an empty or a space-only string is treated as `0` in all numeri ```smart header="Compare with `Object.is`" -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: +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`, technically that's true, because internally the number has a sign bit that may be different even if all other bits are zeroes. @@ -409,14 +410,14 @@ There are more functions and constants in `Math` object, including trigonometry, ## Summary -To write big numbers: +To write numbers with many zeroes: -- Append `"e"` with the zeroes count to the number. Like: `123e6` is `123` with 6 zeroes. -- A negative number after `"e"` causes the number to be divided by 1 with given zeroes. That's for one-millionth or such. +- Append `"e"` with the zeroes count to the number. Like: `123e6` is the same as `123` with 6 zeroes `123000000`. +- A negative number after `"e"` causes the number to be divided by 1 with given zeroes. E.g. `123e-6` means `0.000123` (`123` millionths). For different numeral systems: -- Can write numbers directly in hex (`0x`), octal (`0o`) and binary (`0b`) systems +- Can write numbers directly in hex (`0x`), octal (`0o`) and binary (`0b`) systems. - `parseInt(str, base)` parses the string `str` into an integer in numeral system with given `base`, `2 ≤ base ≤ 36`. - `num.toString(base)` converts a number to a string in the numeral system with the given `base`. diff --git a/1-js/05-data-types/03-string/article.md b/1-js/05-data-types/03-string/article.md index 8a2fe14f7..765823d7c 100644 --- a/1-js/05-data-types/03-string/article.md +++ b/1-js/05-data-types/03-string/article.md @@ -534,7 +534,7 @@ The "right" algorithm to do string comparisons is more complex than it may seem, So, the browser needs to know the language to compare. -Luckily, all modern browsers (IE10- requires the additional library [Intl.JS](https://github.com/andyearnshaw/Intl.js/)) support the internationalization standard [ECMA 402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf). +Luckily, all modern browsers (IE10- requires the additional library [Intl.js](https://github.com/andyearnshaw/Intl.js/)) support the internationalization standard [ECMA-402](http://www.ecma-international.org/ecma-402/1.0/ECMA-402.pdf). It provides a special method to compare strings in different languages, following their rules. diff --git a/1-js/05-data-types/04-array/10-maximal-subarray/task.md b/1-js/05-data-types/04-array/10-maximal-subarray/task.md index e63c4e625..f1a1d9f95 100644 --- a/1-js/05-data-types/04-array/10-maximal-subarray/task.md +++ b/1-js/05-data-types/04-array/10-maximal-subarray/task.md @@ -10,15 +10,15 @@ The task is: find the contiguous subarray of `arr` with the maximal sum of items Write the function `getMaxSubSum(arr)` that will return that sum. -For instance: +For instance: ```js -getMaxSubSum([-1, *!*2, 3*/!*, -9]) = 5 (the sum of highlighted items) -getMaxSubSum([*!*2, -1, 2, 3*/!*, -9]) = 6 -getMaxSubSum([-1, 2, 3, -9, *!*11*/!*]) = 11 -getMaxSubSum([-2, -1, *!*1, 2*/!*]) = 3 -getMaxSubSum([*!*100*/!*, -9, 2, -3, 5]) = 100 -getMaxSubSum([*!*1, 2, 3*/!*]) = 6 (take all) +getMaxSubSum([-1, *!*2, 3*/!*, -9]) == 5 (the sum of highlighted items) +getMaxSubSum([*!*2, -1, 2, 3*/!*, -9]) == 6 +getMaxSubSum([-1, 2, 3, -9, *!*11*/!*]) == 11 +getMaxSubSum([-2, -1, *!*1, 2*/!*]) == 3 +getMaxSubSum([*!*100*/!*, -9, 2, -3, 5]) == 100 +getMaxSubSum([*!*1, 2, 3*/!*]) == 6 (take all) ``` If all items are negative, it means that we take none (the subarray is empty), so the sum is zero: diff --git a/1-js/05-data-types/04-array/3-call-array-this/solution.md b/1-js/05-data-types/04-array/3-call-array-this/solution.md index e994ae078..3cb0317cf 100644 --- a/1-js/05-data-types/04-array/3-call-array-this/solution.md +++ b/1-js/05-data-types/04-array/3-call-array-this/solution.md @@ -9,7 +9,7 @@ arr.push(function() { alert( this ); }) -arr[2](); // "a","b",function +arr[2](); // a,b,function(){...} ``` The array has 3 values: initially it had two, plus the function. diff --git a/1-js/05-data-types/04-array/article.md b/1-js/05-data-types/04-array/article.md index 7dc54bd4b..33498f40a 100644 --- a/1-js/05-data-types/04-array/article.md +++ b/1-js/05-data-types/04-array/article.md @@ -123,7 +123,7 @@ For stacks, the latest pushed item is received first, that's also called LIFO (L Arrays in JavaScript can work both as a queue and as a stack. They allow you to add/remove elements both to/from the beginning or the end. -In computer science the data structure that allows it is called [deque](https://en.wikipedia.org/wiki/Double-ended_queue). +In computer science the data structure that allows this, is called [deque](https://en.wikipedia.org/wiki/Double-ended_queue). **Methods that work with the end of the array:** @@ -156,7 +156,7 @@ In computer science the data structure that allows it is called [deque](https:// `shift` : Extracts the first element of the array and returns it: - ```js + ```js run let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // remove Apple and alert it @@ -167,7 +167,7 @@ In computer science the data structure that allows it is called [deque](https:// `unshift` : Add the element to the beginning of the array: - ```js + ```js run let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js new file mode 100644 index 000000000..8dea23a06 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/solution.js @@ -0,0 +1,6 @@ +function groupById(array) { + return array.reduce((obj, value) => { + obj[value.id] = value; + return obj; + }, {}) +} diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js new file mode 100644 index 000000000..e48ba138d --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/_js.view/test.js @@ -0,0 +1,21 @@ +describe("groupById", function() { + + it("creates an object grouped by id", function() { + let users = [ + {id: 'john', name: "John Smith", age: 20}, + {id: 'ann', name: "Ann Smith", age: 24}, + {id: 'pete', name: "Pete Peterson", age: 31}, + ]; + + assert.deepEqual(groupById(users), { + john: {id: 'john', name: "John Smith", age: 20}, + ann: {id: 'ann', name: "Ann Smith", age: 24}, + pete: {id: 'pete', name: "Pete Peterson", age: 31}, + }); + }); + + it("works with an empty array", function() { + users = []; + assert.deepEqual(groupById(users), {}); + }); +}); diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md b/1-js/05-data-types/05-array-methods/12-reduce-object/solution.md new file mode 100644 index 000000000..e69de29bb diff --git a/1-js/05-data-types/05-array-methods/12-reduce-object/task.md b/1-js/05-data-types/05-array-methods/12-reduce-object/task.md new file mode 100644 index 000000000..d3c8f8eb1 --- /dev/null +++ b/1-js/05-data-types/05-array-methods/12-reduce-object/task.md @@ -0,0 +1,37 @@ +importance: 4 + +--- + +# Create keyed object from array + +Let's say we received an array of users in the form `{id:..., name:..., age... }`. + +Create a function `groupById(arr)` that creates an object from it, with `id` as the key, and array items as values. + +For example: + +```js +let users = [ + {id: 'john', name: "John Smith", age: 20}, + {id: 'ann', name: "Ann Smith", age: 24}, + {id: 'pete', name: "Pete Peterson", age: 31}, +]; + +let usersById = groupById(users); + +/* +// after the call we should have: + +usersById = { + john: {id: 'john', name: "John Smith", age: 20}, + ann: {id: 'ann', name: "Ann Smith", age: 24}, + pete: {id: 'pete', name: "Pete Peterson", age: 31}, +} +*/ +``` + +Such function is really handy when working with server data. + +In this task we assume that `id` is unique. There may be no two array items with the same `id`. + +Please use array `.reduce` method in the solution. diff --git a/1-js/05-data-types/05-array-methods/article.md b/1-js/05-data-types/05-array-methods/article.md index 5a1a7762a..301696440 100644 --- a/1-js/05-data-types/05-array-methods/article.md +++ b/1-js/05-data-types/05-array-methods/article.md @@ -384,7 +384,7 @@ The order became `1, 15, 2`. Incorrect. But why? **The items are sorted as strings by default.** -Literally, all elements are converted to strings for comparisons. For strings, lexicographic ordering is applied and indeed `"2" > "15"`. +Literally, all elements are converted to strings for comparisons. For strings, lexicographic ordering is applied and indeed `"2" > "15"`. To use our own sorting order, we need to supply a function as the argument of `arr.sort()`. @@ -431,7 +431,6 @@ By the way, if we ever want to know which elements are compared -- nothing preve The algorithm may compare an element with multiple others in the process, but it tries to make as few comparisons as possible. - ````smart header="A comparison function may return any number" Actually, a comparison function is only required to return a positive number to say "greater" and a negative number to say "less". @@ -456,6 +455,22 @@ arr.sort( (a, b) => a - b ); This works exactly the same as the longer version above. ```` +````smart header="Use `localeCompare` for strings" +Remember [strings](info:string#correct-comparisons) comparison algorithm? It compares letters by their codes by default. + +For many alphabets, it's better to use `str.localeCompare` method to correctly sort letters, such as `Ö`. + +For example, let's sort a few countries in German: + +```js run +let countries = ['Österreich', 'Andorra', 'Vietnam']; + +alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong) + +alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!) +``` +```` + ### reverse The method [arr.reverse](mdn:js/Array/reverse) reverses the order of elements in `arr`. @@ -530,7 +545,7 @@ The methods [arr.reduce](mdn:js/Array/reduce) and [arr.reduceRight](mdn:js/Array The syntax is: ```js -let value = arr.reduce(function(previousValue, item, index, array) { +let value = arr.reduce(function(accumulator, item, index, array) { // ... }, [initial]); ``` @@ -539,14 +554,16 @@ The function is applied to all array elements one after another and "carries on" Arguments: -- `previousValue` -- is the result of the previous function call, equals `initial` the first time (if `initial` is provided). +- `accumulator` -- is the result of the previous function call, equals `initial` the first time (if `initial` is provided). - `item` -- is the current array item. - `index` -- is its position. - `array` -- is the array. As function is applied, the result of the previous function call is passed to the next one as the first argument. -Sounds complicated, but it's not if you think about the first argument as the "accumulator" that stores the combined result of all previous execution. And at the end it becomes the result of `reduce`. +So, the first argument is essentially the accumulator that stores the combined result of all previous executions. And at the end it becomes the result of `reduce`. + +Sounds complicated? The easiest way to grasp that is by example. @@ -611,7 +628,6 @@ let arr = []; arr.reduce((sum, current) => sum + current); ``` - So it's advised to always specify the initial value. The method [arr.reduceRight](mdn:js/Array/reduceRight) does the same, but goes from right to left. diff --git a/1-js/05-data-types/06-iterable/article.md b/1-js/05-data-types/06-iterable/article.md index b55f8f018..8a38516e1 100644 --- a/1-js/05-data-types/06-iterable/article.md +++ b/1-js/05-data-types/06-iterable/article.md @@ -1,7 +1,7 @@ # Iterables -*Iterable* objects is a generalization of arrays. That's a concept that allows to make any object useable in a `for..of` loop. +*Iterable* objects is a generalization of arrays. That's a concept that allows us to make any object useable in a `for..of` loop. Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well. For instance, strings are also iterable. @@ -12,7 +12,7 @@ If an object isn't technically an array, but represents a collection (list, set) We can easily grasp the concept of iterables by making one of our own. -For instance, we have an object, that is not an array, but looks suitable for `for..of`. +For instance, we have an object that is not an array, but looks suitable for `for..of`. Like a `range` object that represents an interval of numbers: @@ -224,12 +224,12 @@ let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (array toString conversion works) ``` -The full syntax for `Array.from` allows to provide an optional "mapping" function: +The full syntax for `Array.from` also allows us to provide an optional "mapping" function: ```js Array.from(obj[, mapFn, thisArg]) ``` -The optional second argument `mapFn` can be a function that will be applied to each element before adding to the array, and `thisArg` allows to set `this` for it. +The optional second argument `mapFn` can be a function that will be applied to each element before adding it to the array, and `thisArg` allows us to set `this` for it. For instance: @@ -294,7 +294,7 @@ Objects that can be used in `for..of` are called *iterable*. - Technically, iterables must implement the method named `Symbol.iterator`. - The result of `obj[Symbol.iterator]` is called an *iterator*. It handles the further iteration process. - - An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the iteration end, otherwise the `value` is the next value. + - An iterator must have the method named `next()` that returns an object `{done: Boolean, value: any}`, here `done:true` denotes the end of the iteration process, otherwise the `value` is the next value. - The `Symbol.iterator` method is called automatically by `for..of`, but we also can do it directly. - Built-in iterables like strings or arrays, also implement `Symbol.iterator`. - String iterator knows about surrogate pairs. diff --git a/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md index 4c8af1f24..160675185 100644 --- a/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md +++ b/1-js/05-data-types/07-map-set/02-filter-anagrams/solution.md @@ -36,7 +36,7 @@ Letter-sorting is done by the chain of calls in the line `(*)`. For convenience let's split it into multiple lines: ```js -let sorted = arr[i] // PAN +let sorted = word // PAN .toLowerCase() // pan .split('') // ['p','a','n'] .sort() // ['a','n','p'] diff --git a/1-js/05-data-types/07-map-set/article.md b/1-js/05-data-types/07-map-set/article.md index c4d7c21a4..e08c84084 100644 --- a/1-js/05-data-types/07-map-set/article.md +++ b/1-js/05-data-types/07-map-set/article.md @@ -41,6 +41,12 @@ alert( map.size ); // 3 As we can see, unlike objects, keys are not converted to strings. Any type of key is possible. +```smart header="`map[key]` isn't the right way to use a `Map`" +Although `map[key]` also works, e.g. we can set `map[key] = 2`, this is treating `map` as a plain JavaScript object, so it implies all corresponding limitations (no object keys and so on). + +So we should use `map` methods: `set`, `get` and so on. +``` + **Map can also use objects as keys.** For instance: @@ -192,7 +198,7 @@ let prices = Object.fromEntries([ alert(prices.orange); // 2 ``` -We can use `Object.fromEntries` to get an plain object from `Map`. +We can use `Object.fromEntries` to get a plain object from `Map`. E.g. we store the data in a `Map`, but we need to pass it to a 3rd-party code that expects a plain object. @@ -214,7 +220,7 @@ let obj = Object.fromEntries(map.entries()); // make a plain object (*) alert(obj.orange); // 2 ``` -A call to `map.entries()` returns an array of key/value pairs, exactly in the right format for `Object.fromEntries`. +A call to `map.entries()` returns an iterable of key/value pairs, exactly in the right format for `Object.fromEntries`. We could also make line `(*)` shorter: ```js diff --git a/1-js/05-data-types/08-weakmap-weakset/article.md b/1-js/05-data-types/08-weakmap-weakset/article.md index 11ff9d5eb..bcc5e5e6f 100644 --- a/1-js/05-data-types/08-weakmap-weakset/article.md +++ b/1-js/05-data-types/08-weakmap-weakset/article.md @@ -141,7 +141,6 @@ And here's another part of the code, maybe another file using it: let john = { name: "John" }; countUser(john); // count his visits -countUser(john); // later john leaves us john = null; diff --git a/1-js/05-data-types/10-destructuring-assignment/article.md b/1-js/05-data-types/10-destructuring-assignment/article.md index 907c28cab..46aa760a9 100644 --- a/1-js/05-data-types/10-destructuring-assignment/article.md +++ b/1-js/05-data-types/10-destructuring-assignment/article.md @@ -121,6 +121,25 @@ for (let [key, value] of user) { } ``` ```` + +```smart header="Swap variables trick" +A well-known trick for swapping values of two variables: + +```js run +let guest = "Jane"; +let admin = "Pete"; + +// Swap values: make guest=Pete, admin=Jane +[guest, admin] = [admin, guest]; + +alert(`${guest} ${admin}`); // Pete Jane (successfully swapped!) +``` + +Here we create a temporary array of two variables and immediately destructure it in swapped order. + +We can swap more than two variables this way. + + ### The rest '...' If we want not just to get first values, but also to gather all that follows -- we can add one more parameter that gets "the rest" using three dots `"..."`: diff --git a/1-js/05-data-types/11-date/6-get-seconds-today/solution.md b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md index a483afe93..8f8e52b68 100644 --- a/1-js/05-data-types/11-date/6-get-seconds-today/solution.md +++ b/1-js/05-data-types/11-date/6-get-seconds-today/solution.md @@ -23,4 +23,6 @@ function getSecondsToday() { let d = new Date(); return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); } + +alert( getSecondsToday() ); ``` diff --git a/1-js/05-data-types/11-date/8-format-date-relative/solution.md b/1-js/05-data-types/11-date/8-format-date-relative/solution.md index 2507c840c..372485685 100644 --- a/1-js/05-data-types/11-date/8-format-date-relative/solution.md +++ b/1-js/05-data-types/11-date/8-format-date-relative/solution.md @@ -40,7 +40,7 @@ alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" -// yesterday's date like 31.12.2016, 20:00 +// yesterday's date like 31.12.2016 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); ``` @@ -62,6 +62,8 @@ function formatDate(date) { year = year.toString().slice(-2); month = month < 10 ? '0' + month : month; dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth; + hour = hour < 10 ? '0' + hour : hour; + minutes = minutes < 10 ? '0' + minutes : minutes; if (diffSec < 1) { return 'right now'; diff --git a/1-js/05-data-types/11-date/8-format-date-relative/task.md b/1-js/05-data-types/11-date/8-format-date-relative/task.md index 4dc067375..9651b305f 100644 --- a/1-js/05-data-types/11-date/8-format-date-relative/task.md +++ b/1-js/05-data-types/11-date/8-format-date-relative/task.md @@ -20,6 +20,6 @@ alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago" alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago" -// yesterday's date like 31.12.16, 20:00 +// yesterday's date like 31.12.16 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) ); ``` diff --git a/1-js/05-data-types/11-date/article.md b/1-js/05-data-types/11-date/article.md index 6f52a0d7c..a2de63ae4 100644 --- a/1-js/05-data-types/11-date/article.md +++ b/1-js/05-data-types/11-date/article.md @@ -124,7 +124,7 @@ Besides the given methods, there are two special ones that do not have a UTC-var : Returns the timestamp for the date -- a number of milliseconds passed from the January 1st of 1970 UTC+0. [getTimezoneOffset()](mdn:js/Date/getTimezoneOffset) -: Returns the difference between the local time zone and UTC, in minutes: +: Returns the difference between UTC and the local time zone, in minutes: ```js run // if you are in timezone UTC-1, outputs 60 diff --git a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg index 28f0c9b13..59e6a52c4 100644 --- a/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg +++ b/1-js/06-advanced-functions/01-recursion/03-fibonacci-numbers/fibonacci-recursion-tree.svg @@ -1 +1 @@ -fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file +fib ( 5 )fib(4)fib(3)fib(3)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1)fib(1)fib(2)fib(0)fib(1) \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/article.md b/1-js/06-advanced-functions/01-recursion/article.md index 688badb02..320de62f0 100644 --- a/1-js/06-advanced-functions/01-recursion/article.md +++ b/1-js/06-advanced-functions/01-recursion/article.md @@ -302,7 +302,7 @@ let company = { salary: 1000 }, { name: 'Alice', - salary: 600 + salary: 1600 }], development: { @@ -350,7 +350,7 @@ The algorithm is probably even easier to read from the code: ```js run let company = { // the same object, compressed for brevity - sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 600 }], + sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }], development: { sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }], internals: [{name: 'Jack', salary: 1300}] @@ -372,7 +372,7 @@ function sumSalaries(department) { } */!* -alert(sumSalaries(company)); // 6700 +alert(sumSalaries(company)); // 7700 ``` The code is short and easy to understand (hopefully?). That's the power of recursion. It also works for any level of subdepartment nesting. @@ -462,7 +462,7 @@ list.next.next.next = { value: 4 }; list.next.next.next.next = null; ``` -Here we can even more clearer see that there are multiple objects, each one has the `value` and `next` pointing to the neighbour. The `list` variable is the first object in the chain, so following `next` pointers from it we can reach any element. +Here we can even more clearly see that there are multiple objects, each one has the `value` and `next` pointing to the neighbour. The `list` variable is the first object in the chain, so following `next` pointers from it we can reach any element. The list can be easily split into multiple parts and later joined back: diff --git a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg index 346c436a3..edec23912 100644 --- a/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg +++ b/1-js/06-advanced-functions/01-recursion/linked-list-remove-1.svg @@ -1 +1 @@ -value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file +value"new item"nextvalue1nextvalue2nextvalue3nextvalue4nextnulllist \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg index 910b2aa50..8bd4a43fe 100644 --- a/1-js/06-advanced-functions/01-recursion/recursion-pow.svg +++ b/1-js/06-advanced-functions/01-recursion/recursion-pow.svg @@ -1 +1 @@ -pow(x,n)xx * pow(x, n-1)n == 1 ?YesNorecursive call until n==1 \ No newline at end of file +pow(x,n)xx * pow(x, n-1)n == 1 ?YesNorecursive call until n==1 \ No newline at end of file diff --git a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg index 9a3296655..f47f0668b 100644 --- a/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg +++ b/1-js/06-advanced-functions/01-recursion/recursive-salaries.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md b/1-js/06-advanced-functions/02-rest-parameters-spread/article.md similarity index 73% rename from 1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md rename to 1-js/06-advanced-functions/02-rest-parameters-spread/article.md index a14f0fb73..1f139d7a4 100644 --- a/1-js/06-advanced-functions/02-rest-parameters-spread-operator/article.md +++ b/1-js/06-advanced-functions/02-rest-parameters-spread/article.md @@ -1,4 +1,4 @@ -# Rest parameters and spread operator +# Rest parameters and spread syntax Many JavaScript built-in functions support an arbitrary number of arguments. @@ -122,7 +122,7 @@ As we remember, arrow functions don't have their own `this`. Now we know they do ```` -## Spread operator [#spread-operator] +## Spread syntax [#spread-syntax] We've just seen how to get an array from the list of parameters. @@ -148,7 +148,7 @@ alert( Math.max(arr) ); // NaN And surely we can't manually list items in the code `Math.max(arr[0], arr[1], arr[2])`, because we may be unsure how many there are. As our script executes, there could be a lot, or there could be none. And that would get ugly. -*Spread operator* to the rescue! It looks similar to rest parameters, also using `...`, but does quite the opposite. +*Spread syntax* to the rescue! It looks similar to rest parameters, also using `...`, but does quite the opposite. When `...arr` is used in the function call, it "expands" an iterable object `arr` into the list of arguments. @@ -169,7 +169,7 @@ let arr2 = [8, 3, -8, 1]; alert( Math.max(...arr1, ...arr2) ); // 8 ``` -We can even combine the spread operator with normal values: +We can even combine the spread syntax with normal values: ```js run @@ -179,7 +179,7 @@ let arr2 = [8, 3, -8, 1]; alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25 ``` -Also, the spread operator can be used to merge arrays: +Also, the spread syntax can be used to merge arrays: ```js run let arr = [3, 5, 1]; @@ -192,9 +192,9 @@ let merged = [0, ...arr, 2, ...arr2]; alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2) ``` -In the examples above we used an array to demonstrate the spread operator, but any iterable will do. +In the examples above we used an array to demonstrate the spread syntax, but any iterable will do. -For instance, here we use the spread operator to turn the string into array of characters: +For instance, here we use the spread syntax to turn the string into array of characters: ```js run let str = "Hello"; @@ -202,7 +202,7 @@ let str = "Hello"; alert( [...str] ); // H,e,l,l,o ``` -The spread operator internally uses iterators to gather elements, the same way as `for..of` does. +The spread syntax internally uses iterators to gather elements, the same way as `for..of` does. So, for a string, `for..of` returns characters and `...str` becomes `"H","e","l","l","o"`. The list of characters is passed to array initializer `[...str]`. @@ -220,24 +220,69 @@ The result is the same as `[...str]`. But there's a subtle difference between `Array.from(obj)` and `[...obj]`: - `Array.from` operates on both array-likes and iterables. -- The spread operator operates only on iterables. +- The spread syntax works only with iterables. So, for the task of turning something into an array, `Array.from` tends to be more universal. +## Get a new copy of an array/object + +Remember when we talked about `Object.assign()` [in the past](info:object-copy#cloning-and-merging-object-assign)? + +It is possible to do the same thing with the spread syntax. + +```js run +let arr = [1, 2, 3]; +let arrCopy = [...arr]; // spread the array into a list of parameters + // then put the result into a new array + +// do the arrays have the same contents? +alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true + +// are the arrays equal? +alert(arr === arrCopy); // false (not same reference) + +// modifying our initial array does not modify the copy: +arr.push(4); +alert(arr); // 1, 2, 3, 4 +alert(arrCopy); // 1, 2, 3 +``` + +Note that it is possible to do the same thing to make a copy of an object: + +```js run +let obj = { a: 1, b: 2, c: 3 }; +let objCopy = { ...obj }; // spread the object into a list of parameters + // then return the result in a new object + +// do the objects have the same contents? +alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true + +// are the objects equal? +alert(obj === objCopy); // false (not same reference) + +// modifying our initial object does not modify the copy: +obj.d = 4; +alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4} +alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3} +``` + +This way of copying an object is much shorter than `let objCopy = Object.assign({}, obj);` or for an array `let arrCopy = Object.assign([], arr);` so we prefer to use it whenever we can. + + ## Summary -When we see `"..."` in the code, it is either rest parameters or the spread operator. +When we see `"..."` in the code, it is either rest parameters or the spread syntax. There's an easy way to distinguish between them: - When `...` is at the end of function parameters, it's "rest parameters" and gathers the rest of the list of arguments into an array. -- When `...` occurs in a function call or alike, it's called a "spread operator" and expands an array into a list. +- When `...` occurs in a function call or alike, it's called a "spread syntax" and expands an array into a list. Use patterns: - Rest parameters are used to create functions that accept any number of arguments. -- The spread operator is used to pass an array to functions that normally require a list of many arguments. +- The spread syntax is used to pass an array to functions that normally require a list of many arguments. Together they help to travel between a list and an array of parameters with ease. diff --git a/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md new file mode 100644 index 000000000..7cbd85ab7 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/solution.md @@ -0,0 +1,5 @@ +The answer is: **Pete**. + +A function gets outer variables as they are now, it uses the most recent values. + +Old variable values are not saved anywhere. When a function wants a variable, it takes the current value from its own Lexical Environment or the outer one. diff --git a/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md new file mode 100644 index 000000000..819189773 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/1-closure-latest-changes/task.md @@ -0,0 +1,23 @@ +importance: 5 + +--- + +# Does a function pickup latest changes? + +The function sayHi uses an external variable name. When the function runs, which value is it going to use? + +```js +let name = "John"; + +function sayHi() { + alert("Hi, " + name); +} + +name = "Pete"; + +sayHi(); // what will it show: "John" or "Pete"? +``` + +Such situations are common both in browser and server-side development. A function may be scheduled to execute later than it is created, for instance after a user action or a network request. + +So, the question is: does it pick up the latest changes? diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/10-make-army/_js.view/solution.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/_js.view/solution.js rename to 1-js/06-advanced-functions/03-closure/10-make-army/_js.view/solution.js diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/source.js b/1-js/06-advanced-functions/03-closure/10-make-army/_js.view/source.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/_js.view/source.js rename to 1-js/06-advanced-functions/03-closure/10-make-army/_js.view/source.js diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/_js.view/test.js b/1-js/06-advanced-functions/03-closure/10-make-army/_js.view/test.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/_js.view/test.js rename to 1-js/06-advanced-functions/03-closure/10-make-army/_js.view/test.js diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg b/1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy.svg similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/lexenv-makearmy.svg rename to 1-js/06-advanced-functions/03-closure/10-make-army/lexenv-makearmy.svg diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/solution.md b/1-js/06-advanced-functions/03-closure/10-make-army/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/8-make-army/solution.md rename to 1-js/06-advanced-functions/03-closure/10-make-army/solution.md diff --git a/1-js/06-advanced-functions/03-closure/8-make-army/task.md b/1-js/06-advanced-functions/03-closure/10-make-army/task.md similarity index 86% rename from 1-js/06-advanced-functions/03-closure/8-make-army/task.md rename to 1-js/06-advanced-functions/03-closure/10-make-army/task.md index ede8fd045..93e64f2d0 100644 --- a/1-js/06-advanced-functions/03-closure/8-make-army/task.md +++ b/1-js/06-advanced-functions/03-closure/10-make-army/task.md @@ -31,5 +31,5 @@ army[5](); // and number 5 also outputs 10... // ... all shooters show 10 instead of their 0, 1, 2, 3... ``` -Why all shooters show the same? Fix the code so that they work as intended. +Why do all of the shooters show the same value? Fix the code so that they work as intended. diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/lexenv-nested-work.svg similarity index 99% rename from 1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg rename to 1-js/06-advanced-functions/03-closure/2-closure-variable-access/lexenv-nested-work.svg index e5b7f83e7..5cdf7f1a4 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-work.svg +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/lexenv-nested-work.svg @@ -1 +1 @@ -makeWorker: function name: "John"<empty>outerouterouternullname: "Pete" \ No newline at end of file +makeWorker: function name: "John"<empty>outerouterouternullname: "Pete" \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md new file mode 100644 index 000000000..0a522132f --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/solution.md @@ -0,0 +1,9 @@ +The answer is: **Pete**. + +The `work()` function in the code below gets `name` from the place of its origin through the outer lexical environment reference: + +![](lexenv-nested-work.svg) + +So, the result is `"Pete"` here. + +But if there were no `let name` in `makeWorker()`, then the search would go outside and take the global variable as we can see from the chain above. In that case the result would be `"John"`. diff --git a/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md new file mode 100644 index 000000000..d12a385c8 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/2-closure-variable-access/task.md @@ -0,0 +1,29 @@ +importance: 5 + +--- + +# Which variables are available? + +The function `makeWorker` below makes another function and returns it. That new function can be called from somewhere else. + +Will it have access to the outer variables from its creation place, or the invocation place, or both? + +```js +function makeWorker() { + let name = "Pete"; + + return function() { + alert(name); + }; +} + +let name = "John"; + +// create a function +let work = makeWorker(); + +// call it +work(); // what will it show? +``` + +Which value it will show? "Pete" or "John"? diff --git a/1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md b/1-js/06-advanced-functions/03-closure/3-counter-independent/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/1-counter-independent/solution.md rename to 1-js/06-advanced-functions/03-closure/3-counter-independent/solution.md diff --git a/1-js/06-advanced-functions/03-closure/1-counter-independent/task.md b/1-js/06-advanced-functions/03-closure/3-counter-independent/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/1-counter-independent/task.md rename to 1-js/06-advanced-functions/03-closure/3-counter-independent/task.md diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md b/1-js/06-advanced-functions/03-closure/4-counter-object-independent/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/2-counter-object-independent/solution.md rename to 1-js/06-advanced-functions/03-closure/4-counter-object-independent/solution.md diff --git a/1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md b/1-js/06-advanced-functions/03-closure/4-counter-object-independent/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/2-counter-object-independent/task.md rename to 1-js/06-advanced-functions/03-closure/4-counter-object-independent/task.md diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/3-function-in-if/solution.md rename to 1-js/06-advanced-functions/03-closure/5-function-in-if/solution.md diff --git a/1-js/06-advanced-functions/03-closure/3-function-in-if/task.md b/1-js/06-advanced-functions/03-closure/5-function-in-if/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/3-function-in-if/task.md rename to 1-js/06-advanced-functions/03-closure/5-function-in-if/task.md diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/4-closure-sum/solution.md rename to 1-js/06-advanced-functions/03-closure/6-closure-sum/solution.md diff --git a/1-js/06-advanced-functions/03-closure/4-closure-sum/task.md b/1-js/06-advanced-functions/03-closure/6-closure-sum/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/4-closure-sum/task.md rename to 1-js/06-advanced-functions/03-closure/6-closure-sum/task.md diff --git a/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md b/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md new file mode 100644 index 000000000..404bae80b --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-let-scope/solution.md @@ -0,0 +1,40 @@ +The result is: **error**. + +Try running it: + +```js run +let x = 1; + +function func() { +*!* + console.log(x); // ReferenceError: Cannot access 'x' before initialization +*/!* + let x = 2; +} + +func(); +``` + +In this example we can observe the peculiar difference between a "non-existing" and "unitialized" variable. + +As you may have read in the article [](info:closure), a variable starts in the "uninitialized" state from the moment when the execution enters a code block (or a function). And it stays uninitalized until the corresponding `let` statement. + +In other words, a variable technically exists, but can't be used before `let`. + +The code above demonstrates it. + +```js +function func() { +*!* + // the local variable x is known to the engine from the beginning of the function, + // but "unitialized" (unusable) until let ("dead zone") + // hence the error +*/!* + + console.log(x); // ReferenceError: Cannot access 'x' before initialization + + let x = 2; +} +``` + +This zone of temporary unusability of a variable (from the beginning of the code block till `let`) is sometimes called the "dead zone". diff --git a/1-js/06-advanced-functions/03-closure/7-let-scope/task.md b/1-js/06-advanced-functions/03-closure/7-let-scope/task.md new file mode 100644 index 000000000..fb7445e66 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/7-let-scope/task.md @@ -0,0 +1,21 @@ +importance: 4 + +--- + +# Is variable visible? + +What will be the result of this code? + +```js +let x = 1; + +function func() { + console.log(x); // ? + + let x = 2; +} + +func(); +``` + +P.S. There's a pitfall in this task. The solution is not obvious. diff --git a/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md b/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md deleted file mode 100644 index bd57085ea..000000000 --- a/1-js/06-advanced-functions/03-closure/7-sort-by-field/solution.md +++ /dev/null @@ -1,22 +0,0 @@ - - -```js run -let users = [ - { name: "John", age: 20, surname: "Johnson" }, - { name: "Pete", age: 18, surname: "Peterson" }, - { name: "Ann", age: 19, surname: "Hathaway" } -]; - -*!* -function byField(field) { - return (a, b) => a[field] > b[field] ? 1 : -1; -} -*/!* - -users.sort(byField('name')); -users.forEach(user => alert(user.name)); // Ann, John, Pete - -users.sort(byField('age')); -users.forEach(user => alert(user.name)); // Pete, Ann, John -``` - diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/solution.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/solution.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/solution.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/source.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/source.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/source.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js b/1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/test.js similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/_js.view/test.js rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/_js.view/test.js diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/solution.md rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/solution.md diff --git a/1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md b/1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/6-filter-through-function/task.md rename to 1-js/06-advanced-functions/03-closure/8-filter-through-function/task.md diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/solution.js b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/solution.js new file mode 100644 index 000000000..8a71c869d --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/solution.js @@ -0,0 +1,3 @@ +function byField(fieldName){ + return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1; +} diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/source.js b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/source.js new file mode 100644 index 000000000..23b433834 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/source.js @@ -0,0 +1,5 @@ +function byField(fieldName){ + + // Your code goes here. + +} diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/test.js b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/test.js new file mode 100644 index 000000000..e3c335e03 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/_js.view/test.js @@ -0,0 +1,39 @@ +describe("byField", function(){ + + let users = [ + { name: "John", age: 20, surname: "Johnson" }, + { name: "Pete", age: 18, surname: "Peterson" }, + { name: "Ann", age: 19, surname: "Hathaway" }, + ]; + + it("sorts users by name", function(){ + let nameSortedKey = [ + { name: "Ann", age: 19, surname: "Hathaway" }, + { name: "John", age: 20, surname: "Johnson"}, + { name: "Pete", age: 18, surname: "Peterson" }, + ]; + let nameSortedAnswer = users.sort(byField("name")); + assert.deepEqual(nameSortedKey, nameSortedAnswer); + }); + + it("sorts users by age", function(){ + let ageSortedKey = [ + { name: "Pete", age: 18, surname: "Peterson" }, + { name: "Ann", age: 19, surname: "Hathaway" }, + { name: "John", age: 20, surname: "Johnson"}, + ]; + let ageSortedAnswer = users.sort(byField("age")); + assert.deepEqual(ageSortedKey, ageSortedKey); + }); + + it("sorts users by surname", function(){ + let surnameSortedKey = [ + { name: "Ann", age: 19, surname: "Hathaway" }, + { name: "John", age: 20, surname: "Johnson"}, + { name: "Pete", age: 18, surname: "Peterson" }, + ]; + let surnameSortedAnswer = users.sort(byField("surname")); + assert.deepEqual(surnameSortedAnswer, surnameSortedKey); + }); + +}); diff --git a/1-js/06-advanced-functions/03-closure/9-sort-by-field/solution.md b/1-js/06-advanced-functions/03-closure/9-sort-by-field/solution.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/9-sort-by-field/solution.md @@ -0,0 +1 @@ + diff --git a/1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md b/1-js/06-advanced-functions/03-closure/9-sort-by-field/task.md similarity index 100% rename from 1-js/06-advanced-functions/03-closure/7-sort-by-field/task.md rename to 1-js/06-advanced-functions/03-closure/9-sort-by-field/task.md diff --git a/1-js/06-advanced-functions/03-closure/article.md b/1-js/06-advanced-functions/03-closure/article.md index 64104541c..1845482e5 100644 --- a/1-js/06-advanced-functions/03-closure/article.md +++ b/1-js/06-advanced-functions/03-closure/article.md @@ -1,203 +1,98 @@ -# Closure +# Variable scope -JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created dynamically, copied to another variable or passed as an argument to another function and called from a totally different place later. +JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created dynamically, passed as an argument to another function and called from a totally different place of code later. -We know that a function can access variables outside of it, this feature is used quite often. +We already know that a function can access variables outside of it. -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? +Now let's expand our knowledge to include more complex scenarios. -Also, what happens when a function travels to another place in the code and is called from there -- does it get access to the outer variables of the new place? +```smart header="We'll talk about `let/const` variables here" +In JavaScript, there are 3 ways to declare a variable: `let`, `const` (the modern ones), and `var` (the remnant of the past). -Different languages behave differently here, and in this chapter we cover the behaviour of JavaScript. - -## A couple of questions - -Let's consider two situations to begin with, and then study the internal mechanics piece-by-piece, so that you'll be able to answer the following questions and more complex ones in the future. - -1. The function `sayHi` uses an external variable `name`. When the function runs, which value is it going to use? - - ```js - let name = "John"; - - function sayHi() { - alert("Hi, " + name); - } - - name = "Pete"; - - *!* - sayHi(); // what will it show: "John" or "Pete"? - */!* - ``` - - Such situations are common both in browser and server-side development. A function may be scheduled to execute later than it is created, for instance after a user action or a network request. - - So, the question is: does it pick up the latest changes? - - -2. The function `makeWorker` makes another function and returns it. That new function can be called from somewhere else. Will it have access to the outer variables from its creation place, or the invocation place, or both? - - ```js - function makeWorker() { - let name = "Pete"; - - return function() { - alert(name); - }; - } - - let name = "John"; - - // create a function - let work = makeWorker(); - - // call it - *!* - work(); // what will it show? "Pete" (name where created) or "John" (name where called)? - */!* - ``` - - -## Lexical Environment - -To understand what's going on, let's first discuss what a "variable" actually is. - -In JavaScript, every running function, code block `{...}`, and the script as a whole have an internal (hidden) associated object known as the *Lexical Environment*. - -The Lexical Environment object consists of two parts: - -1. *Environment Record* -- an object that stores all local variables as its properties (and some other information like the value of `this`). -2. A reference to the *outer lexical environment*, the one associated with the outer code. - -**A "variable" is just a property of the special internal object, `Environment Record`. "To get or change a variable" means "to get or change a property of that object".** - -For instance, in this simple code, there is only one Lexical Environment: - -![lexical environment](lexical-environment-global.svg) - -This is a so-called global Lexical Environment, associated with the whole script. - -On the picture above, the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical Environment has no outer reference, so it points to `null`. - -And that's how it changes when a variable is defined and assigned: - -![lexical environment](lexical-environment-global-2.svg) - -Rectangles on the right-hand side demonstrate how the global Lexical Environment changes during the execution: - -1. When the script starts, the Lexical Environment is empty. -2. The `let phrase` definition appears. It has been assigned no value, so `undefined` is stored. -3. `phrase` is assigned a value. -4. `phrase` changes value. - -Everything looks simple for now, right? - -To summarize: - -- A variable is a property of a special internal object, associated with the currently executing block/function/script. -- Working with variables is actually working with the properties of that object. - -### Function Declaration - -Until now, we only observed variables. Now enter Function Declarations. - -**Unlike `let` variables, they are fully initialized not when the execution reaches them, but earlier, when a Lexical Environment is created.** - -For top-level functions, it means the moment when the script is started. - -That is why we can call a function declaration before it is defined. - -The code below demonstrates that the Lexical Environment is non-empty from the beginning. It has `say`, because that's a Function Declaration. And later it gets `phrase`, declared with `let`: - -![lexical environment](lexical-environment-global-3.svg) - - -### Inner and outer Lexical Environment - -Now let's go on and explore what happens when a function accesses an outer variable. - -During the call, `say()` uses the outer variable `phrase`. Let's look at the details of what's going on. - -When a function runs, a new Lexical Environment is created automatically to store local variables and parameters of the call. - -For instance, for `say("John")`, it looks like this (the execution is at the line, labelled with an arrow): - - - -![lexical environment](lexical-environment-simple.svg) +- In this article we'll use `let` variables in examples. +- Variables, declared with `const`, behave the same, so this article is about `const` too. +- The old `var` has some notable differences, they will be covered in the article . +``` -So, during the function call we have two Lexical Environments: the inner one (for the function call) and the outer one (global): +## Code blocks -- The inner Lexical Environment corresponds to the current execution of `say`. +If a variable is declared inside a code block `{...}`, it's only visible inside that block. - It has a single property: `name`, the function argument. We called `say("John")`, so the value of `name` is `"John"`. -- The outer Lexical Environment is the global Lexical Environment. +For example: - It has `phrase` variable and the function itself. +```js run +{ + // do some job with local variables that should not be seen outside -The inner Lexical Environment has a reference to the `outer` one. + let message = "Hello"; // only visible in this block -**When the code wants to access a variable -- the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.** + alert(message); // Hello +} -If a variable is not found anywhere, that's an error in strict mode. Without `use strict`, an assignment to a non-existing variable like `user = "John"` creates a new global variable `user`. That's for backwards compatibility. +alert(message); // Error: message is not defined +``` -Let's see how the search proceeds in our example: +We can use this to isolate a piece of code that does its own task, with variables that only belong to it: -- When the `alert` inside `say` wants to access `name`, it finds it immediately in the function Lexical Environment. -- When it wants to access `phrase`, then there is no `phrase` locally, so it follows the reference to the enclosing Lexical Environment and finds it there. +```js run +{ + // show message + let message = "Hello"; + alert(message); +} -![lexical environment lookup](lexical-environment-simple-lookup.svg) +{ + // show another message + let message = "Goodbye"; + alert(message); +} +``` -Now we can give the answer to the first question from the beginning of the chapter. +````smart header="There'd be an error without blocks" +Please note, without separate blocks there would be an error, if we use `let` with the existing variable name: -**A function gets outer variables as they are now, it uses the most recent values.** +```js run +// show message +let message = "Hello"; +alert(message); -Old variable values are not saved anywhere. When a function wants a variable, it takes the current value from its own Lexical Environment or the outer one. +// show another message +*!* +let message = "Goodbye"; // Error: variable already declared +*/!* +alert(message); +``` +```` -So the answer to the first question is `Pete`: +For `if`, `for`, `while` and so on, variables declared in `{...}` are also only visible inside: ```js run -let name = "John"; +if (true) { + let phrase = "Hello!"; -function sayHi() { - alert("Hi, " + name); + alert(phrase); // Hello! } -name = "Pete"; // (*) - -*!* -sayHi(); // Pete -*/!* +alert(phrase); // Error, no such variable! ``` +Here, after `if` finishes, the `alert` below won't see the `phrase`, hence the error. -The execution flow of the code above: - -1. The global Lexical Environment has `name: "John"`. -2. At the line `(*)` the global variable is changed. Now it has `name: "Pete"`. -3. When the function `sayHi()` is executed it takes `name` from outside, the global Lexical Environment, where its value is already `"Pete"`. +That's great, as it allows us to create block-local variables, specific to an `if` branch. +The similar thing holds true for `for` and `while` loops: -```smart header="One call -- one Lexical Environment" -Please note that a new function Lexical Environment is created each time a function runs. - -And if a function is called multiple times, then each invocation will have its own Lexical Environment, with local variables and parameters specific for that very run. -``` +```js run +for (let i = 0; i < 3; i++) { + // the variable i is only visible inside this for + alert(i); // 0, then 1, then 2 +} -```smart header="Lexical Environment is a specification object" -"Lexical Environment" is a specification object: it only exists "theoretically" in the [language specification](https://tc39.es/ecma262/#sec-lexical-environments) to describe how things work. We can't get this object in our code and manipulate it directly. JavaScript engines also may optimize it, discard variables that are unused to save memory and perform other internal tricks, as long as the visible behavior remains as described. +alert(i); // Error, no such variable ``` +Visually, `let i` is outside of `{...}`. But the `for` construct is special here: the variable, declared inside it, is considered a part of the block. ## Nested functions @@ -223,32 +118,16 @@ function sayHiBye(firstName, lastName) { Here the *nested* function `getFullName()` is made for convenience. It can access the outer variables and so can return the full name. Nested functions are quite common in JavaScript. -What's much more interesting, a nested function can be returned: either as a property of a new object (if the outer function creates an object with methods) or as a result by itself. It can then be used somewhere else. No matter where, it still has access to the same outer variables. - -For instance, here the nested function is assigned to the new object by the [constructor function](info:constructor-new): - -```js run -// constructor function returns a new object -function User(name) { - - // the object method is created as a nested function - this.sayHi = function() { - alert(name); - }; -} - -let user = new User("John"); -user.sayHi(); // the method "sayHi" code has access to the outer "name" -``` +What's much more interesting, a nested function can be returned: either as a property of a new object or as a result by itself. It can then be used somewhere else. No matter where, it still has access to the same outer variables. -And here we just create and return a "counting" function: +Below, `makeCounter` creates the "counter" function that returns the next number on each invocation: ```js run function makeCounter() { let count = 0; return function() { - return count++; // has access to the outer "count" + return count++; }; } @@ -259,314 +138,195 @@ alert( counter() ); // 1 alert( counter() ); // 2 ``` -Let's go on with the `makeCounter` example. It creates the "counter" function that returns the next number on each invocation. Despite being simple, slightly modified variants of that code have practical uses, for instance, as a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator), and more. - -How does the counter work internally? - -When the inner function runs, the variable in `count++` is searched from inside out. For the example above, the order will be: +Despite being simple, slightly modified variants of that code have practical uses, for instance, as a [random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) to generate random values for automated tests. -![](lexical-search-order.svg) +How does this work? If we create multiple counters, will they be independent? What's going on with the variables here? -1. The locals of the nested function... -2. The variables of the outer function... -3. And so on until it reaches global variables. +Undestanding such things is great for the overall knowledge of JavaScript and beneficial for more complex scenarios. So let's go a bit in-depth. -In this example `count` is found on step `2`. When an outer variable is modified, it's changed where it's found. So `count++` finds the outer variable and increases it in the Lexical Environment where it belongs. Like if we had `let count = 1`. - -Here are two questions to consider: - -1. Can we somehow reset the counter `count` from the code that doesn't belong to `makeCounter`? E.g. after `alert` calls in the example above. -2. If we call `makeCounter()` multiple times -- it returns many `counter` functions. Are they independent or do they share the same `count`? - -Try to answer them before you continue reading. - -... - -All done? - -Okay, let's go over the answers. - -1. There is no way: `count` is a local function variable, we can't access it from the outside. -2. For every call to `makeCounter()` a new function Lexical Environment is created, with its own `count`. So the resulting `counter` functions are independent. - -Here's the demo: - -```js run -function makeCounter() { - let count = 0; - return function() { - return count++; - }; -} - -let counter1 = makeCounter(); -let counter2 = makeCounter(); +## Lexical Environment -alert( counter1() ); // 0 -alert( counter1() ); // 1 +```warn header="Here be dragons!" +The in-depth technical explanation lies ahead. -alert( counter2() ); // 0 (independent) +As far as I'd like to avoid low-level language details, any understanding without them would be lacking and incomplete, so get ready. ``` +For clarity, the explanation is split into multiple steps. -Hopefully, the situation with outer variables is clear now. For most situations such understanding is enough. There are few details in the specification that we omitted for brevity. So in the next section we cover even more details. - -## Environments in detail - -Here's what's going on in the `makeCounter` example step-by-step. Follow it to make sure that you understand how it works in detail. - -Please note the additional `[[Environment]]` property is covered here. We didn't mention it before for simplicity. - -1. When the script has just started, there is only the global Lexical Environment: - - ![](lexenv-nested-makecounter-1.svg) - - At that starting moment there is only the `makeCounter` function, because it's a Function Declaration. It did not run yet. - - **All functions "on birth" receive a hidden property `[[Environment]]` with a reference to the Lexical Environment of their creation.** - - We didn't talk about it before. That's how the function knows where it was made. - - Here, `makeCounter` is created in the global Lexical Environment, so `[[Environment]]` keeps a reference to it. - - In other words, a function is "imprinted" with a reference to the Lexical Environment where it was born. And `[[Environment]]` is the hidden function property that has that reference. - -2. The code runs on, the new global variable `counter` is declared and gets the result of the `makeCounter()` call. Here's a snapshot of the moment when the execution is on the first line inside `makeCounter()`: - - ![](lexenv-nested-makecounter-2.svg) +### Step 1. Variables - At the moment of the call of `makeCounter()`, the Lexical Environment is created, to hold its variables and arguments. - - As all Lexical Environments, it stores two things: - 1. An Environment Record with local variables. In our case `count` is the only local variable (appearing when the line with `let count` is executed). - 2. The outer lexical reference, which is set to the value of `[[Environment]]` of the function. Here `[[Environment]]` of `makeCounter` references the global Lexical Environment. - - So, now we have two Lexical Environments: the first one is global, the second one is for the current `makeCounter` call, with the outer reference to global. - -3. During the execution of `makeCounter()`, a tiny nested function is created. - - It doesn't matter whether the function is created using Function Declaration or Function Expression. All functions get the `[[Environment]]` property that references the Lexical Environment in which they were made. So our new tiny nested function gets it as well. - - For our new nested function the value of `[[Environment]]` is the current Lexical Environment of `makeCounter()` (where it was born): +In JavaScript, every running function, code block `{...}`, and the script as a whole have an internal (hidden) associated object known as the *Lexical Environment*. - ![](lexenv-nested-makecounter-3.svg) +The Lexical Environment object consists of two parts: - Please note that on this step the inner function was created, but not yet called. The code inside `return count++;` is not running. +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. -4. As the execution goes on, the call to `makeCounter()` finishes, and the result (the tiny nested function) is assigned to the global variable `counter`: +**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".** - ![](lexenv-nested-makecounter-4.svg) +In this simple code without functions, there is only one Lexical Environment: - That function has only one line: `return count++`, that will be executed when we run it. +![lexical environment](lexical-environment-global.svg) -5. When `counter()` is called, a new Lexical Environment is created for the call. It's empty, as `counter` has no local variables by itself. But the `[[Environment]]` of `counter` is used as the `outer` reference for it, that provides access to the variables of the former `makeCounter()` call where it was created: +This is the so-called *global* Lexical Environment, associated with the whole script. - ![](lexenv-nested-makecounter-5.svg) +On the picture above, the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical Environment has no outer reference, that's why the arrow points to `null`. - Now when the call looks for `count` variable, it first searches its own Lexical Environment (empty), then the Lexical Environment of the outer `makeCounter()` call, where it finds it. +As the code starts executing and goes on, the Lexical Environment changes. - Please note how memory management works here. Although `makeCounter()` call finished some time ago, its Lexical Environment was retained in memory, because there's a nested function with `[[Environment]]` referencing it. +Here's a little bit longer code: - Generally, a Lexical Environment object lives as long as there is a function which may use it. And only when there are none remaining, it is cleared. +![lexical environment](closure-variable-phrase.svg) -6. The call to `counter()` not only returns the value of `count`, but also increases it. Note that the modification is done "in place". The value of `count` is modified exactly in the environment where it was found. +Rectangles on the right-hand side demonstrate how the global Lexical Environment changes during the execution: - ![](lexenv-nested-makecounter-6.svg) +1. When the script starts, the Lexical Environment is pre-populated with all declared variables. + - Initially, they are in the "Uninitialized" state. That's a special internal state, it means that the engine knows about the variable, but it cannot be referenced until it has been declared with `let`. It's almost the same as if the variable didn't exist. +2. Then `let phrase` definition appears. There's no assignment yet, so its value is `undefined`. We can use the variable since this moment. +3. `phrase` is assigned a value. +4. `phrase` changes the value. -7. Next `counter()` invocations do the same. +Everything looks simple for now, right? -The answer to the second question from the beginning of the chapter should now be obvious. +- A variable is a property of a special internal object, associated with the currently executing block/function/script. +- Working with variables is actually working with the properties of that object. -The `work()` function in the code below gets `name` from the place of its origin through the outer lexical environment reference: +```smart header="Lexical Environment is a specification object" +"Lexical Environment" is a specification object: it only exists "theoretically" in the [language specification](https://tc39.es/ecma262/#sec-lexical-environments) to describe how things work. We can't get this object in our code and manipulate it directly. -![](lexenv-nested-work.svg) +JavaScript engines also may optimize it, discard variables that are unused to save memory and perform other internal tricks, as long as the visible behavior remains as described. +``` -So, the result is `"Pete"` here. +### Step 2. Function Declarations -But if there were no `let name` in `makeWorker()`, then the search would go outside and take the global variable as we can see from the chain above. In that case it would be `"John"`. +A function is also a value, like a variable. -```smart header="Closures" -There is a general programming term "closure", that developers generally should know. +**The difference is that a Function Declaration is instantly fully initialized.** -A [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) is a function that remembers its outer variables and can access them. In some languages, that's not possible, or a function should be written in a special way to make it happen. But as explained above, in JavaScript, all functions are naturally closures (there is only one exclusion, to be covered in ). +When a Lexical Environment is created, a Function Declaration immediately becomes a ready-to-use function (unlike `let`, that is unusable till the declaration). -That is: they automatically remember where they were created using a hidden `[[Environment]]` property, and all of them can access outer variables. +That's why we can use a function, declared as Function Declaration, even before the declaration itself. -When on an interview, a frontend developer gets a question about "what's a closure?", a valid answer would be a definition of the closure and an explanation that all functions in JavaScript are closures, and maybe a few more words about technical details: the `[[Environment]]` property and how Lexical Environments work. -``` +For example, here's the initial state of the global Lexical Environment when we add a function: -## Code blocks and loops, IIFE +![](closure-function-declaration.svg) -The examples above concentrated on functions. But a Lexical Environment exists for any code block `{...}`. +Naturally, this behavior only applies to Function Declarations, not Function Expressions where we assign a function to a variable, such as `let say = function(name)...`. -A Lexical Environment is created when a code block runs and contains block-local variables. Here are a couple of examples. +### Step 3. Inner and outer Lexical Environment -### If +When a function runs, at the beginning of the call, a new Lexical Environment is created automatically to store local variables and parameters of the call. -In the example below, the `user` variable exists only in the `if` block: +For instance, for `say("John")`, it looks like this (the execution is at the line, labelled with an arrow): -![](lexenv-if.svg) - -When the execution gets into the `if` block, the new "if-only" Lexical Environment is created for it. - -It has the reference to the outer one, so `phrase` can be found. But all variables and Function Expressions, declared inside `if`, reside in that Lexical Environment and can't be seen from the outside. - -For instance, after `if` finishes, the `alert` below won't see the `user`, hence the error. +![](lexical-environment-simple.svg) -### For, while +During the function call we have two Lexical Environments: the inner one (for the function call) and the outer one (global): -For a loop, every iteration has a separate Lexical Environment. If a variable is declared in `for(let ...)`, then it's also in there: +- The inner Lexical Environment corresponds to the current execution of `say`. It has a single property: `name`, the function argument. We called `say("John")`, so the value of the `name` is `"John"`. +- The outer Lexical Environment is the global Lexical Environment. It has the `phrase` variable and the function itself. -```js run -for (let i = 0; i < 10; i++) { - // Each loop has its own Lexical Environment - // {i: value} -} +The inner Lexical Environment has a reference to the `outer` one. -alert(i); // Error, no such variable -``` +**When the code wants to access a variable -- the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.** -Please note: `let i` is visually outside of `{...}`. The `for` construct is special here: each iteration of the loop has its own Lexical Environment with the current `i` in it. +If a variable is not found anywhere, that's an error in strict mode (without `use strict`, an assignment to a non-existing variable creates a new global variable, for compatibility with old code). -Again, similarly to `if`, after the loop `i` is not visible. +In this example the search proceeds as follows: -### Code blocks +- For the `name` variable, the `alert` inside `say` finds it immediately in the inner Lexical Environment. +- When it wants to access `phrase`, then there is no `phrase` locally, so it follows the reference to the outer Lexical Environment and finds it there. -We also can use a "bare" code block `{…}` to isolate variables into a "local scope". +![lexical environment lookup](lexical-environment-simple-lookup.svg) -For instance, in a web browser all scripts (except with `type="module"`) share the same global area. So if we create a global variable in one script, it becomes available to others. But that becomes a source of conflicts if two scripts use the same variable name and overwrite each other. -That may happen if the variable name is a widespread word, and script authors are unaware of each other. +### Step 4. Returning a function -If we'd like to avoid that, we can use a code block to isolate the whole script or a part of it: +Let's return to the `makeCounter` example. -```js run -{ - // do some job with local variables that should not be seen outside - - let message = "Hello"; +```js +function makeCounter() { + let count = 0; - alert(message); // Hello + return function() { + return count++; + }; } -alert(message); // Error: message is not defined +let counter = makeCounter(); ``` -The code outside of the block (or inside another script) doesn't see variables inside the block, because the block has its own Lexical Environment. +At the beginning of each `makeCounter()` call, a new Lexical Environment object is created, to store variables for this `makeCounter` run. -### IIFE +So we have two nested Lexical Environments, just like in the example above: -In the past, there were no block-level lexical environments in JavaScript. +![](closure-makecounter.svg) -So programmers had to invent something. And what they did was called "immediately-invoked function expressions" (abbreviated as IIFE). +What's different is that, during the execution of `makeCounter()`, a tiny nested function is created of only one line: `return count++`. We don't run it yet, only create. -That's not a thing we should use nowadays, but you can find them in old scripts, so it's better to understand them. +All functions remember the Lexical Environment in which they were made. Technically, there's no magic here: all functions have the hidden property named `[[Environment]]`, that keeps the reference to the Lexical Environment where the function was created: -An IIFE looks like this: +![](closure-makecounter-environment.svg) -```js run -(function() { +So, `counter.[[Environment]]` has the reference to `{count: 0}` Lexical Environment. That's how the function remembers where it was created, no matter where it's called. The `[[Environment]]` reference is set once and forever at function creation time. - let message = "Hello"; +Later, when `counter()` is called, a new Lexical Environment is created for the call, and its outer Lexical Environment reference is taken from `counter.[[Environment]]`: - alert(message); // Hello +![](closure-makecounter-nested-call.svg) -})(); -``` +Now when the code inside `counter()` looks for `count` variable, it first searches its own Lexical Environment (empty, as there are no local variables there), then the Lexical Environment of the outer `makeCounter()` call, where finds it and changes. -Here a Function Expression is created and immediately called. So the code executes right away and has its own private variables. +**A variable is updated in the Lexical Environment where it lives.** -The Function Expression is wrapped with parenthesis `(function {...})`, because when JavaScript meets `"function"` in the main code flow, it understands it as the start of a Function Declaration. But a Function Declaration must have a name, so this kind of code will give an error: +Here's the state after the execution: -```js run -// Try to declare and immediately call a function -function() { // <-- Error: Unexpected token ( +![](closure-makecounter-nested-call-2.svg) - let message = "Hello"; +If we call `counter()` multiple times, the `count` variable will be increased to `2`, `3` and so on, at the same place. - alert(message); // Hello - -}(); -``` - -Even if we say: "okay, let's add a name", that won't work, as JavaScript does not allow Function Declarations to be called immediately: - -```js run -// syntax error because of parentheses below -function go() { - -}(); // <-- can't call Function Declaration immediately -``` - -So, the parentheses around the function is a trick to show JavaScript that the function is created in the context of another expression, and hence it's a Function Expression: it needs no name and can be called immediately. - -There exist other ways besides parentheses to tell JavaScript that we mean a Function Expression: - -```js run -// Ways to create IIFE - -(function() { - alert("Parentheses around the function"); -}*!*)*/!*(); +```smart header="Closure" +There is a general programming term "closure", that developers generally should know. -(function() { - alert("Parentheses around the whole thing"); -}()*!*)*/!*; +A [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)) is a function that remembers its outer variables and can access them. In some languages, that's not possible, or a function should be written in a special way to make it happen. But as explained above, in JavaScript, all functions are naturally closures (there is only one exception, to be covered in ). -*!*!*/!*function() { - alert("Bitwise NOT operator starts the expression"); -}(); +That is: they automatically remember where they were created using a hidden `[[Environment]]` property, and then their code can access outer variables. -*!*+*/!*function() { - alert("Unary plus starts the expression"); -}(); +When on an interview, a frontend developer gets a question about "what's a closure?", a valid answer would be a definition of the closure and an explanation that all functions in JavaScript are closures, and maybe a few more words about technical details: the `[[Environment]]` property and how Lexical Environments work. ``` -In all the above cases we declare a Function Expression and run it immediately. Let's note again: nowadays there's no reason to write such code. - ## Garbage collection -Usually, a Lexical Environment is cleaned up and deleted after the function runs. For instance: +Usually, a Lexical Environment is removed from memory with all the variables after the function call finishes. That's because there are no references to it. As any JavaScript object, it's only kept in memory while it's reachable. -```js -function f() { - let value1 = 123; - let value2 = 456; -} - -f(); -``` +...But if there's a nested function that is still reachable after the end of a function, then it has `[[Environment]]` property that references the lexical environment. -Here, two values are technically the properties of the Lexical Environment. But after `f()` finishes, that Lexical Environment becomes unreachable, so it's deleted from the memory. +In that case the Lexical Environment is still reachable even after the completion of the function, so it stays alive. -...But if there's a nested function that is still reachable after the end of `f`, then it has `[[Environment]]` property that references the outer lexical environment, so it's also reachable and alive: +For example: ```js function f() { let value = 123; - function g() { alert(value); } - -*!* - return g; -*/!* + return function() { + alert(value); + } } -let func = f(); // func gets a reference to g -// so it stays and memory and its outer lexical environment stays as well +let g = f(); // g.[[Environment]] stores a reference to the Lexical Environment +// of the corresponding f() call ``` Please note that if `f()` is called many times, and resulting functions are saved, then all corresponding Lexical Environment objects will also be retained in memory. All 3 of them in the code below: @@ -585,20 +345,20 @@ let arr = [f(), f(), f()]; A Lexical Environment object dies when it becomes unreachable (just like any other object). In other words, it exists only while there's at least one nested function referencing it. -In the code below, after `g` becomes unreachable, its enclosing Lexical Environment (and hence the `value`) is cleaned from memory; +In the code below, after the nested function is removed, its enclosing Lexical Environment (and hence the `value`) is cleaned from memory: ```js function f() { let value = 123; - function g() { alert(value); } - - return g; + return function() { + alert(value); + } } -let func = f(); // while func has a reference to g, it stays in memory +let g = f(); // while g function exists, the value stays in memory -func = null; // ...and now the memory is cleaned up +g = null; // ...and now the memory is cleaned up ``` ### Real-life optimizations @@ -649,9 +409,6 @@ let g = f(); g(); ``` -```warn header="See ya!" This feature of V8 is good to know. If you are debugging with Chrome/Opera, sooner or later you will meet it. -That is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be changed sometime. -You always can check for it by running the examples on this page. -``` +That is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be changed sometime. You always can check for it by running the examples on this page. diff --git a/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg b/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg new file mode 100644 index 000000000..97f76e569 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-function-declaration.svg @@ -0,0 +1 @@ +outernullexecution startphrase: <uninitialized> say: function... \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-environment.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-environment.svg new file mode 100644 index 000000000..b9060bc8a --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-environment.svg @@ -0,0 +1 @@ +null[[Environment]]makeCounter: function counter: undefinedcount: 0outerouter \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg new file mode 100644 index 000000000..3e4206ca6 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call-2.svg @@ -0,0 +1 @@ +count: 1<empty>nullouterouteroutermakeCounter: function counter: functionmodified here \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg new file mode 100644 index 000000000..e1bb8cc8f --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter-nested-call.svg @@ -0,0 +1 @@ +count: 0<empty>nullouterouteroutermakeCounter: function counter: function \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-makecounter.svg b/1-js/06-advanced-functions/03-closure/closure-makecounter.svg new file mode 100644 index 000000000..2a1c4a729 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-makecounter.svg @@ -0,0 +1 @@ +makeCounter: function counter: undefinedcount: 0nullglobal LexicalEnvironmentLexicalEnvironment of makeCounter() callouterouter \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg b/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg new file mode 100644 index 000000000..741c05448 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/closure-variable-phrase.svg @@ -0,0 +1 @@ +phrase: "Bye"phrase: "Hello"phrase: undefinedphrase: <uninitialized>outernullexecution start \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-if.svg b/1-js/06-advanced-functions/03-closure/lexenv-if.svg index 48dd7d1bf..b644fe154 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-if.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-if.svg @@ -1 +1 @@ -phrase: "Hello"outerouternulluser: "John" \ No newline at end of file +phrase: "Hello"outerouternulluser: "John" \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg index f70fb41f0..66e5200f8 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-2.svg @@ -1 +1 @@ -makeCounter: functioncounter: undefinedcount: 0outerouternullglobal LexicalEnvironmentLexicalEnvironment of makeCounter() call \ No newline at end of file +makeCounter: functioncounter: undefinedcount: 0outerouternullglobal LexicalEnvironmentLexicalEnvironment of makeCounter() call \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg index 9a3298ca3..28c526c4f 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-3.svg @@ -1 +1 @@ -makeCounter: functioncounter: undefinedcount: 0outerouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: undefinedcount: 0outerouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg index 02d8a93fb..acc1e8fb9 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-4.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 0outerouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: functioncount: 0outerouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg index 6c8f66495..cf91c331d 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-5.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 0<empty>outerouterouternull[[Environment]] \ No newline at end of file +makeCounter: functioncounter: functioncount: 0<empty>outerouterouternull[[Environment]] \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg index 3b0ac9d0c..def542ceb 100644 --- a/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg +++ b/1-js/06-advanced-functions/03-closure/lexenv-nested-makecounter-6.svg @@ -1 +1 @@ -makeCounter: functioncounter: functioncount: 1outerouternull[[Environment]]modified here \ No newline at end of file +makeCounter: functioncounter: functioncount: 1outerouternull[[Environment]]modified here \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg index aac8c5927..9620f0485 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-global.svg @@ -1 +1 @@ -phrase: "Hello"outernullLexicalEnvironment \ No newline at end of file +phrase: "Hello"outernullLexical Environment \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg index 40e2b92ad..ff0486ede 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-simple-lookup.svg @@ -1 +1 @@ -say: function phrase: "Hello"name: "John"outerouternull \ No newline at end of file +say: function phrase: "Hello"name: "John"outerouternull \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg b/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg index a962f0e5f..abd77fff9 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-environment-simple.svg @@ -1 +1 @@ -say: function phrase: "Hello"name: "John"outerouternullLexicalEnvironment for the call \ No newline at end of file +say: function phrase: "Hello"name: "John"outerouternullLexical Environment of the call \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/lexical-search-order.svg b/1-js/06-advanced-functions/03-closure/lexical-search-order.svg index 44db4b0c1..89a9d110a 100644 --- a/1-js/06-advanced-functions/03-closure/lexical-search-order.svg +++ b/1-js/06-advanced-functions/03-closure/lexical-search-order.svg @@ -1 +1 @@ -123 \ No newline at end of file +123 \ No newline at end of file diff --git a/1-js/06-advanced-functions/03-closure/variable-scope-lookup.svg b/1-js/06-advanced-functions/03-closure/variable-scope-lookup.svg new file mode 100644 index 000000000..674437196 --- /dev/null +++ b/1-js/06-advanced-functions/03-closure/variable-scope-lookup.svg @@ -0,0 +1 @@ +functionUser(name){this.sayHi=function(){alert(name);};}letuser=newUser("John");user.sayHi(); \ No newline at end of file diff --git a/1-js/06-advanced-functions/04-var/article.md b/1-js/06-advanced-functions/04-var/article.md index 02fd43fe5..1e8bb5bae 100644 --- a/1-js/06-advanced-functions/04-var/article.md +++ b/1-js/06-advanced-functions/04-var/article.md @@ -1,33 +1,30 @@ # The old "var" +```smart header="This article is for understanding old scripts" +The information in this article is useful for understanding old scripts. + +That's not how we write a new code. +``` + In the very first chapter about [variables](info:variables), we mentioned three ways of variable declaration: 1. `let` 2. `const` 3. `var` -`let` and `const` behave exactly the same way in terms of Lexical Environments. - -But `var` is a very different beast, that originates from very old times. It's generally not used in modern scripts, but still lurks in the old ones. - -If you don't plan on meeting such scripts you may even skip this chapter or postpone it, but then there's a chance that it bites you later. - -From the first sight, `var` behaves similar to `let`. That is, declares a variable: +The `var` declaration is similar to `let`. Most of the time we can replace `let` by `var` or vice-versa and expect things to work: ```js run -function sayHi() { - var phrase = "Hello"; // local variable, "var" instead of "let" +var message = "Hi"; +alert(message); // Hi +``` - alert(phrase); // Hello -} +But internally `var` is a very different beast, that originates from very old times. It's generally not used in modern scripts, but still lurks in the old ones. -sayHi(); +If you don't plan on meeting such scripts you may even skip this chapter or postpone it. -alert(phrase); // Error, phrase is not defined -``` - -...But here are the differences. +On the other hand, it's important to understand differences when migrating old scripts from `var` to `let`, to avoid odd errors. ## "var" has no block scope @@ -88,7 +85,27 @@ alert(phrase); // Error: phrase is not defined (Check the Developer Console) As we can see, `var` pierces through `if`, `for` or other code blocks. That's because a long time ago in JavaScript blocks had no Lexical Environments. And `var` is a remnant of that. -## "var" declarations are processed at the function start +## "var" tolerates redeclarations + +If we declare the same variable with `let` twice in the same scope, that's an error: + +```js run +let user; +let user; // SyntaxError: 'user' has already been declared +``` + +With `var`, we can redeclare a variable any number of times. If we use `var` with an already-declared variable, it's just ignored: + +```js run +var user = "Pete"; + +var user = "John"; // this "var" does nothing (already declared) +// ...it doesn't trigger an error + +alert(user); // John +``` + +## "var" variables can be declared below their use `var` declarations are processed when the function starts (or script starts for globals). @@ -147,7 +164,7 @@ So in the example above, `if (false)` branch never executes, but that doesn't ma **Declarations are hoisted, but assignments are not.** -That's better to demonstrate with an example, like this: +That's best demonstrated with an example: ```js run function sayHi() { @@ -188,6 +205,74 @@ Because all `var` declarations are processed at the function start, we can refer In both examples above `alert` runs without an error, because the variable `phrase` exists. But its value is not yet assigned, so it shows `undefined`. +### IIFE + +As in the past there was only `var`, and it has no block-level visibility, programmers invented a way to emulate it. What they did was called "immediately-invoked function expressions" (abbreviated as IIFE). + +That's not something we should use nowadays, but you can find them in old scripts. + +An IIFE looks like this: + +```js run +(function() { + + let message = "Hello"; + + alert(message); // Hello + +})(); +``` + +Here a Function Expression is created and immediately called. So the code executes right away and has its own private variables. + +The Function Expression is wrapped with parenthesis `(function {...})`, because when JavaScript meets `"function"` in the main code flow, it understands it as the start of a Function Declaration. But a Function Declaration must have a name, so this kind of code will give an error: + +```js run +// Try to declare and immediately call a function +function() { // <-- Error: Function statements require a function name + + let message = "Hello"; + + alert(message); // Hello + +}(); +``` + +Even if we say: "okay, let's add a name", that won't work, as JavaScript does not allow Function Declarations to be called immediately: + +```js run +// syntax error because of parentheses below +function go() { + +}(); // <-- can't call Function Declaration immediately +``` + +So, the parentheses around the function is a trick to show JavaScript that the function is created in the context of another expression, and hence it's a Function Expression: it needs no name and can be called immediately. + +There exist other ways besides parentheses to tell JavaScript that we mean a Function Expression: + +```js run +// Ways to create IIFE + +(function() { + alert("Parentheses around the function"); +}*!*)*/!*(); + +(function() { + alert("Parentheses around the whole thing"); +}()*!*)*/!*; + +*!*!*/!*function() { + alert("Bitwise NOT operator starts the expression"); +}(); + +*!*+*/!*function() { + alert("Unary plus starts the expression"); +}(); +``` + +In all the above cases we declare a Function Expression and run it immediately. Let's note again: nowadays there's no reason to write such code. + ## Summary There are two main differences of `var` compared to `let/const`: @@ -195,6 +280,6 @@ There are two main differences of `var` compared to `let/const`: 1. `var` variables have no block scope, they are visible minimum at the function level. 2. `var` declarations are processed at function start (script start for globals). -There's one more minor difference related to the global object, we'll cover that in the next chapter. +There's one more very minor difference related to the global object, that we'll cover in the next chapter. These differences make `var` worse than `let` most of the time. Block-level variables is such a great thing. That's why `let` was introduced in the standard long ago, and is now a major way (along with `const`) to declare a variable. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js index 065a77d1f..661dd0cf4 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/solution.js @@ -1,15 +1,7 @@ -function debounce(f, ms) { - - let isCooldown = false; - +function debounce(func, ms) { + let timeout; return function() { - if (isCooldown) return; - - f.apply(this, arguments); - - isCooldown = true; - - setTimeout(() => isCooldown = false, ms); + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, arguments), ms); }; - -} \ No newline at end of file +} diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js index 8136b873c..750e649f8 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/_js.view/test.js @@ -1,50 +1,48 @@ -describe("debounce", function() { - before(function() { +describe('debounce', function () { + before(function () { this.clock = sinon.useFakeTimers(); }); - after(function() { + after(function () { this.clock.restore(); }); - it("trigger the fuction execution immediately", function () { - let mode; - const f = () => mode='leading'; - - debounce(f, 1000)(); // runs without a delay - - assert.equal(mode, 'leading'); + it('for one call - runs it after given ms', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); + + debounced('test'); + assert(f.notCalled, 'not called immediately'); + this.clock.tick(1000); + assert(f.calledOnceWith('test'), 'called after 1000ms'); }); - - it("calls the function at maximum once in ms milliseconds", function() { - let log = ''; - function f(a) { - log += a; - } + it('for 3 calls - runs the last one after given ms', function () { + const f = sinon.spy(); + const debounced = debounce(f, 1000); - f = debounce(f, 1000); + debounced('a'); + setTimeout(() => debounced('b'), 200); // ignored (too early) + setTimeout(() => debounced('c'), 500); // runs (1000 ms passed) + this.clock.tick(1000); - f(1); // runs at once - f(2); // ignored + assert(f.notCalled, 'not called after 1000ms'); - setTimeout(() => f(3), 100); // ignored (too early) - setTimeout(() => f(4), 1100); // runs (1000 ms passed) - setTimeout(() => f(5), 1500); // ignored (less than 1000 ms from the last run) + this.clock.tick(500); - this.clock.tick(5000); - assert.equal(log, "14"); + assert(f.calledOnceWith('c'), 'called after 1500ms'); }); - it("keeps the context of the call", function() { + it('keeps the context of the call', function () { let obj = { f() { assert.equal(this, obj); - } + }, }; obj.f = debounce(obj.f, 1000); - obj.f("test"); + obj.f('test'); + this.clock.tick(5000); }); - + }); diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg new file mode 100644 index 000000000..5896a5fa4 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.svg @@ -0,0 +1 @@ +200ms1500ms1000ms0cf(a)f(b)f(c)500mstimecalls: after 1000ms \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html new file mode 100644 index 000000000..e3b4d5842 --- /dev/null +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/debounce.view/index.html @@ -0,0 +1,24 @@ + + + +Function handler is called on this input: +
+ + +

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

+ + + \ No newline at end of file diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md index 4f5867ded..83e75f315 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/solution.md @@ -1,28 +1,13 @@ ```js demo -function debounce(f, ms) { - - let isCooldown = false; - +function debounce(func, ms) { + let timeout; return function() { - if (isCooldown) return; - - f.apply(this, arguments); - - isCooldown = true; - - setTimeout(() => isCooldown = false, ms); + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, arguments), ms); }; - } -``` - -A call to `debounce` returns a wrapper. There may be two states: -- `isCooldown = false` -- ready to run. -- `isCooldown = true` -- waiting for the timeout. - -In the first call `isCooldown` is falsy, so the call proceeds, and the state changes to `true`. +``` -While `isCooldown` is true, all other calls are ignored. +A call to `debounce` returns a wrapper. When called, it schedules the original function call after given `ms` and cancels the previous such timeout. -Then `setTimeout` reverts it to `false` after the given delay. diff --git a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md index 2620f1c71..550bf52da 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/03-debounce/task.md @@ -4,21 +4,48 @@ importance: 5 # Debounce decorator -The result of `debounce(f, ms)` decorator should be a wrapper that passes the call to `f` at maximum once per `ms` milliseconds. +The result of `debounce(f, ms)` decorator is a wrapper that suspends calls to `f` until there's `ms` milliseconds of inactivity (no calls, "cooldown period"), then invokes `f` once with the latest arguments. -In other words, when we call a "debounced" function, it guarantees that all future calls to the function made less than `ms` milliseconds after the previous call will be ignored. +For instance, we had a function `f` and replaced it with `f = debounce(f, 1000)`. -For instance: +Then if the wrapped function is called at 0ms, 200ms and 500ms, and then there are no calls, then the actual `f` will be only called once, at 1500ms. That is: after the cooldown period of 1000ms from the last call. -```js no-beautify -let f = debounce(alert, 1000); +![](debounce.svg) -f(1); // runs immediately -f(2); // ignored +...And it will get the arguments of the very last call, other calls are ignored. -setTimeout( () => f(3), 100); // ignored ( only 100 ms passed ) -setTimeout( () => f(4), 1100); // runs -setTimeout( () => f(5), 1500); // ignored (less than 1000 ms from the last run) +Here's the code for it (uses the debounce decorator from the [Lodash library](https://lodash.com/docs/4.17.15#debounce): + +```js +let f = _.debounce(alert, 1000); + +f("a"); +setTimeout( () => f("b"), 200); +setTimeout( () => f("c"), 500); +// debounced function waits 1000ms after the last call and then runs: alert("c") ``` -In practice `debounce` is useful for functions that retrieve/update something when we know that nothing new can be done in such a short period of time, so it's better not to waste resources. + +Now a practical example. Let's say, the user types something, and we'd like to send a request to the server when the input is finished. + +There's no point in sending the request for every character typed. Instead we'd like to wait, and then process the whole result. + +In a web-browser, we can setup an event handler -- a function that's called on every change of an input field. Normally, an event handler is called very often, for every typed key. But if we `debounce` it by 1000ms, then it will be only called once, after 1000ms after the last input. + +```online + +In this live example, the handler puts the result into a box below, try it: + +[iframe border=1 src="debounce" height=200] + +See? The second input calls the debounced function, so its content is processed after 1000ms from the last input. +``` + +So, `debounce` is a great way to process a sequence of events: be it a sequence of key presses, mouse movements or something else. + + +It waits the given time after the last call, and then runs its function, that can process the result. + +The task is to implement `debounce` decorator. + +Hint: that's just a few lines if you think about it :) \ No newline at end of file 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 9e08874af..6cb664fdb 100644 --- a/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md +++ b/1-js/06-advanced-functions/09-call-apply-decorators/04-throttle/task.md @@ -4,16 +4,19 @@ importance: 5 # Throttle decorator -Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper, passing the call to `f` at maximum once per `ms` milliseconds. Those calls that fall into the "cooldown" period, are ignored. +Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper. -**The difference with `debounce` -- if an ignored call is the last during the cooldown, then it executes at the end of the delay.** +When it's called multiple times, it passes the call to `f` at maximum once per `ms` milliseconds. + +The difference with debounce is that it's completely different decorator: +- `debounce` runs the function once after the "cooldown" period. Good for processing the final result. +- `throttle` runs it not more often than given `ms` time. Good for regular updates that shouldn't be very often. Let's check the real-life application to better understand that requirement and to see where it comes from. **For instance, we want to track mouse movements.** In a browser we can setup a function to run at every mouse movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms). - **We'd like to update some information on the web-page when the pointer moves.** ...But updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in updating more often than once per 100ms. 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 373500e13..d0dda4df1 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 @@ -149,8 +149,8 @@ let user = { name: "John" }; let admin = { name: "Admin" }; // use call to pass different objects as "this" -sayHi.call( user ); // this = John -sayHi.call( admin ); // this = Admin +sayHi.call( user ); // John +sayHi.call( admin ); // Admin ``` And here we use `call` to call `say` with the given context and phrase: @@ -209,7 +209,7 @@ To make it all clear, let's see more deeply how `this` is passed along: 2. So when `worker.slow(2)` is executed, the wrapper gets `2` as an argument and `this=worker` (it's the object before dot). 3. Inside the wrapper, assuming the result is not yet cached, `func.call(this, x)` passes the current `this` (`=worker`) and the current argument (`=2`) to the original method. -## Going multi-argument with "func.apply" +## Going multi-argument Now let's make `cachingDecorator` even more universal. Till now it was working only with single-argument functions. @@ -236,7 +236,7 @@ There are many solutions possible: For many practical applications, the 3rd variant is good enough, so we'll stick to it. -Also we need to replace `func.call(this, x)` with `func.call(this, ...arguments)`, to pass all arguments to the wrapped function call, not just the first one. +Also we need to pass not just `x`, but all arguments in `func.call`. Let's recall that in a `function()` we can get a pseudo-array of its arguments as `arguments`, so `func.call(this, x)` should be replaced with `func.call(this, ...arguments)`. Here's a more powerful `cachingDecorator`: @@ -284,6 +284,8 @@ There are two changes: - In the line `(*)` it calls `hash` to create a single key from `arguments`. Here we use a simple "joining" function that turns arguments `(3, 5)` into the key `"3,5"`. More complex cases may require other hashing functions. - Then `(**)` uses `func.call(this, ...arguments)` to pass both the context and all arguments the wrapper got (not just the first one) to the original function. +## func.apply + Instead of `func.call(this, ...arguments)` we could use `func.apply(this, arguments)`. The syntax of built-in method [func.apply](mdn:js/Function/apply) is: @@ -299,18 +301,18 @@ The only syntax difference between `call` and `apply` is that `call` expects a l So these two calls are almost equivalent: ```js -func.call(context, ...args); // pass an array as list with spread operator -func.apply(context, args); // is same as using apply +func.call(context, ...args); // pass an array as list with spread syntax +func.apply(context, args); // is same as using call ``` -There's only a minor difference: +There's only a subtle difference: -- The spread operator `...` allows to pass *iterable* `args` as the list to `call`. +- The spread syntax `...` allows to pass *iterable* `args` as the list to `call`. - The `apply` accepts only *array-like* `args`. -So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works. +So, where we expect an iterable, `call` works, and where we expect an array-like, `apply` works. -And for objects that are both iterable and array-like, like a real array, we technically could use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better. +And for objects that are both iterable and array-like, like a real array, we can use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better. Passing all arguments along with the context to another function is called *call forwarding*. @@ -344,7 +346,7 @@ function hash(args) { } ``` -...Unfortunately, that won't work. Because we are calling `hash(arguments)` and `arguments` object is both iterable and array-like, but not a real array. +...Unfortunately, that won't work. Because we are calling `hash(arguments)`, and `arguments` object is both iterable and array-like, but not a real array. So calling `join` on it would fail, as we can see below: diff --git a/1-js/06-advanced-functions/10-bind/article.md b/1-js/06-advanced-functions/10-bind/article.md index 16a50942d..787c7d68e 100644 --- a/1-js/06-advanced-functions/10-bind/article.md +++ b/1-js/06-advanced-functions/10-bind/article.md @@ -279,7 +279,7 @@ What if we'd like to fix some arguments, but not the context `this`? For example The native `bind` does not allow that. We can't just omit the context and jump to arguments. -Fortunately, a helper function `partial` for binding only arguments can be easily implemented. +Fortunately, a function `partial` for binding only arguments can be easily implemented. Like this: @@ -313,7 +313,7 @@ The result of `partial(func[, arg1, arg2...])` call is a wrapper `(*)` that call - Then gives it `...argsBound` -- arguments from the `partial` call (`"10:00"`) - Then gives it `...args` -- arguments given to the wrapper (`"Hello"`) -So easy to do it with the spread operator, right? +So easy to do it with the spread syntax, right? Also there's a ready [_.partial](https://lodash.com/docs#partial) implementation from lodash library. diff --git a/1-js/07-object-properties/01-property-descriptors/article.md b/1-js/07-object-properties/01-property-descriptors/article.md index e894f0662..3593bffae 100644 --- a/1-js/07-object-properties/01-property-descriptors/article.md +++ b/1-js/07-object-properties/01-property-descriptors/article.md @@ -66,7 +66,7 @@ Object.defineProperty(obj, propertyName, descriptor) : The object and its property to apply the descriptor. `descriptor` -: Property descriptor to apply. +: Property descriptor object to apply. If the property exists, `defineProperty` updates its flags. Otherwise, it creates the property with the given value and flags; in that case, if a flag is not supplied, it is assumed `false`. 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 726529c5b..45b9e70ed 100644 --- a/1-js/07-object-properties/02-property-accessors/article.md +++ b/1-js/07-object-properties/02-property-accessors/article.md @@ -1,11 +1,11 @@ # Property getters and setters -There are two kinds of properties. +There are two kinds of object properties. The first kind is *data properties*. We already know how to work with them. All properties that we've been using until now were data properties. -The second type of properties is something new. It's *accessor properties*. They are essentially functions that work on getting and setting a value, but look like regular properties to an external code. +The second type of properties is something new. It's *accessor properties*. They are essentially functions that execute on getting and setting a value, but look like regular properties to an external code. ## Getters and setters @@ -53,7 +53,7 @@ alert(user.fullName); // John Smith */!* ``` -From outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't *call* `user.fullName` as a function, we *read* it normally: the getter runs behind the scenes. +From the outside, an accessor property looks like a regular one. That's the idea of accessor properties. We don't *call* `user.fullName` as a function, we *read* it normally: the getter runs behind the scenes. As of now, `fullName` has only a getter. If we attempt to assign `user.fullName=`, there will be an error: @@ -94,11 +94,7 @@ alert(user.name); // Alice alert(user.surname); // Cooper ``` -As the result, we have a "virtual" property `fullName`. It is readable and writable, but in fact does not exist. - -```smart header="No way to handle `delete`" -There's no similar method to handle deletion of an accessor property. Only getter/setter methods may exist. -``` +As the result, we have a "virtual" property `fullName`. It is readable and writable. ## Accessor descriptors @@ -138,7 +134,7 @@ alert(user.fullName); // John Smith for(let key in user) alert(key); // name, surname ``` -Please note once again that a property can be either an accessor (has `get/set` methods) or a data property (has a `value`), not both. +Please note that a property can be either an accessor (has `get/set` methods) or a data property (has a `value`), not both. If we try to supply both `get` and `value` in the same descriptor, there will be an error: diff --git a/1-js/08-prototypes/01-prototype-inheritance/article.md b/1-js/08-prototypes/01-prototype-inheritance/article.md index 69e7c5f5c..710390f15 100644 --- a/1-js/08-prototypes/01-prototype-inheritance/article.md +++ b/1-js/08-prototypes/01-prototype-inheritance/article.md @@ -16,7 +16,7 @@ The prototype is a little bit "magical". When we want to read a property from `o The property `[[Prototype]]` is internal and hidden, but there are many ways to set it. -One of them is to use `__proto__`, like this: +One of them is to use the special name `__proto__`, like this: ```js run let animal = { @@ -32,7 +32,7 @@ rabbit.__proto__ = animal; ``` ```smart header="`__proto__` is a historical getter/setter for `[[Prototype]]`" -Please note that `__proto__` is *not the same* as `[[Prototype]]`. That's a getter/setter for it. +Please note that `__proto__` is *not the same* as `[[Prototype]]`. It's a getter/setter for it. It exists for historical reasons. In modern language it is replaced with functions `Object.getPrototypeOf/Object.setPrototypeOf` that also get/set the prototype. We'll study the reasons for that and these functions later. diff --git a/1-js/08-prototypes/02-function-prototype/article.md b/1-js/08-prototypes/02-function-prototype/article.md index c106d1d90..b1ef51826 100644 --- a/1-js/08-prototypes/02-function-prototype/article.md +++ b/1-js/08-prototypes/02-function-prototype/article.md @@ -41,7 +41,7 @@ That's the resulting picture: On the picture, `"prototype"` is a horizontal arrow, meaning a regular property, and `[[Prototype]]` is vertical, meaning the inheritance of `rabbit` from `animal`. ```smart header="`F.prototype` only used at `new F` time" -`F.prototype` property is only used when `new F` is called, it assigns `[[Prototype]]` of the new object. After that, there's no connection between `F.prototype` and the new object. Think of it as a "one-time gift". +`F.prototype` property is only used when `new F` is called, it assigns `[[Prototype]]` of the new object. If, after the creation, `F.prototype` property changes (`F.prototype = `), then new objects created by `new F` will have another object as `[[Prototype]]`, but already existing objects keep the old one. ``` diff --git a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg index caa5ec04b..36cc81cd9 100644 --- a/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg +++ b/1-js/08-prototypes/03-native-prototypes/native-prototypes-classes.svg @@ -1 +1 @@ -toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file +toString: function other object methodsObject.prototypenullslice: function other array methods[[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]][[Prototype]]Array.prototypecall: function other function methodsFunction.prototypetoFixed: function other number methodsNumber.prototype[1, 2, 3]function f(args) { ... }5 \ No newline at end of file diff --git a/1-js/08-prototypes/04-prototype-methods/article.md b/1-js/08-prototypes/04-prototype-methods/article.md index 80f5a956a..d517e1564 100644 --- a/1-js/08-prototypes/04-prototype-methods/article.md +++ b/1-js/08-prototypes/04-prototype-methods/article.md @@ -57,7 +57,6 @@ The descriptors are in the same format as described in the chapter = " in the declaration, and that's it. + +The important difference of class fields is that they are set on individual objects, not `User.prototype`: + +```js run +class User { +*!* + name = "John"; +*/!* +} + +let user = new User(); +alert(user.name); // John +alert(User.prototype.name); // undefined +``` + +Technically, they are processed after the constructor has done it's job, and we can use for them complex expressions and function calls: + +```js run +class User { +*!* + name = prompt("Name, please?", "John"); +*/!* +} + +let user = new User(); +alert(user.name); // John +``` + +### Making bound methods with class fields + +As demonstrated in the chapter functions in JavaScript have a dynamic `this`. It depends on the context of the call. + +So if an object method is passed around and called in another context, `this` won't be a reference to its object any more. + +For instance, this code will show `undefined`: + +```js run +class Button { + constructor(value) { + this.value = value; + } + + click() { + alert(this.value); + } +} + +let button = new Button("hello"); + +*!* +setTimeout(button.click, 1000); // undefined +*/!* +``` + +The problem is called "losing `this`". + +There are two approaches to fixing it, as discussed in the chapter : + +1. Pass a wrapper-function, such as `setTimeout(() => button.click(), 1000)`. +2. Bind the method to object, e.g. in the constructor: + +```js run +class Button { + constructor(value) { + this.value = value; +*!* + this.click = this.click.bind(this); +*/!* + } -alert(User.prototype.sayHi); // placed in User.prototype -alert(User.prototype.name); // undefined, not placed in User.prototype + click() { + alert(this.value); + } +} + +let button = new Button("hello"); + +*!* +setTimeout(button.click, 1000); // hello +*/!* +``` + +Class fields provide a more elegant syntax for the latter solution: + +```js run +class Button { + constructor(value) { + this.value = value; + } +*!* + click = () => { + alert(this.value); + } +*/!* +} + +let button = new Button("hello"); + +setTimeout(button.click, 1000); // hello ``` -The property `name` is not placed into `User.prototype`. Instead, it is created by `new` before calling the constructor, it's a property of the object itself. +The class field `click = () => {...}` creates an independent function on each `Button` object, with `this` bound to the object. Then we can pass `button.click` around anywhere, and it will be called with the right `this`. + +That's especially useful in browser environment, when we need to setup a method as an event listener. ## Summary diff --git a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js index ca613ca5e..be2053cfc 100644 --- a/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js +++ b/1-js/09-classes/02-class-inheritance/2-clock-class-extended/solution.view/extended-clock.js @@ -1,7 +1,7 @@ class ExtendedClock extends Clock { constructor(options) { super(options); - let { precision=1000 } = options; + let { precision = 1000 } = options; this.precision = precision; } diff --git a/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md b/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md index b82a4255e..1d0f98a74 100644 --- a/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md +++ b/1-js/09-classes/02-class-inheritance/3-class-extend-object/task.md @@ -1,4 +1,4 @@ -importance: 5 +importance: 3 --- @@ -38,5 +38,5 @@ class Rabbit extends Object { let rabbit = new Rabbit("Rab"); -alert( rabbit.hasOwnProperty('name') ); // true +alert( rabbit.hasOwnProperty('name') ); // Error ``` diff --git a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg index 2f994f7b7..3471904ab 100644 --- a/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg +++ b/1-js/09-classes/02-class-inheritance/animal-rabbit-extends.svg @@ -1 +1 @@ -constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "White Rabbit"constructorconstructorextends \ No newline at end of file +constructor: Animal run: function stop: functionAnimal.prototypeconstructor: Rabbit hide: functionRabbit.prototypeAnimalRabbitnew Rabbit[[Prototype]][[Prototype]]prototypeprototypename: "White Rabbit"constructorconstructorextends \ No newline at end of file diff --git a/1-js/09-classes/02-class-inheritance/article.md b/1-js/09-classes/02-class-inheritance/article.md index 1000fdd84..3d3c145eb 100644 --- a/1-js/09-classes/02-class-inheritance/article.md +++ b/1-js/09-classes/02-class-inheritance/article.md @@ -16,7 +16,7 @@ class Animal { this.name = name; } run(speed) { - this.speed += speed; + this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { @@ -124,7 +124,7 @@ class Animal { } run(speed) { - this.speed += speed; + this.speed = speed; alert(`${this.name} runs with speed ${this.speed}.`); } @@ -438,7 +438,7 @@ It works as intended, due to `[[HomeObject]]` mechanics. A method, such as `long As we've known before, generally functions are "free", not bound to objects in JavaScript. So they can be copied between objects and called with another `this`. -The very existance of `[[HomeObject]]` violates that principle, because methods remember their objects. `[[HomeObject]]` can't be changed, so this bond is forever. +The very existence of `[[HomeObject]]` violates that principle, because methods remember their objects. `[[HomeObject]]` can't be changed, so this bond is forever. The only place in the language where `[[HomeObject]]` is used -- is `super`. So, if a method does not use `super`, then we can still consider it free and copy between objects. But with `super` things may go wrong. @@ -478,7 +478,7 @@ tree.sayHi(); // I'm an animal (?!?) */!* ``` -A call to `tree.sayHi()` shows "I'm an animal". Definitevely wrong. +A call to `tree.sayHi()` shows "I'm an animal". Definitely wrong. The reason is simple: - In the line `(*)`, the method `tree.sayHi` was copied from `rabbit`. Maybe we just wanted to avoid code duplication? @@ -499,7 +499,7 @@ In the example below a non-method syntax is used for comparison. `[[HomeObject]] ```js run let animal = { - eat: function() { // intentially writing like this instead of eat() {... + eat: function() { // intentionally writing like this instead of eat() {... // ... } }; diff --git a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg index 8ad09f484..f13d441c9 100644 --- a/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg +++ b/1-js/09-classes/02-class-inheritance/super-homeobject-wrong.svg @@ -1 +1 @@ -sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file +sayHiplantsayHitreesayHianimalrabbit[[HomeObject]]sayHi \ No newline at end of file diff --git a/1-js/09-classes/03-static-properties-methods/article.md b/1-js/09-classes/03-static-properties-methods/article.md index dd09a0262..ab08f2ded 100644 --- a/1-js/09-classes/03-static-properties-methods/article.md +++ b/1-js/09-classes/03-static-properties-methods/article.md @@ -20,7 +20,7 @@ User.staticMethod(); // true That actually does the same as assigning it as a property directly: ```js run -class User() { } +class User { } User.staticMethod = function() { alert(this === User); diff --git a/1-js/09-classes/04-private-protected-properties-methods/article.md b/1-js/09-classes/04-private-protected-properties-methods/article.md index 6d77e8ed7..60ed0ef1b 100644 --- a/1-js/09-classes/04-private-protected-properties-methods/article.md +++ b/1-js/09-classes/04-private-protected-properties-methods/article.md @@ -279,7 +279,7 @@ With private fields that's impossible: `this['#name']` doesn't work. That's a sy ## Summary -In terms of OOP, delimiting of the internal interface from the external one is called [encapsulation]("https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)"). +In terms of OOP, delimiting of the internal interface from the external one is called [encapsulation](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)). It gives the following benefits: diff --git a/1-js/09-classes/06-instanceof/instanceof.svg b/1-js/09-classes/06-instanceof/instanceof.svg index 8b63f2070..78bff9f12 100644 --- a/1-js/09-classes/06-instanceof/instanceof.svg +++ b/1-js/09-classes/06-instanceof/instanceof.svg @@ -1 +1 @@ -Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file +Animal.prototypeObject.prototypeRabbit.prototype[[Prototype]]rabbit[[Prototype]][[Prototype]]null[[Prototype]]= Animal.prototype? \ No newline at end of file diff --git a/1-js/09-classes/07-mixins/article.md b/1-js/09-classes/07-mixins/article.md index 2ec196105..60f6f7c4a 100644 --- a/1-js/09-classes/07-mixins/article.md +++ b/1-js/09-classes/07-mixins/article.md @@ -140,7 +140,7 @@ let eventMixin = { * menu.off('select', handler) */ off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; + let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { diff --git a/1-js/09-classes/07-mixins/head.html b/1-js/09-classes/07-mixins/head.html index 77ea38b20..20e3a6354 100644 --- a/1-js/09-classes/07-mixins/head.html +++ b/1-js/09-classes/07-mixins/head.html @@ -18,7 +18,7 @@ * menu.off('select', handler) */ off(eventName, handler) { - let handlers = this._eventHandlers && this._eventHandlers[eventName]; + let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for(let i = 0; i < handlers.length; i++) { if (handlers[i] == handler) { diff --git a/1-js/10-error-handling/1-try-catch/article.md b/1-js/10-error-handling/1-try-catch/article.md index 09b1ee398..7d41b71b1 100644 --- a/1-js/10-error-handling/1-try-catch/article.md +++ b/1-js/10-error-handling/1-try-catch/article.md @@ -4,7 +4,7 @@ No matter how great we are at programming, sometimes our scripts have errors. Th Usually, a script "dies" (immediately stops) in case of an error, printing it to console. -But there's a syntax construct `try..catch` that allows to "catch" errors and, instead of dying, do something more reasonable. +But there's a syntax construct `try..catch` that allows us to "catch" errors so the script can, instead of dying, do something more reasonable. ## The "try..catch" syntax @@ -26,13 +26,13 @@ It works like this: 1. First, the code in `try {...}` is executed. 2. If there were no errors, then `catch(err)` is ignored: the execution reaches the end of `try` and goes on, skipping `catch`. -3. If an error occurs, then `try` execution is stopped, and the control flows to the beginning of `catch(err)`. The `err` variable (can use any name for it) will contain an error object with details about what happened. +3. If an error occurs, then the `try` execution is stopped, and control flows to the beginning of `catch(err)`. The `err` variable (we can use any name for it) will contain an error object with details about what happened. ![](try-catch-flow.svg) -So, an error inside the `try {…}` block does not kill the script: we have a chance to handle it in `catch`. +So, an error inside the `try {…}` block does not kill the script -- we have a chance to handle it in `catch`. -Let's see examples. +Let's look at some examples. - An errorless example: shows `alert` `(1)` and `(2)`: @@ -87,7 +87,7 @@ try { The JavaScript engine first reads the code, and then runs it. The errors that occur on the reading phase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code. -So, `try..catch` can only handle errors that occur in the valid code. Such errors are called "runtime errors" or, sometimes, "exceptions". +So, `try..catch` can only handle errors that occur in valid code. Such errors are called "runtime errors" or, sometimes, "exceptions". ```` @@ -298,7 +298,7 @@ try { *!* alert(e.name); // SyntaxError */!* - alert(e.message); // Unexpected token o in JSON at position 2 + alert(e.message); // Unexpected token b in JSON at position 2 } ``` @@ -353,29 +353,33 @@ try { Of course, everything's possible! Programmers do make mistakes. Even in open-source utilities used by millions for decades -- suddenly a bug may be discovered that leads to terrible hacks. -In our case, `try..catch` is meant to catch "incorrect data" errors. But by its nature, `catch` gets *all* errors from `try`. Here it gets an unexpected error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug. +In our case, `try..catch` is placed to catch "incorrect data" errors. But by its nature, `catch` gets *all* errors from `try`. Here it gets an unexpected error, but still shows the same `"JSON Error"` message. That's wrong and also makes the code more difficult to debug. -Fortunately, we can find out which error we get, for instance from its `name`: +To avoid such problems, we can employ the "rethrowing" technique. The rule is simple: + +**Catch should only process errors that it knows and "rethrow" all others.** + +The "rethrowing" technique can be explained in more detail as: + +1. Catch gets all errors. +2. In the `catch(err) {...}` block we analyze the error object `err`. +2. If we don't know how to handle it, we do `throw err`. + +Usually, we can check the error type using the `instanceof` operator: ```js run try { user = { /*...*/ }; -} catch(e) { +} catch(err) { *!* - alert(e.name); // "ReferenceError" for accessing an undefined variable + if (err instanceof ReferenceError) { */!* + alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable + } } ``` -The rule is simple: - -**Catch should only process errors that it knows and "rethrow" all others.** - -The "rethrowing" technique can be explained in more detail as: - -1. Catch gets all errors. -2. In the `catch(err) {...}` block we analyze the error object `err`. -2. If we don't know how to handle it, we do `throw err`. +We can also get the error class name from `err.name` property. All native errors have it. Another option is to read `err.constructor.name`. In the code below, we use rethrowing so that `catch` only handles `SyntaxError`: @@ -398,7 +402,7 @@ try { } catch(e) { *!* - if (e.name == "SyntaxError") { + if (e instanceof SyntaxError) { alert( "JSON Error: " + e.message ); } else { throw e; // rethrow (*) @@ -425,7 +429,7 @@ function readData() { */!* } catch (e) { // ... - if (e.name != 'SyntaxError') { + if (!(e instanceof SyntaxError)) { *!* throw e; // rethrow (don't know how to deal with it) */!* diff --git a/1-js/10-error-handling/1-try-catch/try-catch-flow.svg b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg index ab17dcda0..ac816e356 100644 --- a/1-js/10-error-handling/1-try-catch/try-catch-flow.svg +++ b/1-js/10-error-handling/1-try-catch/try-catch-flow.svg @@ -1 +1 @@ -BeginNo ErrorsAn error occured in the codeIgnore catch blockIgnore the rest of tryExecute catch blocktry { }// code... \ No newline at end of file +BeginNo ErrorsAn error occured in the codeIgnore catch blockIgnore the rest of tryExecute catch blocktry { }// code... \ No newline at end of file diff --git a/1-js/10-error-handling/2-custom-errors/article.md b/1-js/10-error-handling/2-custom-errors/article.md index b48313322..ff2e4c529 100644 --- a/1-js/10-error-handling/2-custom-errors/article.md +++ b/1-js/10-error-handling/2-custom-errors/article.md @@ -215,11 +215,39 @@ Now custom errors are much shorter, especially `ValidationError`, as we got rid The purpose of the function `readUser` in the code above is "to read the user data". There may occur different kinds of errors in the process. Right now we have `SyntaxError` and `ValidationError`, but in the future `readUser` function may grow and probably generate other kinds of errors. -The code which calls `readUser` should handle these errors. Right now it uses multiple `if`s in the `catch` block, that check the class and handle known errors and rethrow the unknown ones. But if the `readUser` function generates several kinds of errors, then we should ask ourselves: do we really want to check for all error types one-by-one in every code that calls `readUser`? +The code which calls `readUser` should handle these errors. Right now it uses multiple `if`s in the `catch` block, that check the class and handle known errors and rethrow the unknown ones. -Often the answer is "No": the outer code wants to be "one level above all that", it just wants to have some kind of "data reading error" -- why exactly it happened is often irrelevant (the error message describes it). Or, even better, it could have a way to get the error details, but only if we need to. +The scheme is like this: -So let's make a new class `ReadError` to represent such errors. If an error occurs inside `readUser`, we'll catch it there and generate `ReadError`. We'll also keep the reference to the original error in its `cause` property. Then the outer code will only have to check for `ReadError`. +```js +try { + ... + readUser() // the potential error source + ... +} catch (err) { + if (err instanceof ValidationError) { + // handle validation errors + } else if (err instanceof SyntaxError) { + // handle syntax errors + } else { + throw err; // unknown error, rethrow it + } +} +``` + +In the code above we can see two types of errors, but there can be more. + +If the `readUser` function generates several kinds of errors, then we should ask ourselves: do we really want to check for all error types one-by-one every time? + +Often the answer is "No": we'd like to be "one level above all that". We just want to know if there was a "data reading error" -- why exactly it happened is often irrelevant (the error message describes it). Or, even better, we'd like to have a way to get the error details, but only if we need to. + +The technique that we describe here is called "wrapping exceptions". + +1. We'll make a new class `ReadError` to represent a generic "data reading" error. +2. The function `readUser` will catch data reading errors that occur inside it, such as `ValidationError` and `SyntaxError`, and generate a `ReadError` instead. +3. The `ReadError` object will keep the reference to the original error in its `cause` property. + +Then the code that calls `readUser` will only have to check for `ReadError`, not for every kind of data reading errors. And if it needs more details of an error, it can check its `cause` property. Here's the code that defines `ReadError` and demonstrates its use in `readUser` and `try..catch`: @@ -293,7 +321,7 @@ In the code above, `readUser` works exactly as described -- catches syntax and v So the outer code checks `instanceof ReadError` and that's it. No need to list all possible error types. -The approach is called "wrapping exceptions", because we take "low level exceptions" and "wrap" them into `ReadError` that is more abstract and more convenient to use for the calling code. It is widely used in object-oriented programming. +The approach is called "wrapping exceptions", because we take "low level" exceptions and "wrap" them into `ReadError` that is more abstract. It is widely used in object-oriented programming. ## Summary diff --git a/1-js/11-async/01-callbacks/article.md b/1-js/11-async/01-callbacks/article.md index daab93316..9d1a260d5 100644 --- a/1-js/11-async/01-callbacks/article.md +++ b/1-js/11-async/01-callbacks/article.md @@ -2,15 +2,17 @@ # Introduction: callbacks -```warn header="We use browser methods here" -To demonstrate the use of callbacks, promises and other abstract concepts, we'll be using some browser methods; specifically, loading scripts and performing simple document manipulations. +```warn header="We use browser methods in examples here" +To demonstrate the use of callbacks, promises and other abstract concepts, we'll be using some browser methods: specifically, loading scripts and performing simple document manipulations. -If you're not familiar with these methods, and their usage in the examples is confusing, or if you would just like to understand them better, you may want to read a few chapters from the [next part](/document) of the tutorial. +If you're not familiar with these methods, and their usage in the examples is confusing, you may want to read a few chapters from the [next part](/document) of the tutorial. + +Although, we'll try to make things clear anyway. There won't be anything really complex browser-wise. ``` -Many actions in JavaScript are *asynchronous*. In other words, we initiate them now, but they finish later. +Many functions are provided by JavaScript host environments that allow you to schedule *asynchronous* actions. In other words, actions that we initiate now, but they finish later. -For instance, we can schedule such actions using `setTimeout`. +For instance, one such function is the `setTimeout` function. There are other real-world examples of asynchronous actions, e.g. loading scripts and modules (we'll cover them in later chapters). @@ -18,13 +20,15 @@ Take a look at the function `loadScript(src)`, that loads a script with the give ```js function loadScript(src) { + // creates a - - - + table.tBodies[0].append(...sortedRows); + diff --git a/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html b/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html index e41eb229f..9071c88ee 100644 --- a/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html +++ b/2-ui/1-document/07-modifying-document/12-sort-table/source.view/index.html @@ -1,33 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - - - -
NameSurnameAge
JohnSmith10
PeteBrown15
AnnLee5
+ + + + + + + + + + + + + + + + + + + + +
NameSurnameAge
JohnSmith10
PeteBrown15
AnnLee5
.........
- - - - + diff --git a/2-ui/1-document/07-modifying-document/12-sort-table/task.md b/2-ui/1-document/07-modifying-document/12-sort-table/task.md index 41d6fca29..7cdba35bc 100644 --- a/2-ui/1-document/07-modifying-document/12-sort-table/task.md +++ b/2-ui/1-document/07-modifying-document/12-sort-table/task.md @@ -6,33 +6,29 @@ importance: 5 There's a table: +```html run - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + +
NameSurnameAge
JohnSmith10
PeteBrown15
AnnLee5
.........
NameSurnameAge
JohnSmith10
PeteBrown15
AnnLee5
.........
+``` There may be more rows in it. diff --git a/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md b/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md index 67bb5e13d..de8be56e9 100644 --- a/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md +++ b/2-ui/1-document/07-modifying-document/9-calendar-table/solution.md @@ -3,7 +3,7 @@ We'll create the table as a string: `"...
"`, and then assign it t The algorithm: 1. Create the table header with `` and weekday names. -1. Create the date object `d = new Date(year, month-1)`. That's the first day of `month` (taking into account that months in JavaScript start from `0`, not `1`). -2. First few cells till the first day of the month `d.getDay()` may be empty. Let's fill them in with ``. -3. Increase the day in `d`: `d.setDate(d.getDate()+1)`. If `d.getMonth()` is not yet the next month, then add the new cell `` to the calendar. If that's a Sunday, then add a newline "</tr><tr>". -4. If the month has finished, but the table row is not yet full, add empty `` into it, to make it square. +2. Create the date object `d = new Date(year, month-1)`. That's the first day of `month` (taking into account that months in JavaScript start from `0`, not `1`). +3. First few cells till the first day of the month `d.getDay()` may be empty. Let's fill them in with ``. +4. Increase the day in `d`: `d.setDate(d.getDate()+1)`. If `d.getMonth()` is not yet the next month, then add the new cell `` to the calendar. If that's a Sunday, then add a newline "</tr><tr>". +5. If the month has finished, but the table row is not yet full, add empty `` into it, to make it square. diff --git a/2-ui/1-document/07-modifying-document/article.md b/2-ui/1-document/07-modifying-document/article.md index b624da47c..c4796a1d4 100644 --- a/2-ui/1-document/07-modifying-document/article.md +++ b/2-ui/1-document/07-modifying-document/article.md @@ -540,7 +540,7 @@ So if we need to add a lot of text into HTML dynamically, and we're at page load - `"beforeend"` -- insert `html` into `elem`, at the end, - `"afterend"` -- insert `html` right after `elem`. - Also there are also similar methods, `elem.insertAdjacentText` and `elem.insertAdjacentElement`, that insert text strings and elements, but they are rarely used. + Also there are similar methods, `elem.insertAdjacentText` and `elem.insertAdjacentElement`, that insert text strings and elements, but they are rarely used. - To append HTML to the page before it has finished loading: - `document.write(html)` diff --git a/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg b/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg index 1811adea7..6e1fb4874 100644 --- a/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg +++ b/2-ui/1-document/07-modifying-document/before-prepend-append-after.svg @@ -1 +1 @@ -ol.afterol.appendol.prependol.before(…nodes or strings) \ No newline at end of file +ol.afterol.appendol.prependol.before(…nodes or strings) \ No newline at end of file diff --git a/2-ui/1-document/07-modifying-document/insert-adjacent.svg b/2-ui/1-document/07-modifying-document/insert-adjacent.svg index 4b247fd72..64beee037 100644 --- a/2-ui/1-document/07-modifying-document/insert-adjacent.svg +++ b/2-ui/1-document/07-modifying-document/insert-adjacent.svg @@ -1 +1 @@ -ol.insertAdjacentHTML(*, html)afterendbeforeendafterbeginbeforebegin \ No newline at end of file +ol.insertAdjacentHTML(*, html)afterendbeforeendafterbeginbeforebegin \ No newline at end of file diff --git a/2-ui/1-document/08-styles-and-classes/article.md b/2-ui/1-document/08-styles-and-classes/article.md index 34d441ae5..9154d43d6 100644 --- a/2-ui/1-document/08-styles-and-classes/article.md +++ b/2-ui/1-document/08-styles-and-classes/article.md @@ -249,7 +249,7 @@ For instance: ```smart header="Computed and resolved values" There are two concepts in [CSS](https://drafts.csswg.org/cssom/#resolved-values): -1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`. +1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`. 2. A *resolved* style value is the one finally applied to the element. Values like `1em` or `125%` are relative. The browser takes the computed value and makes all units fixed and absolute, for instance: `height:20px` or `font-size:16px`. For geometry properties resolved values may have a floating point, like `width:50.5px`. A long time ago `getComputedStyle` was created to get computed values, but it turned out that resolved values are much more convenient, and the standard changed. diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html index ca9c4d579..8f855ecfa 100755 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/ball-half/index.html @@ -9,7 +9,7 @@ background-color: #00FF00; position: relative; } - + #ball { position: absolute; } @@ -20,7 +20,7 @@

- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
@@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg index f5562f97a..ca8bbc3bd 100644 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/field.svg @@ -1 +1 @@ -(0,0)clientWidth \ No newline at end of file +(0,0)clientWidth \ No newline at end of file diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md index c6fe6c3bb..afa1d8f50 100644 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.md @@ -24,17 +24,22 @@ ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px'; ``` -**Attention: the pitfall!** +Now the ball is finally centered. + +````warn header="Attention: the pitfall!" The code won't work reliably while `` has no width/height: ```html ``` +```` When the browser does not know the width/height of an image (from tag attributes or CSS), then it assumes them to equal `0` until the image finishes loading. -After the first load browser usually caches the image, and on next loads it will have the size immediately. But on the first load the value of `ball.offsetWidth` is `0`. That leads to wrong coordinates. +So the value of `ball.offsetWidth` will be `0` until the image loads. That leads to wrong coordinates in the code above. + +After the first load, the browser usually caches the image, and on reloads it will have the size immediately. But on the first load the value of `ball.offsetWidth` is `0`. We should fix that by adding `width/height` to ``: diff --git a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html index 9ebe6001e..9f21e5421 100755 --- a/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html +++ b/2-ui/1-document/09-size-and-scroll/4-put-ball-in-center/solution.view/index.html @@ -26,7 +26,7 @@ ``` +In the first example, the HTML attribute is used to initialize the `button.onclick`, while in the second example -- the script, that's all the difference. + **As there's only one `onclick` property, we can't assign more than one event handler.** In the example below adding a handler with JavaScript overwrites the existing handler: @@ -124,16 +124,6 @@ In the example below adding a handler with JavaScript overwrites the existing ha ``` -By the way, we can assign an existing function as a handler directly: - -```js -function sayThanks() { - alert('Thanks!'); -} - -elem.onclick = sayThanks; -``` - To remove a handler -- assign `elem.onclick = null`. ## Accessing the element: this @@ -150,7 +140,17 @@ In the code below `button` shows its contents using `this.innerHTML`: If you're starting to work with events -- please note some subtleties. -**The function should be assigned as `sayThanks`, not `sayThanks()`.** +We can set an existing function as a handler: + +```js +function sayThanks() { + alert('Thanks!'); +} + +elem.onclick = sayThanks; +``` + +But be careful: the function should be assigned as `sayThanks`, not `sayThanks()`. ```js // right @@ -160,7 +160,7 @@ button.onclick = sayThanks; button.onclick = sayThanks(); ``` -If we add parentheses, `sayThanks()` -- is a function call. So the last line actually takes the *result* of the function execution, that is `undefined` (as the function returns nothing), and assigns it to `onclick`. That doesn't work. +If we add parentheses, then `sayThanks()` becomes is a function call. So the last line actually takes the *result* of the function execution, that is `undefined` (as the function returns nothing), and assigns it to `onclick`. That doesn't work. ...On the other hand, in the markup we do need the parentheses: @@ -168,21 +168,17 @@ If we add parentheses, `sayThanks()` -- is a function call. So the last line ac ``` -The difference is easy to explain. When the browser reads the attribute, it creates a handler function with *body from its content*: `sayThanks()`. +The difference is easy to explain. When the browser reads the attribute, it creates a handler function with body from the attribute content. So the markup generates this property: ```js button.onclick = function() { *!* - sayThanks(); // the attribute content + sayThanks(); // <-- the attribute content goes here */!* }; ``` -**Use functions, not strings.** - -The assignment `elem.onclick = "alert(1)"` would work too. It works for compatibility reasons, but is strongly not recommended. - **Don't use `setAttribute` for handlers.** Such a call won't work: @@ -201,7 +197,7 @@ Assign a handler to `elem.onclick`, not `elem.ONCLICK`, because DOM properties a The fundamental problem of the aforementioned ways to assign handlers -- we can't assign multiple handlers to one event. -For instance, one part of our code wants to highlight a button on click, and another one wants to show a message. +Let's say, one part of our code wants to highlight a button on click, and another one wants to show a message on the same click. We'd like to assign two event handlers for that. But a new DOM property will overwrite the existing one: @@ -211,12 +207,12 @@ input.onclick = function() { alert(1); } input.onclick = function() { alert(2); } // replaces the previous handler ``` -Web-standard developers understood that long ago and suggested an alternative way of managing handlers using special methods `addEventListener` and `removeEventListener`. They are free of such a problem. +Developers of web standards understood that long ago and suggested an alternative way of managing handlers using special methods `addEventListener` and `removeEventListener`. They are free of such a problem. The syntax to add a handler: ```js -element.addEventListener(event, handler[, options]); +element.addEventListener(event, handler, [options]); ``` `event` @@ -229,13 +225,12 @@ element.addEventListener(event, handler[, options]); : An additional optional object with properties: - `once`: if `true`, then the listener is automatically removed after it triggers. - `capture`: the phase where to handle the event, to be covered later in the chapter . For historical reasons, `options` can also be `false/true`, that's the same as `{capture: false/true}`. - - `passive`: if `true`, then the handler will not `preventDefault()`, we'll cover that later in . - + - `passive`: if `true`, then the handler will not call `preventDefault()`, we'll explain that later in . To remove the handler, use `removeEventListener`: ```js -element.removeEventListener(event, handler[, options]); +element.removeEventListener(event, handler, [options]); ``` ````warn header="Removal requires the same function" @@ -249,7 +244,7 @@ elem.addEventListener( "click" , () => alert('Thanks!')); elem.removeEventListener( "click", () => alert('Thanks!')); ``` -The handler won't be removed, because `removeEventListener` gets another function -- with the same code, but that doesn't matter. +The handler won't be removed, because `removeEventListener` gets another function -- with the same code, but that doesn't matter, as it's a different function object. Here's the right way: @@ -291,47 +286,33 @@ Multiple calls to `addEventListener` allow to add multiple handlers, like this: As we can see in the example above, we can set handlers *both* using a DOM-property and `addEventListener`. But generally we use only one of these ways. ````warn header="For some events, handlers only work with `addEventListener`" -There exist events that can't be assigned via a DOM-property. Must use `addEventListener`. - -For instance, the event `transitionend` (CSS animation finished) is like that. - -Try the code below. In most browsers only the second handler works, not the first one. - -```html run - +There exist events that can't be assigned via a DOM-property. Only with `addEventListener`. - +For instance, the `DOMContentLoaded` event, that triggers when the document is loaded and DOM is built. - +```js +// this way it works +document.addEventListener("DOMContentLoaded", function() { + alert("DOM built"); +}); ``` +So `addEventListener` is more universal. Although, such events are an exception rather than the rule. ```` ## Event object -To properly handle an event we'd want to know more about what's happened. Not just a "click" or a "keypress", but what were the pointer coordinates? Which key was pressed? And so on. +To properly handle an event we'd want to know more about what's happened. Not just a "click" or a "keydown", but what were the pointer coordinates? Which key was pressed? And so on. When an event happens, the browser creates an *event object*, puts details into it and passes it as an argument to the handler. -Here's an example of getting mouse coordinates from the event object: +Here's an example of getting pointer coordinates from the event object: ```html run @@ -354,11 +335,11 @@ Some properties of `event` object: : Element that handled the event. That's exactly the same as `this`, unless the handler is an arrow function, or its `this` is bound to something else, then we can get the element from `event.currentTarget`. `event.clientX / event.clientY` -: Window-relative coordinates of the cursor, for mouse events. +: Window-relative coordinates of the cursor, for pointer events. -There are more properties. They depend on the event type, so we'll study them later when we come to different events in details. +There are more properties. Many of them depend on the event type: keyboard events have one set of properties, pointer events - another one, we'll study them later when we come to different events in details. -````smart header="The event object is also accessible from HTML" +````smart header="The event object is also available in HTML handlers" If we assign a handler in HTML, we can also use the `event` object, like this: ```html autorun height=60 @@ -380,15 +361,17 @@ For instance: ``` -As we can see, when `addEventListener` receives an object as the handler, it calls `object.handleEvent(event)` in case of an event. +As we can see, when `addEventListener` receives an object as the handler, it calls `obj.handleEvent(event)` in case of an event. We could also use a class for that: @@ -462,7 +445,7 @@ HTML attributes are used sparingly, because JavaScript in the middle of an HTML DOM properties are ok to use, but we can't assign more than one handler of the particular event. In many cases that limitation is not pressing. -The last way is the most flexible, but it is also the longest to write. There are few events that only work with it, for instance `transtionend` and `DOMContentLoaded` (to be covered). Also `addEventListener` supports objects as event handlers. In that case the method `handleEvent` is called in case of the event. +The last way is the most flexible, but it is also the longest to write. There are few events that only work with it, for instance `transitionend` and `DOMContentLoaded` (to be covered). Also `addEventListener` supports objects as event handlers. In that case the method `handleEvent` is called in case of the event. No matter how you assign the handler -- it gets an event object as the first argument. That object contains the details about what's happened. diff --git a/2-ui/2-events/02-bubbling-and-capturing/article.md b/2-ui/2-events/02-bubbling-and-capturing/article.md index 1ac989c79..decc25154 100644 --- a/2-ui/2-events/02-bubbling-and-capturing/article.md +++ b/2-ui/2-events/02-bubbling-and-capturing/article.md @@ -181,7 +181,7 @@ The code sets click handlers on *every* element in the document to see which one If you click on `

`, then the sequence is: 1. `HTML` -> `BODY` -> `FORM` -> `DIV` (capturing phase, the first listener): -2. `P` (target phrase, triggers two times, as we've set two listeners: capturing and bubbling) +2. `P` (target phase, triggers two times, as we've set two listeners: capturing and bubbling) 3. `DIV` -> `FORM` -> `BODY` -> `HTML` (bubbling phase, the second listener). There's a property `event.eventPhase` that tells us the number of the phase on which the event was caught. But it's rarely used, because we usually know it in the handler. @@ -204,7 +204,7 @@ elem.addEventListener("click", e => alert(2)); When an event happens -- the most nested element where it happens gets labeled as the "target element" (`event.target`). -- Then the event moves down from the document root to `event.target`, calling handlers assigned with `addEventListener(...., true)` on the way (`true` is a shorthand for `{capture: true}`). +- Then the event moves down from the document root to `event.target`, calling handlers assigned with `addEventListener(..., true)` on the way (`true` is a shorthand for `{capture: true}`). - Then handlers are called on the target element itself. - Then the event bubbles up from `event.target` up to the root, calling handlers assigned using `on` and `addEventListener` without the 3rd argument or with the 3rd argument `false/{capture:false}`. diff --git a/2-ui/2-events/02-bubbling-and-capturing/event-order-bubbling.svg b/2-ui/2-events/02-bubbling-and-capturing/event-order-bubbling.svg index 580022a9f..e3d749898 100644 --- a/2-ui/2-events/02-bubbling-and-capturing/event-order-bubbling.svg +++ b/2-ui/2-events/02-bubbling-and-capturing/event-order-bubbling.svg @@ -1 +1 @@ -123Most deeply nested element \ No newline at end of file +123Most deeply nested element \ No newline at end of file diff --git a/2-ui/2-events/03-event-delegation/4-behavior-tooltip/task.md b/2-ui/2-events/03-event-delegation/4-behavior-tooltip/task.md index 73102a92b..3001b9915 100644 --- a/2-ui/2-events/03-event-delegation/4-behavior-tooltip/task.md +++ b/2-ui/2-events/03-event-delegation/4-behavior-tooltip/task.md @@ -22,8 +22,10 @@ In this task we assume that all elements with `data-tooltip` have only text insi Details: +- The distance between the element and the tooltip should be `5px`. +- The tooltip should be centered relative to the element, if possible. - The tooltip should not cross window edges. Normally it should be above the element, but if the element is at the page top and there's no space for the tooltip, then below it. -- The tooltip is given in the `data-tooltip` attribute. It can be arbitrary HTML. +- The tooltip content is given in the `data-tooltip` attribute. It can be arbitrary HTML. You'll need two events here: - `mouseover` triggers when a pointer comes over an element. diff --git a/2-ui/2-events/03-event-delegation/article.md b/2-ui/2-events/03-event-delegation/article.md index 3d8beda0f..df086f24b 100644 --- a/2-ui/2-events/03-event-delegation/article.md +++ b/2-ui/2-events/03-event-delegation/article.md @@ -101,8 +101,8 @@ table.onclick = function(event) { Explanations: 1. The method `elem.closest(selector)` returns the nearest ancestor that matches the selector. In our case we look for `` on the way up from the source element. -2. If `event.target` is not inside any ``, then the call returns `null`, and we don't have to do anything. -3. In case of nested tables, `event.target` may be a `` lying outside of the current table. So we check if that's actually *our table's* ``. +2. If `event.target` is not inside any ``, then the call returns immediately, as there's nothing to do. +3. In case of nested tables, `event.target` may be a ``, but lying outside of the current table. So we check if that's actually *our table's* ``. 4. And, if it's so, then highlight it. As the result, we have a fast, efficient highlighting code, that doesn't care about the total number of `` in the table. @@ -121,7 +121,7 @@ The first idea may be to assign a separate handler to each button. But there's a The handler reads the attribute and executes the method. Take a look at the working example: -```html autorun height=60 run +```html autorun height=60 run untrusted